diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 00000000..4b0b23a9 --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,71 @@ +on: + push: + branches: [main, auto, canary] + pull_request: + branches: + - main + +name: Test coverage + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + coverage: + name: Collect test coverage + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: + - ubuntu-latest + - windows-latest + # nightly rust might break from time to time + continue-on-error: true + env: + RUSTFLAGS: -D warnings + CARGO_TERM_COLOR: always + steps: + - name: Set temp directory on Windows + if: ${{ runner.os == 'Windows' }} + run: | + mkdir -p "D:\a\_temp" + echo "TEMP=D:\a\_temp" >> $GITHUB_ENV + echo "TMP=D:\a\_temp" >> $GITHUB_ENV + shell: bash + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + # Nightly Rust is used for cargo llvm-cov --doc below. + - uses: dtolnay/rust-toolchain@4fd1da8b0805d2d2e936788875a7d65dbd677dc2 # nightly + with: + default: true + components: llvm-tools-preview + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + + - name: Install latest nextest release + uses: taiki-e/install-action@82cd3e7658a6f96c86c0234aeeda1748937cb0a1 # v2.85.13 + with: + tool: nextest + - name: Install cargo-llvm-cov + uses: taiki-e/install-action@82cd3e7658a6f96c86c0234aeeda1748937cb0a1 # v2.85.13 + with: + tool: cargo-llvm-cov + + - name: Collect coverage data + env: + RUSTUP_TOOLCHAIN: nightly + # Generate separate reports for nextest and doctests, and combine them. + run: | + cargo llvm-cov --no-report nextest + cargo llvm-cov --no-report --doc + cargo llvm-cov report --doctests --lcov --output-path lcov.info + - name: Upload coverage data to codecov + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} # zizmor: ignore[secrets-outside-env] + with: + files: lcov.info diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 5ced168d..6d7d163c 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Run sccache-cache - uses: mozilla-actions/sccache-action@v0.0.3 + uses: mozilla-actions/sccache-action@v0.0.11 - uses: actions/checkout@v3 - name: Run rust format uses: actions-rs/toolchain@v1.0.6 @@ -25,30 +25,5 @@ jobs: profile: minimal default: true components: rustfmt - - uses: Swatinem/rust-cache@v2.8.1 + - uses: Swatinem/rust-cache@v2.9.2 - run: cargo fmt -- --check - - run_test: - name: Run tests - runs-on: ubuntu-latest - steps: - - name: Run sccache-cache - uses: mozilla-actions/sccache-action@v0.0.9 - with: - version: "v0.10.0" - - uses: actions/checkout@v3 - - uses: Swatinem/rust-cache@v2.8.1 - - name: Install tarpaulin - run: | - curl -s https://api.github.com/repos/xd009642/tarpaulin/releases/latest | \ - grep "browser_download_url.*x86_64-unknown-linux-musl.tar.gz" | cut -d : -f 2,3 | tr -d \" | wget -qi - - tar -xzf cargo-tarpaulin-*.tar.gz - mv cargo-tarpaulin ~/.cargo/bin/ - - name: Run tests with tarpaulin - run: cargo tarpaulin --workspace --exclude tui --exclude shared --exclude db --out Xml - - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@v3 - with: - token: ${{ secrets.CODECOV_TOKEN }} - file: cobertura.xml - verbose: true diff --git a/.rot/known_issues.json b/.rot/known_issues.json new file mode 100644 index 00000000..19827bdd --- /dev/null +++ b/.rot/known_issues.json @@ -0,0 +1,4 @@ +{ + "version": 1, + "issues": {} +} \ No newline at end of file diff --git a/.rot/snapshot.json b/.rot/snapshot.json new file mode 100644 index 00000000..685c093b --- /dev/null +++ b/.rot/snapshot.json @@ -0,0 +1,6516 @@ +{ + "version": 1, + "generated_at": 1790690430, + "entries": { + "app/src/modifier/delete_tx.rs|crate::delete_tx#free:29abf9b3d651": { + "kind": "Free", + "file": "app/src/modifier/delete_tx.rs", + "item_path": "crate::delete_tx#free:29abf9b3d651", + "comment_kind": "Line", + "comment_text": "// Reverse the transaction effect on balances.", + "comment_hash": "29abf9b3d65104b889567e4fb4153d2558943ffd4ae80968b6a578b3c7defd5c", + "body_text": "match &tx.tx_type {\n TxType::Income | TxType::Borrow | TxType::LendRepay => {}\n TxType::Expense | TxType::Lend | TxType::BorrowRepay => {}\n TxType::Transfer => {}\n }\nTx::delete_tx(tx.id, db_conn)?;\nTxTag::delete_by_tx_id(tx.id, db_conn)?;\nfor balance in final_balance_updates {}\nfor balance in balance_to_update {}\ntidy_balances(tx.date.date(), db_conn)?;\nOk(())\n}", + "body_hash": "ef94c825e98c935eb5c60f7126b25ac306a91e761a81a9f9344aaf74be8be4b9" + }, + "app/src/modifier/new_activity.rs|crate::activity_edit_tx#free:9f41e4a73be4": { + "kind": "Free", + "file": "app/src/modifier/new_activity.rs", + "item_path": "crate::activity_edit_tx#free:9f41e4a73be4", + "comment_kind": "Line", + "comment_text": "// The new tx row is inserted first so the activity view lists it above the old one", + "comment_hash": "9f41e4a73be416eba0e8b01a4590e066319b5c814ae2c47423d6efe7fc4ca31d", + "body_text": "let new_tx_activity = NewActivityTx::new_from_new_tx(new_tx, new_activity.id).insert(conn)?;\nlet old_tx_activity =\n NewActivityTx::new_from_full_tx(old_tx, false, new_activity.id).insert(conn)?;\nlet mut old_tag_list = Vec::new();\nfor tag in &old_tx.tags {}\nlet mut new_tag_list = split_tags(tags);\nif new_tag_list.is_empty() {}\nlet mut new_tags = Vec::new();\nfor tag in new_tag_list {}\nActivityTxTag::insert_batch(new_tags, conn)?;\nActivityTxTag::insert_batch(old_tag_list, conn)?;\nOk(())\n}", + "body_hash": "3c25c6034eb77de315fbbe1dff494c209c362ad18cff7b0392fa8c304ecdd468" + }, + "app/src/modifier/recurring_tx.rs|crate#free:2fbad60a97b6": { + "kind": "Free", + "file": "app/src/modifier/recurring_tx.rs", + "item_path": "crate#free:2fbad60a97b6", + "comment_kind": "Line", + "comment_text": "// If day is set to 31 but the month has 28 or 30 days, work with the minimum of the two so it\n// doesn't start pointing to an invalid day of the month.", + "comment_hash": "2fbad60a97b6f801eee9f4f19da7ec1d7eef19b8ff2cbe7591f79f99ed8e569c", + "body_text": "fn clamp_day(year: i32, month: u32, day: i32) -> NaiveDate {\n let clamped_day = (day as u32).min(last_day_of_month(year, month));\n\n NaiveDate::from_ymd_opt(year, month, clamped_day).unwrap()\n}\nfn step_month(year: i32, month: u32, months_to_add: i32) -> (i32, u32) {\n let start = NaiveDate::from_ymd_opt(year, month, 1).unwrap();\n\n let stepped = start\n .checked_add_months(Months::new(months_to_add as u32))\n .unwrap();\n\n (stepped.year(), stepped.month())\n}", + "body_hash": "4f8b15562c5d251cccdab435216327b8531cd81120dc49d961770470b2be7e76" + }, + "app/src/modifier/recurring_tx.rs|crate::advance_date": { + "kind": "Function", + "file": "app/src/modifier/recurring_tx.rs", + "item_path": "crate::advance_date", + "comment_kind": "OuterDoc", + "comment_text": " Advances one interval forward from an already-valid occurrence date. `recur_value` must\n\n be `Some` for Weekly/Monthly/Yearly and `recur_month` must be `Some` for Yearly\n", + "comment_hash": "9a6188c7b2e78a2cda7823a84ba37dcf92074f35696fe97278356fb7dd21e1ff", + "body_text": "pub(crate) fn advance_date(\n current: NaiveDate,\n frequency: RecurrenceFrequency,\n interval: i32,\n recur_value: Option,\n recur_month: Option,\n) -> NaiveDate {\n match frequency {\n RecurrenceFrequency::Daily => current + Days::new(interval as u64),\n RecurrenceFrequency::Weekly => current + Days::new(7 * interval as u64),\n RecurrenceFrequency::Monthly => {\n let (year, month) = step_month(current.year(), current.month(), interval);\n clamp_day(year, month, recur_value.unwrap())\n }\n RecurrenceFrequency::Yearly => clamp_day(\n current.year() + interval,\n recur_month.unwrap() as u32,\n recur_value.unwrap(),\n ),\n }\n}", + "body_hash": "b72bb5fbc3fcd24a5cce2470fef5846254b5b9f816031df711185d74b3eb5879" + }, + "app/src/modifier/recurring_tx.rs|crate::last_day_of_month": { + "kind": "Function", + "file": "app/src/modifier/recurring_tx.rs", + "item_path": "crate::last_day_of_month", + "comment_kind": "OuterDoc", + "comment_text": " Returns the last valid day of `year`-`month`, so a stored day like 31 can be clamped\n\n down for months that don't have it (e.g. February).\n", + "comment_hash": "a7a46f0d551a7b76d31f6e6156b4e155ca63592c42b432fa340812b238529d54", + "body_text": "fn last_day_of_month(year: i32, month: u32) -> u32 {\n let next_month_first = if month == 12 {\n NaiveDate::from_ymd_opt(year + 1, 1, 1)\n } else {\n NaiveDate::from_ymd_opt(year, month + 1, 1)\n }\n .unwrap();\n\n (next_month_first - Days::new(1)).day()\n}", + "body_hash": "ad1fd3ee293bab0932d74f25446192160258922fb688f1af4fef40515637d189" + }, + "app/src/modifier/recurring_tx.rs|crate::tests::advance_date_monthly_does_not_drift_after_clamping#free:b6244ae5a21f": { + "kind": "Free", + "file": "app/src/modifier/recurring_tx.rs", + "item_path": "crate::tests::advance_date_monthly_does_not_drift_after_clamping#free:b6244ae5a21f", + "comment_kind": "Line", + "comment_text": "// 31st of every month: Jan 31 -> Feb 28 (clamped) -> back to Mar 31 (no drift)", + "comment_hash": "b6244ae5a21ff5ac5567f96c987e281248a335d9823aab4ce922fa5f044d5fea", + "body_text": "let jan_31 = date(2026, 1, 31);\nlet feb = advance_date(jan_31, RecurrenceFrequency::Monthly, 1, Some(31), None);\nassert_eq!(feb, date(2026, 2, 28));\nlet mar = advance_date(feb, RecurrenceFrequency::Monthly, 1, Some(31), None);\nassert_eq!(mar, date(2026, 3, 31));\n}", + "body_hash": "f94cc4d04357dd669e923b6bc96f2fc31923e3f7ed2e39e3dbaa8d305f7aa23a" + }, + "app/src/modifier/recurring_tx.rs|crate::tests::advance_date_monthly_interval_crosses_year_boundary#free:18754975190e": { + "kind": "Free", + "file": "app/src/modifier/recurring_tx.rs", + "item_path": "crate::tests::advance_date_monthly_interval_crosses_year_boundary#free:18754975190e", + "comment_kind": "Line", + "comment_text": "// Quarterly (every 3 months) starting in November", + "comment_hash": "18754975190e416fe2dd4fd6650d1fa381ce872850441d8816f23ab1ae489fc4", + "body_text": "let nov = date(2026, 11, 15);\nlet next = advance_date(nov, RecurrenceFrequency::Monthly, 3, Some(15), None);\nassert_eq!(next, date(2027, 2, 15));\n}", + "body_hash": "dc9cd61fa113ffd53a16b0399d28fc9a3a90b6139ab698d3e9de19c85f9b7d38" + }, + "app/src/modifier/recurring_tx.rs|crate::tests::advance_date_weekly_preserves_weekday_across_month_boundary#free:354267dd5632": { + "kind": "Free", + "file": "app/src/modifier/recurring_tx.rs", + "item_path": "crate::tests::advance_date_weekly_preserves_weekday_across_month_boundary#free:354267dd5632", + "comment_kind": "Line", + "comment_text": "// 2026-01-28 is a Wednesday, every-2-weeks should land on the next Wednesday twice over", + "comment_hash": "354267dd5632d31033a6719b3df50ac0095d9fd271b2144252558def371b0f9f", + "body_text": "let start = date(2026, 1, 28);\nlet next = advance_date(\n start,\n RecurrenceFrequency::Weekly,\n 2,\n Some(start.weekday().num_days_from_sunday() as i32),\n None,\n );\nassert_eq!(next, date(2026, 2, 11));\nassert_eq!(next.weekday(), start.weekday());\n}", + "body_hash": "cb97535b5c05e4781c367f0f4ec38e20fda6c2962bf38975b2ebe8162123d1a1" + }, + "app/src/modifier/recurring_tx.rs|crate::tests::first_occurrence_weekly_rolls_forward_to_chosen_weekday#free:7860c26ed7eb": { + "kind": "Free", + "file": "app/src/modifier/recurring_tx.rs", + "item_path": "crate::tests::first_occurrence_weekly_rolls_forward_to_chosen_weekday#free:7860c26ed7eb", + "comment_kind": "Line", + "comment_text": "// The start date's own weekday doesn't constrain which day can be chosen - it's\n// just an anchor. Picking a day 2 days ahead in the week should roll forward, not error.", + "comment_hash": "7860c26ed7ebc510ce4145f06924e46768b5cad5e96dfadf1b4b2d2fae81076d", + "body_text": "let start = date(2026, 3, 14);\nlet start_weekday = start.weekday().num_days_from_sunday() as i32;\nlet target_weekday = (start_weekday + 2).rem_euclid(7);\nlet first = first_occurrence_on_or_after(\n start,\n RecurrenceFrequency::Weekly,\n Some(target_weekday),\n None,\n );\nassert_eq!(first, start + Days::new(2));\n}", + "body_hash": "3ca93305dbd013ed5a4d8a2f953fa909e0bb0ce5b7f6179e56019b4e58f3985f" + }, + "app/src/modifier/recurring_tx.rs|crate::tests::first_occurrence_weekly_wraps_to_next_week_when_day_already_passed#free:9e9b2a61b87c": { + "kind": "Free", + "file": "app/src/modifier/recurring_tx.rs", + "item_path": "crate::tests::first_occurrence_weekly_wraps_to_next_week_when_day_already_passed#free:9e9b2a61b87c", + "comment_kind": "Line", + "comment_text": "// A day that's \"behind\" in the week relative to start wraps all the way to next week", + "comment_hash": "9e9b2a61b87cfec6ef6b458bd80f1afacc0a49e09768382724b9e8f9cc7248a3", + "body_text": "let target_weekday = (start_weekday + 6).rem_euclid(7);\nlet first = first_occurrence_on_or_after(\n start,\n RecurrenceFrequency::Weekly,\n Some(target_weekday),\n None,\n );\nassert_eq!(first, start + Days::new(6));\n}", + "body_hash": "d1f9647d6c11d22bfe61c756bf86eb65195d83db4be5530857cc6cef4db122e0" + }, + "app/src/modifier/shared.rs|crate::tidy_balances#free:116f1185eaac": { + "kind": "Free", + "file": "app/src/modifier/shared.rs", + "item_path": "crate::tidy_balances#free:116f1185eaac", + "comment_kind": "Line", + "comment_text": "// A bad delete can corrupt the final balance while the monthly ones stay\n// correct, so whenever the two disagree, trust the latest monthly balance.\n// This issue is from an older version which has since been fixed and should not occur again.\n// This is just as a safety net.", + "comment_hash": "116f1185eaacbdb15a1323d7895a7db6b123a274a8fdf1a8bf6dcca1d6501e02", + "body_text": "let mut final_balance = Balance::get_final_balance(db_conn)?;\nlet balance_highest_date = Balance::get_balance_highest_date(db_conn)?;\nfor balance in balance_highest_date {}\nOk(())\n}", + "body_hash": "670ad62afce072bc0001e35deb4b8138ca235132317db89ab0ccb16a74290ca3" + }, + "app/src/modifier/shared.rs|crate::tidy_recursive": { + "kind": "Function", + "file": "app/src/modifier/shared.rs", + "item_path": "crate::tidy_recursive", + "comment_kind": "OuterDoc", + "comment_text": " Recursively tidy balances from the given date to the max date\n", + "comment_hash": "c80c3677a19eefcaa962f69f8f5ceeb02ba3732207cab398617cbf5f13581d0d", + "body_text": "fn tidy_recursive(\n max_date: NaiveDate,\n date: NaiveDate,\n db_conn: &mut impl ConnCache,\n) -> Result<()> {\n let nature = FetchNature::Monthly;\n\n let txs = Tx::get_txs(date, nature, db_conn)?;\n\n let current_balance = Balance::get_balance(date, nature, db_conn)?;\n\n let mut last_balance = Balance::get_last_balance(date, nature, db_conn)?;\n\n for tx in txs {\n match tx.tx_type.as_str().into() {\n TxType::Income | TxType::Borrow | TxType::LendRepay => {\n let method_id = tx.from_method;\n *last_balance.get_mut(&method_id).unwrap() += tx.amount;\n }\n TxType::Expense | TxType::Lend | TxType::BorrowRepay => {\n let method_id = tx.from_method;\n *last_balance.get_mut(&method_id).unwrap() -= tx.amount;\n }\n\n TxType::Transfer => {\n let from_method_id = tx.from_method;\n let to_method_id = tx.to_method.as_ref().unwrap();\n\n *last_balance.get_mut(&from_method_id).unwrap() -= tx.amount;\n *last_balance.get_mut(to_method_id).unwrap() += tx.amount;\n }\n }\n }\n\n let mut to_insert_balance = Vec::new();\n\n for mut balance in current_balance {\n let method_id = balance.method_id;\n let last_balance = *last_balance.get(&method_id).unwrap();\n\n if balance.balance != last_balance {\n balance.balance = last_balance.value();\n to_insert_balance.push(balance);\n }\n }\n\n for to_insert in to_insert_balance {\n to_insert.insert(db_conn)?;\n }\n\n if date <= max_date {\n let next_date = date + Months::new(1);\n tidy_recursive(max_date, next_date, db_conn)?;\n }\n\n Ok(())\n}", + "body_hash": "baebdc813a8f1fee48eb77b92976ee2b976914eaee9b567fe51a6228cefb6cd9" + }, + "app/src/ui_helper/autofiller.rs|crate::impl Autofiller<'a>::recur_value": { + "kind": "Function", + "file": "app/src/ui_helper/autofiller.rs", + "item_path": "crate::impl Autofiller<'a>::recur_value", + "comment_kind": "OuterDoc", + "comment_text": " Only Weekly has a name to complete (the day of the week). Monthly/Yearly take a plain\n\n day-of-month number and Daily takes nothing, so those never get a suggestion.\n", + "comment_hash": "f02a4438df2f0daf5b5d24eadc3e7504ea494181baa60787791de23d98920d6e", + "body_text": "#[must_use]\npub fn recur_value(&self, user_input: &str, frequency: RecurrenceFrequency) -> String {\n if frequency != RecurrenceFrequency::Weekly {\n return String::new();\n }\n\n let weekdays = WEEKDAY_NAMES\n .iter()\n .map(std::string::ToString::to_string)\n .collect::>();\n\n suggest_from_options(user_input, &weekdays)\n }", + "body_hash": "b0b28fde46e93784d6669f60c78f571b73a4fbd816b77223a51644e68f9d15ec" + }, + "app/src/ui_helper/autofiller.rs|crate::impl Autofiller<'a>::tags": { + "kind": "Function", + "file": "app/src/ui_helper/autofiller.rs", + "item_path": "crate::impl Autofiller<'a>::tags", + "comment_kind": "OuterDoc", + "comment_text": " Tags are comma separated, so only the tag currently being typed (the last one) is completed\n", + "comment_hash": "032e2a768f5b4616f77baeb2760f350cec7714dce69e4d04c0c4aa9cb4473e68", + "body_text": "#[must_use]\npub fn tags(&self, user_input: &str) -> String {\n let tags = self\n .conn\n .cache()\n .tags\n .values()\n .map(|m| m.name.clone())\n .collect::>();\n\n let last_value = user_input.rsplit(',').next().unwrap_or_default().trim();\n\n suggest_from_options(last_value, &tags)\n }", + "body_hash": "279fe3ead5600362af742639d89ce7ab73c6a724d449fbcacb83c31d23d9ec73" + }, + "app/src/ui_helper/shared.rs|crate::get_best_match": { + "kind": "Function", + "file": "app/src/ui_helper/shared.rs", + "item_path": "crate::get_best_match", + "comment_kind": "OuterDoc", + "comment_text": " Uses the normalized Levenshtein distance to find the closest match for\n\n `data` among `matching_set`\n", + "comment_hash": "fba69057f8b4f29231731eb50ae503442bcc2554366e461cd4642d7a12d78b71", + "body_text": "#[must_use]\npub(crate) fn get_best_match(data: &str, matching_set: &[String]) -> String {\n let mut best_match = &matching_set[0];\n let mut best_score = -1.0;\n\n for x in matching_set {\n let new_score = normalized_levenshtein(&x.to_lowercase(), &data.to_lowercase());\n\n if new_score > best_score {\n best_match = x;\n best_score = new_score;\n }\n }\n best_match.clone()\n}", + "body_hash": "c16574854533ba50f538982a9e0103c35cb6ed05d07f479829fcf844b2f018ed" + }, + "app/src/ui_helper/stepper.rs|crate::impl Stepper<'a>::tag#free:0a5751a450ad": { + "kind": "Free", + "file": "app/src/ui_helper/stepper.rs", + "item_path": "crate::impl Stepper<'a>::tag#free:0a5751a450ad", + "comment_kind": "Line", + "comment_text": "// If current tag is empty\n// select the first possible tag if available\n// Tags are separated by comma. Collect all the tags", + "comment_hash": "0a5751a450ad8118a82228c0460f3c4da17175fb42432730aac55e12d5195c15", + "body_text": "let mut current_tags = user_tag\n .split(',')\n .map(|s| s.trim().to_string())\n .collect::>();", + "body_hash": "d63b0e86f51b7f26d7151cf498ae90700ce3ed7abf595293a0c4daa7715b3425" + }, + "app/src/ui_helper/stepper.rs|crate::impl Stepper<'a>::tag#free:d03b44fe1853": { + "kind": "Free", + "file": "app/src/ui_helper/stepper.rs", + "item_path": "crate::impl Stepper<'a>::tag#free:d03b44fe1853", + "comment_kind": "Line", + "comment_text": "// Tag1, tag2, tag3\n// in this case, only work with tag3, keep the rest as it is", + "comment_hash": "d03b44fe1853abc8035cd8b49b0be038640cd3f6502a9815776fd1cae7de0a3e", + "body_text": "let working_tag = current_tags.pop().unwrap();\nlet tag_exists = all_tags\n .iter()\n .position(|tag| tag.to_lowercase() == working_tag.to_lowercase());\nif tag_exists.is_none() {} else if let Some(index) = tag_exists {}\nOk(())\n}", + "body_hash": "63ae80d2f200c807191d25c06c85172ffdd81af6fa7e99f35d459f89481c26b0" + }, + "app/src/ui_helper/verifier.rs|crate::impl Verifier<'a>::amount": { + "kind": "Function", + "file": "app/src/ui_helper/verifier.rs", + "item_path": "crate::impl Verifier<'a>::amount", + "comment_kind": "OuterDoc", + "comment_text": " Validates and normalizes the user's amount input:\n\n\n\n - an empty input is left alone\n\n - every character except digits, `.` and the calculation symbols is dropped\n\n - a calculation such as `1+5*10` is evaluated, `*` and `/` first\n\n - the value is normalized to exactly 2 decimal places\n\n - a zero or negative value is turned positive and rejected\n", + "comment_hash": "593f5019e1861ead4c2692bd3d2136861b7648b9cda3a34d94ef59e926dbd0f2", + "body_text": "pub fn amount(&self, user_amount: &mut String) -> Result {\n \n if user_amount.is_empty() {\n return Ok(Output::Nothing(Field::Amount));\n }\n\n let calc_symbols = vec!['*', '/', '+', '-'];\n\n *user_amount = user_amount\n .chars()\n .filter(|c| c.is_numeric() || *c == '.' || calc_symbols.contains(c))\n .collect();\n\n \n \n if user_amount.is_empty() {\n return Err(VerifierError::ParsingError(Field::Amount));\n }\n\n \n if calc_symbols.iter().any(|s| user_amount.contains(*s)) {\n \n \n \n \n\n \n let count = user_amount\n .chars()\n .filter(|c| calc_symbols.contains(c))\n .count();\n\n \n let mut working_value = user_amount.to_owned();\n\n for _i in 0..count {\n for symbol in &calc_symbols {\n if let Some(location) = working_value.find(*symbol) {\n \n \n let mut first_value = String::new();\n let mut last_value = String::new();\n\n \n \n for char in working_value.chars().skip(location + 1) {\n if calc_symbols.contains(&char) {\n break;\n }\n last_value.push(char);\n }\n\n \n for char in working_value\n .chars()\n .rev()\n .skip(working_value.len() - location)\n {\n if calc_symbols.contains(&char) {\n break;\n }\n first_value.push(char);\n }\n \n first_value = first_value.chars().rev().collect();\n\n \n \n let final_value = if first_value.is_empty() || last_value.is_empty() {\n if first_value.is_empty() {\n last_value.clone()\n } else {\n first_value.clone()\n }\n } else {\n \n let first_num: f64 = match first_value.parse() {\n Ok(v) => v,\n Err(_) => {\n return Err(VerifierError::ParsingError(Field::Amount));\n }\n };\n\n let last_num: f64 = match last_value.parse() {\n Ok(v) => v,\n Err(_) => {\n return Err(VerifierError::ParsingError(Field::Amount));\n }\n };\n\n match *symbol {\n '*' => format!(\"{:.2}\", (first_num * last_num)),\n '/' => format!(\"{:.2}\", (first_num / last_num)),\n '+' => format!(\"{:.2}\", (first_num + last_num)),\n '-' => format!(\"{:.2}\", (first_num - last_num)),\n _ => String::new(),\n }\n };\n\n \n \n \n working_value = working_value\n .replace(&format!(\"{first_value}{symbol}{last_value}\"), &final_value);\n\n break;\n }\n }\n }\n *user_amount = working_value;\n }\n\n \n \n if user_amount.contains('.') {\n let state = user_amount.split('.').collect::>();\n if state[1].is_empty() {\n *user_amount += \"00\";\n }\n } else {\n *user_amount = format!(\"{user_amount}.00\");\n }\n\n let float_amount: f64 = user_amount\n .parse()\n .map_err(|_| VerifierError::ParsingError(Field::Amount))?;\n\n if float_amount <= 0.0 {\n *user_amount = format!(\"{:.2}\", (float_amount - (float_amount * 2.0)));\n return Err(VerifierError::AmountBelowZero);\n }\n\n \n if user_amount.contains('.') {\n let split_amount = user_amount.split('.').collect::>();\n\n match split_amount[1].len().cmp(&2) {\n Ordering::Less => *user_amount = format!(\"{user_amount}0\"),\n Ordering::Greater => {\n *user_amount = format!(\"{}.{}\", split_amount[0], &split_amount[1][..2]);\n }\n Ordering::Equal => (),\n }\n }\n\n \n let split_amount = user_amount.split('.').collect::>();\n\n \n if split_amount[0].len() > 10 {\n *user_amount = format!(\"{}.{}\", &split_amount[0][..10], split_amount[1]);\n }\n\n Ok(Output::Accepted(Field::Amount))\n }", + "body_hash": "5a44916ff1ce25ddc3f64d0866741fdf013aebc465444f82bc0432f1703dc277" + }, + "app/src/ui_helper/verifier.rs|crate::impl Verifier<'a>::amount#free:3871207fd966": { + "kind": "Free", + "file": "app/src/ui_helper/verifier.rs", + "item_path": "crate::impl Verifier<'a>::amount#free:3871207fd966", + "comment_kind": "Line", + "comment_text": "// `*` and `/` come first in `calc_symbols` so they are resolved before\n// `+` and `-`. One substitution is made per symbol found, so an\n// expression like `1+5*10` collapses `5*10` into 50 first and then\n// works on the remaining `1+50`.\n// One substitution per symbol found, so the loop below runs that many times", + "comment_hash": "3871207fd966fa243ccc66a3ce0117b88e2e3e05872ff6b28e9f0ad7e401435b", + "body_text": "let count = user_amount\n .chars()\n .filter(|c| calc_symbols.contains(c))\n .count();", + "body_hash": "cd0375dd1df28217990bd5ffadcf520fb74dcaa61c591ad99216dabfbc7dd418" + }, + "app/src/ui_helper/verifier.rs|crate::impl Verifier<'a>::amount#free:39be45ba7344": { + "kind": "Free", + "file": "app/src/ui_helper/verifier.rs", + "item_path": "crate::impl Verifier<'a>::amount#free:39be45ba7344", + "comment_kind": "Line", + "comment_text": "// Check if any of the symbols are present", + "comment_hash": "39be45ba7344c28620d8aba5cb3a6762509b002fb77b68b649ae87149487bd17", + "body_text": "if calc_symbols.iter().any(|s| user_amount.contains(*s)) {}", + "body_hash": "ebbc3ce5a6ca6ff3c52a857fd39e29f42ba065a3ede749a99317f047251b327f" + }, + "app/src/ui_helper/verifier.rs|crate::impl Verifier<'a>::amount#free:3c649d78cec8": { + "kind": "Free", + "file": "app/src/ui_helper/verifier.rs", + "item_path": "crate::impl Verifier<'a>::amount#free:3c649d78cec8", + "comment_kind": "Line", + "comment_text": "// One side is missing, as in `-5`, so there is nothing to\n// calculate and the other side is kept as is", + "comment_hash": "3c649d78cec8c6a1bd319dd956f4b54c5fea591aee6960ec88c9205e9f15d8b4", + "body_text": "let final_value = if first_value.is_empty() || last_value.is_empty() {} else {};", + "body_hash": "c83f1cfd570a44976682f1a4795e57c156f5220ade45cb3949cac0a83e8bc591" + }, + "app/src/ui_helper/verifier.rs|crate::impl Verifier<'a>::amount#free:5284ffcb2156": { + "kind": "Free", + "file": "app/src/ui_helper/verifier.rs", + "item_path": "crate::impl Verifier<'a>::amount#free:5284ffcb2156", + "comment_kind": "Line", + "comment_text": "// Make sure a `.` exists and that something follows it, otherwise the\n// value cannot be parsed as a float", + "comment_hash": "5284ffcb2156b48c5a9c21fef88dc28a84a0fa53481db2c3430e1dac90e7c74c", + "body_text": "if user_amount.contains('.') {} else {}\nlet float_amount: f64 = user_amount\n .parse()\n .map_err(|_| VerifierError::ParsingError(Field::Amount))?;\nif float_amount <= 0.0 {}", + "body_hash": "d7386960bbe3c23ce27c909d4ece26c094af9d184641983253c6bf78e6393d2d" + }, + "app/src/ui_helper/verifier.rs|crate::impl Verifier<'a>::amount#free:54a606a29ecf": { + "kind": "Free", + "file": "app/src/ui_helper/verifier.rs", + "item_path": "crate::impl Verifier<'a>::amount#free:54a606a29ecf", + "comment_kind": "Line", + "comment_text": "// Read right of the symbol, up to the end of the string or\n// the next calculation symbol", + "comment_hash": "54a606a29ecf60a886fe7be5db21357c7091d1c6559925f1572e29bc3ab36c79", + "body_text": "for char in working_value.chars().skip(location + 1) {}", + "body_hash": "3f4ae4f07915a61ee5099c2affd43681430e1271c0f6b2c170fb9ec5870d85a5" + }, + "app/src/ui_helper/verifier.rs|crate::impl Verifier<'a>::amount#free:674032ac9dd4": { + "kind": "Free", + "file": "app/src/ui_helper/verifier.rs", + "item_path": "crate::impl Verifier<'a>::amount#free:674032ac9dd4", + "comment_kind": "Line", + "comment_text": "// Pad or cut the decimals down to exactly 2 digits", + "comment_hash": "674032ac9dd4a16505d361cd09829bd82bf433c24a302bb6b1abf7a182c57f5d", + "body_text": "if user_amount.contains('.') {}", + "body_hash": "038a24eeafe873014a597f244ad688aba618c2a19f761c1c8133a42a33802728" + }, + "app/src/ui_helper/verifier.rs|crate::impl Verifier<'a>::amount#free:77f5346b5d99": { + "kind": "Free", + "file": "app/src/ui_helper/verifier.rs", + "item_path": "crate::impl Verifier<'a>::amount#free:77f5346b5d99", + "comment_kind": "Line", + "comment_text": "// The integer part is capped at 10 digits", + "comment_hash": "77f5346b5d99c7e6edfdfc6d97010523e9cc25542270e731463a6dc37065eb2e", + "body_text": "if split_amount[0].len() > 10 {}\nOk(Output::Accepted(Field::Amount))\n}", + "body_hash": "5e80df607743c9c87bb5f1153ba03392540522097b9a8c402d0e176d6a0f8f82" + }, + "app/src/ui_helper/verifier.rs|crate::impl Verifier<'a>::amount#free:8a980c107bd7": { + "kind": "Free", + "file": "app/src/ui_helper/verifier.rs", + "item_path": "crate::impl Verifier<'a>::amount#free:8a980c107bd7", + "comment_kind": "Line", + "comment_text": "// Grab the numbers on both sides of the symbol, e.g. `1+5`\n// gives first_value = 1 and last_value = 5", + "comment_hash": "8a980c107bd7dd00451ea7a808e5278817bdcf0a659c36321f780353b3c98d78", + "body_text": "let mut first_value = String::new();\nlet mut last_value = String::new();", + "body_hash": "0b36bf5a728b6f2f1188b6063ac53e1b65cf8e808dc6f3f4e713557b581105c5" + }, + "app/src/ui_helper/verifier.rs|crate::impl Verifier<'a>::amount#free:910ece1e6743": { + "kind": "Free", + "file": "app/src/ui_helper/verifier.rs", + "item_path": "crate::impl Verifier<'a>::amount#free:910ece1e6743", + "comment_kind": "Line", + "comment_text": "// e.g. `1+5*10` gives first_value = 5, last_value = 10 and\n// symbol = `*`, so `5*10` becomes 50 and the next loop works\n// on `1+50`", + "comment_hash": "910ece1e6743311556a90deac0a17f74119f8d9d9f50d2099a711dd532121b65", + "body_text": "working_value = working_value\n .replace(&format!(\"{first_value}{symbol}{last_value}\"), &final_value);\nbreak;\n}", + "body_hash": "30666071b3fcfbbcb2d043c9685a0edc0805a4e52420a4a6f8d433e0e0b8fcd7" + }, + "app/src/ui_helper/verifier.rs|crate::impl Verifier<'a>::amount#free:a14545bd23ed": { + "kind": "Free", + "file": "app/src/ui_helper/verifier.rs", + "item_path": "crate::impl Verifier<'a>::amount#free:a14545bd23ed", + "comment_kind": "Line", + "comment_text": "// Same as above, but walking the string backwards to get the left operand", + "comment_hash": "a14545bd23ed374811fdcc1e27aaaedb8b9b20fbde5aab33f0c71db3f40ef4ef", + "body_text": "for char in working_value\n .chars()\n .rev()\n .skip(working_value.len() - location)\n {}", + "body_hash": "5ae1ded40740f80f570b3b7982e65ebb6439e57f67e41806b6e2832431b2a903" + }, + "app/src/ui_helper/verifier.rs|crate::impl Verifier<'a>::amount#free:ae34928ba412": { + "kind": "Free", + "file": "app/src/ui_helper/verifier.rs", + "item_path": "crate::impl Verifier<'a>::amount#free:ae34928ba412", + "comment_kind": "Line", + "comment_text": "// Both operands are there, so this symbol's result is ready for replacement", + "comment_hash": "ae34928ba412dc34e79418d4bb10349db2ad589db46ef83c53e06397e8f187d5", + "body_text": "let first_num: f64 = match first_value.parse() {\n Ok(v) => v,\n Err(_) => {}\n };\nlet last_num: f64 = match last_value.parse() {\n Ok(v) => v,\n Err(_) => {}\n };\nmatch *symbol {\n '*' => format!(\"{:.2}\", (first_num * last_num)),\n '/' => format!(\"{:.2}\", (first_num / last_num)),\n '+' => format!(\"{:.2}\", (first_num + last_num)),\n '-' => format!(\"{:.2}\", (first_num - last_num)),\n _ => String::new(),\n }\n}", + "body_hash": "5c0a1a8f4985cb4c5be6dd83d3de665e1ccc9f431a744765bfbc7c56501504df" + }, + "app/src/ui_helper/verifier.rs|crate::impl Verifier<'a>::amount#free:b85ee3f104a0": { + "kind": "Free", + "file": "app/src/ui_helper/verifier.rs", + "item_path": "crate::impl Verifier<'a>::amount#free:b85ee3f104a0", + "comment_kind": "Line", + "comment_text": "// The field was non empty before filtering, so it can only be empty now\n// if nothing usable was left in it", + "comment_hash": "b85ee3f104a0b6360fa056023402be08ff6192ea6458035cce34dd291615b4a4", + "body_text": "if user_amount.is_empty() {}", + "body_hash": "8091c684478ce78364bfdc14f9dcc78a38a75d587b9ab094ad703f220796401e" + }, + "app/src/ui_helper/verifier.rs|crate::impl Verifier<'a>::amount#free:cbb97cf2b9d1": { + "kind": "Free", + "file": "app/src/ui_helper/verifier.rs", + "item_path": "crate::impl Verifier<'a>::amount#free:cbb97cf2b9d1", + "comment_kind": "Line", + "comment_text": "// Spaces are already gone thanks to the filter above, so indexing is safe", + "comment_hash": "cbb97cf2b9d1c9bf58ec8f9eba99e2c80db19b7480feccc9d2f6b0eb62b3f4b0", + "body_text": "let mut working_value = user_amount.to_owned();\nfor _i in 0..count {}\n*user_amount = working_value;\n}", + "body_hash": "ea2e1b77c9d85faf3b391da00c33f05b2e28efa95a0e5464cc612e77b2cba14d" + }, + "app/src/ui_helper/verifier.rs|crate::impl Verifier<'a>::amount#free:d7b152dac013": { + "kind": "Free", + "file": "app/src/ui_helper/verifier.rs", + "item_path": "crate::impl Verifier<'a>::amount#free:d7b152dac013", + "comment_kind": "Line", + "comment_text": "// Safe to split now, the amount always has a `.` followed by 2 digits", + "comment_hash": "d7b152dac0131c04cc87d1f3dd826cd32fa001c2409e18bd0a694ea041455c51", + "body_text": "let split_amount = user_amount.split('.').collect::>();", + "body_hash": "13e1a415bb3b6f91f045cb11001dd8e8b702295d9fefb7cf932e28e70c874419" + }, + "app/src/ui_helper/verifier.rs|crate::impl Verifier<'a>::amount#free:efbab3c38b93": { + "kind": "Free", + "file": "app/src/ui_helper/verifier.rs", + "item_path": "crate::impl Verifier<'a>::amount#free:efbab3c38b93", + "comment_kind": "Line", + "comment_text": "// Un-reverse the string", + "comment_hash": "efbab3c38b936cd271f5fe284842e025d3063442add7aca7a2190a1b3103fe07", + "body_text": "first_value = first_value.chars().rev().collect();", + "body_hash": "93bcec44d21260c0be73d1a88db69f86cef12a47a728fb1032ce83b3d7ef1be2" + }, + "app/src/ui_helper/verifier.rs|crate::impl Verifier<'a>::amount#free:fc3c97a0c260": { + "kind": "Free", + "file": "app/src/ui_helper/verifier.rs", + "item_path": "crate::impl Verifier<'a>::amount#free:fc3c97a0c260", + "comment_kind": "Line", + "comment_text": "// Nothing to verify while the field is empty", + "comment_hash": "fc3c97a0c26081dc098378c98c9de430d386803cdc3ed11cf9385df79900c0a0", + "body_text": "if user_amount.is_empty() {}\nlet calc_symbols = vec!['*', '/', '+', '-'];\n*user_amount = user_amount\n .chars()\n .filter(|c| c.is_numeric() || *c == '.' || calc_symbols.contains(c))\n .collect();", + "body_hash": "ac5dad507b368402958fa50020deb3fb7eba83b1d001db70d1ce02e539ae3d84" + }, + "app/src/ui_helper/verifier.rs|crate::impl Verifier<'a>::date": { + "kind": "Function", + "file": "app/src/ui_helper/verifier.rs", + "item_path": "crate::impl Verifier<'a>::date", + "comment_kind": "OuterDoc", + "comment_text": " Validates and normalizes the user's date input for the given `DateType`:\n\n\n\n - an empty input is left alone\n\n - every character except digits and `-` is dropped, so extra spaces and\n\n other symbols disappear\n\n - the year must be 4 characters long, the month and the day 2\n\n - the month must be within 01-12 and the day within 01-31\n\n - for an exact date, the parts must form a date that actually exists\n\n\n\n A rejected input is repaired in place before the error is returned: a\n\n short year becomes 2022, a long one is cut down to 4 characters, single\n\n digit months and days get a leading 0 and out of range months and days\n\n are clamped to 12 and 31.\n", + "comment_hash": "92850738be6730ca1ba7f7177ad57562715fdb8e914c305016210837b33662bb", + "body_text": "pub fn date(\n self,\n user_date: &mut String,\n date_type: DateType,\n ) -> Result {\n if user_date.is_empty() {\n return Ok(Output::Nothing(Field::Date));\n }\n\n *user_date = user_date\n .chars()\n .filter(|c| c.is_numeric() || *c == '-')\n .collect();\n\n let split_date = user_date\n .split('-')\n .map(ToString::to_string)\n .collect::>();\n\n \n \n match date_type {\n DateType::Exact => {\n if split_date.len() != 3 {\n *user_date = \"2022-01-01\".to_string();\n return Err(VerifierError::InvalidDate);\n }\n }\n DateType::Monthly => {\n if split_date.len() != 2 {\n *user_date = \"2022-01\".to_string();\n return Err(VerifierError::InvalidDate);\n }\n }\n DateType::Yearly => {\n if split_date.len() != 1 {\n *user_date = \"2022\".to_string();\n return Err(VerifierError::InvalidDate);\n }\n }\n }\n\n let (int_month, int_day): (Option, Option) = match date_type {\n DateType::Exact => {\n let month = split_date[1]\n .parse()\n .map_err(|_| VerifierError::ParsingError(Field::Date))?;\n\n let day = split_date[2]\n .parse()\n .map_err(|_| VerifierError::ParsingError(Field::Date))?;\n\n (Some(month), Some(day))\n }\n DateType::Monthly => {\n let month = split_date[1]\n .parse()\n .map_err(|_| VerifierError::ParsingError(Field::Date))?;\n\n (Some(month), None)\n }\n DateType::Yearly => (None, None),\n };\n\n \n \n if split_date[0].len() != 4 {\n match split_date[0].len().cmp(&4) {\n Ordering::Less => match date_type {\n DateType::Exact => {\n *user_date = format!(\"2022-{}-{}\", split_date[1], split_date[2]);\n }\n DateType::Monthly => *user_date = format!(\"2022-{}\", split_date[1]),\n DateType::Yearly => *user_date = \"2022\".to_string(),\n },\n Ordering::Greater => match date_type {\n DateType::Exact => {\n *user_date = format!(\n \"{}-{}-{}\",\n &split_date[0][..4],\n split_date[1],\n split_date[2]\n );\n }\n DateType::Monthly => {\n *user_date = format!(\"{}-{}\", &split_date[0][..4], split_date[1]);\n }\n DateType::Yearly => *user_date = split_date[0][..4].to_string(),\n },\n Ordering::Equal => {}\n }\n return Err(VerifierError::InvalidYear);\n }\n\n \n \n match date_type {\n DateType::Exact => {\n if split_date[1].len() != 2 {\n let unwrapped_month = int_month.unwrap();\n if unwrapped_month < 10 {\n *user_date =\n format!(\"{}-0{unwrapped_month}-{}\", split_date[0], split_date[2]);\n } else if unwrapped_month > 12 {\n *user_date = format!(\"{}-12-{}\", split_date[0], split_date[2]);\n }\n\n return Err(VerifierError::InvalidMonth);\n }\n }\n DateType::Monthly => {\n let unwrapped_month = int_month.unwrap();\n if split_date[1].len() != 2 {\n if unwrapped_month < 10 {\n *user_date = format!(\"{}-0{unwrapped_month}\", split_date[0]);\n } else if unwrapped_month > 12 {\n *user_date = format!(\"{}-12\", split_date[0]);\n }\n\n return Err(VerifierError::InvalidMonth);\n }\n }\n DateType::Yearly => {}\n }\n\n \n \n if let DateType::Exact = date_type {\n let unwrapped_day = int_day.unwrap();\n if split_date[2].len() != 2 {\n if unwrapped_day < 10 {\n *user_date = format!(\"{}-{}-0{unwrapped_day}\", split_date[0], split_date[1]);\n } else if unwrapped_day > 31 {\n *user_date = format!(\"{}-{}-31\", split_date[0], split_date[1]);\n }\n\n return Err(VerifierError::InvalidDay);\n }\n }\n\n \n match date_type {\n DateType::Exact => {\n let unwrapped_month = int_month.unwrap();\n if !(1..=12).contains(&unwrapped_month) {\n if unwrapped_month < 1 {\n *user_date = format!(\"{}-01-{}\", split_date[0], split_date[2]);\n } else if unwrapped_month > 12 {\n *user_date = format!(\"{}-12-{}\", split_date[0], split_date[2]);\n }\n\n return Err(VerifierError::MonthTooBig);\n }\n }\n DateType::Monthly => {\n let unwrapped_month = int_month.unwrap();\n if !(1..=12).contains(&unwrapped_month) {\n if unwrapped_month < 1 {\n *user_date = format!(\"{}-01\", split_date[0]);\n } else if unwrapped_month > 12 {\n *user_date = format!(\"{}-12\", split_date[0]);\n }\n\n return Err(VerifierError::MonthTooBig);\n }\n }\n DateType::Yearly => {}\n }\n\n \n if let DateType::Exact = date_type {\n let unwrapped_day = int_day.unwrap();\n if !(1..=31).contains(&unwrapped_day) {\n if unwrapped_day < 1 {\n *user_date = format!(\"{}-{}-01\", split_date[0], split_date[1]);\n } else if unwrapped_day > 31 {\n *user_date = format!(\"{}-{}-31\", split_date[0], split_date[1]);\n }\n\n return Err(VerifierError::DayTooBig);\n }\n }\n\n \n \n if let DateType::Exact = date_type {\n NaiveDate::parse_from_str(user_date, \"%Y-%m-%d\")\n .map_err(|_| VerifierError::NonExistingDate)?;\n }\n\n Ok(Output::Accepted(Field::Date))\n }", + "body_hash": "6d9c1a356829728a529ad8f94133a460d78f138d481b6c748af8c1a8de41fbcd" + }, + "app/src/ui_helper/verifier.rs|crate::impl Verifier<'a>::date#free:0fbdb455187d": { + "kind": "Free", + "file": "app/src/ui_helper/verifier.rs", + "item_path": "crate::impl Verifier<'a>::date#free:0fbdb455187d", + "comment_kind": "Line", + "comment_text": "// Months have fewer than 31 days at times, so the day still has to be\n// validated against the calendar", + "comment_hash": "0fbdb455187d8f6c13a16faf9f669468bf4595182a3be0f6574516a5f63253ba", + "body_text": "if let DateType::Exact = date_type {}\nOk(Output::Accepted(Field::Date))\n}", + "body_hash": "7dd06d2066d4c0415bb5f1c6cb7ff8c5018c10b2370d77eee0840929b81e3e04" + }, + "app/src/ui_helper/verifier.rs|crate::impl Verifier<'a>::date#free:3b0bac26da7d": { + "kind": "Free", + "file": "app/src/ui_helper/verifier.rs", + "item_path": "crate::impl Verifier<'a>::date#free:3b0bac26da7d", + "comment_kind": "Line", + "comment_text": "// The year has to be 4 characters long. A shorter one becomes 2022, a\n// longer one is cut down to its first 4 characters", + "comment_hash": "3b0bac26da7de255beb57cf8571572a5ee9466accafcdf2365cc7ac4a795d957", + "body_text": "if split_date[0].len() != 4 {}", + "body_hash": "1fc8b8094f477b3787b7994d538fd85cad24832ffcc0c040bcb44d6a1f949a58" + }, + "app/src/ui_helper/verifier.rs|crate::impl Verifier<'a>::date#free:6a6687824221": { + "kind": "Free", + "file": "app/src/ui_helper/verifier.rs", + "item_path": "crate::impl Verifier<'a>::date#free:6a6687824221", + "comment_kind": "Line", + "comment_text": "// The part count has to match the date type, so fall back to the\n// earliest date this type accepts", + "comment_hash": "6a66878242218ee20b357633c4f46e2e5b47d5219a570e8c6d1ee54283707549", + "body_text": "match date_type {\n DateType::Exact => {}\n DateType::Monthly => {}\n DateType::Yearly => {}\n }\nlet (int_month, int_day): (Option, Option) = match date_type {\n DateType::Exact => {}\n DateType::Monthly => {}\n DateType::Yearly => (None, None),\n };", + "body_hash": "7e52f7c27e8c6e401816f4d52048f66c1cc6d61c7fa31e011943f123ad99c0e8" + }, + "app/src/ui_helper/verifier.rs|crate::impl Verifier<'a>::date#free:6ae011307a00": { + "kind": "Free", + "file": "app/src/ui_helper/verifier.rs", + "item_path": "crate::impl Verifier<'a>::date#free:6ae011307a00", + "comment_kind": "Line", + "comment_text": "// Clamp the month into 01-12", + "comment_hash": "6ae011307a002ffd63e64c216e9b36d1185fbfea1a528f50b062dbb3108ba736", + "body_text": "match date_type {\n DateType::Exact => {}\n DateType::Monthly => {}\n DateType::Yearly => {}\n }", + "body_hash": "b97b43407522f41cc39fc542f4e2ea21bce18895d9f823e40c9fcfe43bbbcce6" + }, + "app/src/ui_helper/verifier.rs|crate::impl Verifier<'a>::date#free:86625a042a5d": { + "kind": "Free", + "file": "app/src/ui_helper/verifier.rs", + "item_path": "crate::impl Verifier<'a>::date#free:86625a042a5d", + "comment_kind": "Line", + "comment_text": "// Clamp the day into 01-31", + "comment_hash": "86625a042a5d03daf0abbfc0635ef88448ab26c8c05f51f3487c013c9e9742fc", + "body_text": "if let DateType::Exact = date_type {}", + "body_hash": "68c9dc36bdc4503563b71d4793287b3e3de300055a4a794b7a0769fd8d0f088f" + }, + "app/src/ui_helper/verifier.rs|crate::impl Verifier<'a>::date#free:dff52c30d1e1": { + "kind": "Free", + "file": "app/src/ui_helper/verifier.rs", + "item_path": "crate::impl Verifier<'a>::date#free:dff52c30d1e1", + "comment_kind": "Line", + "comment_text": "// The month has to be 2 characters long. A single digit month gets a\n// leading 0, anything above 12 is clamped to 12", + "comment_hash": "dff52c30d1e1bf6c2a7bf8702956ed037ab13b72c8656962d7617b6b49942efe", + "body_text": "match date_type {\n DateType::Exact => {}\n DateType::Monthly => {}\n DateType::Yearly => {}\n }", + "body_hash": "b97b43407522f41cc39fc542f4e2ea21bce18895d9f823e40c9fcfe43bbbcce6" + }, + "app/src/ui_helper/verifier.rs|crate::impl Verifier<'a>::date#free:fc94c013f61d": { + "kind": "Free", + "file": "app/src/ui_helper/verifier.rs", + "item_path": "crate::impl Verifier<'a>::date#free:fc94c013f61d", + "comment_kind": "Line", + "comment_text": "// The day has to be 2 characters long. A single digit day gets a\n// leading 0, anything above 31 is clamped to 31", + "comment_hash": "fc94c013f61d9ecfe2c6280c321f05602e3315629c3ffeb580963840a491d08d", + "body_text": "if let DateType::Exact = date_type {}", + "body_hash": "68c9dc36bdc4503563b71d4793287b3e3de300055a4a794b7a0769fd8d0f088f" + }, + "app/src/ui_helper/verifier.rs|crate::impl Verifier<'a>::frequency": { + "kind": "Function", + "file": "app/src/ui_helper/verifier.rs", + "item_path": "crate::impl Verifier<'a>::frequency", + "comment_kind": "OuterDoc", + "comment_text": " Checks if the inputted recurrence frequency (Daily/Weekly/Monthly/Yearly) is valid,\n\n accepting the same short mnemonics (d/w/m/y)\n", + "comment_hash": "fc875888d1b81ef8321741e295384724a2fbe378cfbfbbbfe6b2eae85b4b3eed", + "body_text": "pub fn frequency(&self, user_freq: &mut String) -> Result {\n let trimmed_input = user_freq.trim();\n\n if user_freq.is_empty() {\n return Ok(Output::Nothing(Field::Frequency));\n }\n\n let frequencies = RecurrenceFrequency::iter()\n .map(|f| f.to_string())\n .collect::>();\n\n let return_best_match = || {\n let best_match = get_best_match(user_freq, &frequencies);\n\n if best_match == trimmed_input {\n String::new()\n } else {\n best_match\n }\n };\n\n let lowercase = user_freq.to_lowercase();\n\n if lowercase.len() <= 2 {\n if lowercase.starts_with('d') {\n *user_freq = RecurrenceFrequency::Daily.to_string();\n } else if lowercase.starts_with('w') {\n *user_freq = RecurrenceFrequency::Weekly.to_string();\n } else if lowercase.starts_with('m') {\n *user_freq = RecurrenceFrequency::Monthly.to_string();\n } else if lowercase.starts_with('y') {\n *user_freq = RecurrenceFrequency::Yearly.to_string();\n } else {\n *user_freq = return_best_match();\n return Err(VerifierError::InvalidFrequency);\n }\n } else {\n if frequencies.contains(user_freq) {\n return Ok(Output::Accepted(Field::Frequency));\n }\n *user_freq = return_best_match();\n return Err(VerifierError::InvalidFrequency);\n }\n\n Ok(Output::Accepted(Field::Frequency))\n }", + "body_hash": "3ff8d43b788377028d7f308ab72010df86f9e39f6ef9696fd08bbf7c0168bb5b" + }, + "app/src/ui_helper/verifier.rs|crate::impl Verifier<'a>::recur_interval": { + "kind": "Function", + "file": "app/src/ui_helper/verifier.rs", + "item_path": "crate::impl Verifier<'a>::recur_interval", + "comment_kind": "OuterDoc", + "comment_text": " Checks if the inputted \"every N units\" interval is a whole number of at least 1.\n", + "comment_hash": "579203d4968e53b33ab63a1e1b487260be9027f2ebef79aa7074d60e6ca76550", + "body_text": "pub fn recur_interval(&self, user_value: &mut String) -> Result {\n if user_value.is_empty() {\n return Ok(Output::Nothing(Field::RecurInterval));\n }\n\n *user_value = user_value.chars().filter(char::is_ascii_digit).collect();\n\n if user_value.is_empty() {\n return Err(VerifierError::ParsingError(Field::RecurInterval));\n }\n\n let parsed = user_value\n .parse::()\n .map_err(|_| VerifierError::ParsingError(Field::RecurInterval))?;\n\n if parsed < 1 {\n *user_value = \"1\".to_string();\n return Err(VerifierError::InvalidRecurInterval);\n }\n\n Ok(Output::Accepted(Field::RecurInterval))\n }", + "body_hash": "70f79d17c508848ca546d5f4be44e440f0d5a38730bf8ff8f95c02af0ce63a6d" + }, + "app/src/ui_helper/verifier.rs|crate::impl Verifier<'a>::recur_month": { + "kind": "Function", + "file": "app/src/ui_helper/verifier.rs", + "item_path": "crate::impl Verifier<'a>::recur_month", + "comment_kind": "OuterDoc", + "comment_text": " Checks the recurrence's month name, only meaningful for Yearly.\n", + "comment_hash": "5827f235bae3f0dbcd69a836c42c5aa2389d063a79f155a28f2ea434c9ec85d2", + "body_text": "pub fn recur_month(&self, user_value: &mut String) -> Result {\n if user_value.is_empty() {\n return Ok(Output::Nothing(Field::RecurMonth));\n }\n\n let month_names = MONTH_NAMES\n .iter()\n .map(std::string::ToString::to_string)\n .collect::>();\n\n if month_names.contains(user_value) {\n return Ok(Output::Accepted(Field::RecurMonth));\n }\n\n *user_value = get_best_match(user_value, &month_names);\n Err(VerifierError::InvalidRecurMonth)\n }", + "body_hash": "25c117d88f77dc78638d978abb5d3ff0de1823631270d0b0e965ba2a43df630e" + }, + "app/src/ui_helper/verifier.rs|crate::impl Verifier<'a>::tags": { + "kind": "Function", + "file": "app/src/ui_helper/verifier.rs", + "item_path": "crate::impl Verifier<'a>::tags", + "comment_kind": "OuterDoc", + "comment_text": " Splits the inputted tags on commas, trims each one and drops duplicates\n\n while keeping the original order\n", + "comment_hash": "fcc39fd3b776ab92a0827cdc2bca7330769d3d5862bd6cd3df11569c574322fb", + "body_text": "pub fn tags(&self, user_tag: &mut String) {\n let mut split_tags = user_tag.split(',').map(str::trim).collect::>();\n split_tags.retain(|s| !s.is_empty());\n\n let mut seen = HashSet::new();\n\n \n let mut unique = Vec::new();\n\n for item in split_tags {\n if seen.insert(item) {\n unique.push(item);\n }\n }\n\n *user_tag = unique.join(\", \");\n }", + "body_hash": "7c70e1ec09e19185f3632da6c33ae20198a7034cddd5bdb03dc1d969a84ced9f" + }, + "app/src/ui_helper/verifier.rs|crate::impl Verifier<'a>::tags#free:b848e14acdfe": { + "kind": "Free", + "file": "app/src/ui_helper/verifier.rs", + "item_path": "crate::impl Verifier<'a>::tags#free:b848e14acdfe", + "comment_kind": "Line", + "comment_text": "// Vec for keeping the original order", + "comment_hash": "b848e14acdfead143a944c1192fdb2de2245a5b00ef61e467b779873b6effdfd", + "body_text": "let mut unique = Vec::new();\nfor item in split_tags {}\n*user_tag = unique.join(\", \");\n}", + "body_hash": "0e00d8e769e1d7bbb032ca5ba2fcbdd7b44b634eac249897c483152f04894023" + }, + "app/src/ui_helper/verifier.rs|crate::impl Verifier<'a>::tags_forced": { + "kind": "Function", + "file": "app/src/ui_helper/verifier.rs", + "item_path": "crate::impl Verifier<'a>::tags_forced", + "comment_kind": "OuterDoc", + "comment_text": " Trims and de-duplicates the inputted tags like [`Self::tags`], then drops\n\n every tag that does not exist in the database. `NonExistingTag` is returned\n\n if anything was dropped.\n", + "comment_hash": "85e4841dec119d2eb2d4996eca7534ea812a24646e8995bbb029ea6028769a3a", + "body_text": "pub fn tags_forced(&self, user_tag: &mut String) -> Result {\n if user_tag.is_empty() {\n return Ok(Output::Nothing(Field::Tags));\n }\n\n let all_tags = self.conn.cache().get_tags_set();\n\n let mut split_tags = user_tag.split(',').map(str::trim).collect::>();\n split_tags.retain(|s| !s.is_empty());\n\n let mut seen = HashSet::new();\n let mut unique = Vec::new();\n\n for item in split_tags {\n if seen.insert(item) {\n unique.push(item);\n }\n }\n\n let old_tags_len = unique.len();\n\n unique.retain(|&tag| all_tags.contains(tag));\n\n let new_tags_len = unique.len();\n\n *user_tag = unique.join(\", \");\n\n if old_tags_len == new_tags_len {\n Ok(Output::Accepted(Field::Tags))\n } else {\n Err(VerifierError::NonExistingTag)\n }\n }", + "body_hash": "1e90079b84c5eaa8550d912867bcb104cb6113a9521de36b09b3fc4da4d95349" + }, + "app/src/ui_helper/verifier.rs|crate::impl Verifier<'a>::tx_method": { + "kind": "Function", + "file": "app/src/ui_helper/verifier.rs", + "item_path": "crate::impl Verifier<'a>::tx_method", + "comment_kind": "OuterDoc", + "comment_text": " Validates the user's transaction method input:\n\n\n\n - an empty input is left alone\n\n - surrounding spaces are trimmed\n\n - the name has to match an existing method, ignoring case\n\n\n\n Anything else is fuzzy corrected to the closest existing method name and\n\n rejected.\n", + "comment_hash": "7e52e6ab7abf2fece6f575fa2a7da3ada09a8f712a5b5fcf27be8db8be6067bf", + "body_text": "pub fn tx_method(&self, user_method: &mut String) -> Result {\n *user_method = user_method.trim().to_string();\n\n \n if user_method.is_empty() {\n return Ok(Output::Nothing(Field::TxMethod));\n }\n\n let all_tx_methods = self.conn.cache().get_methods();\n\n for method in &all_tx_methods {\n let method_name = &method.name;\n\n if method_name.to_lowercase() == user_method.to_lowercase() {\n *user_method = method_name.clone();\n return Ok(Output::Accepted(Field::Amount));\n }\n }\n\n let user_method_names = all_tx_methods\n .iter()\n .map(|m| m.name.clone())\n .collect::>();\n\n let best_match = get_best_match(user_method, &user_method_names);\n\n *user_method = best_match;\n\n Err(VerifierError::InvalidTxMethod)\n }", + "body_hash": "efbee2eb8d5ff4250069a684c6a533ee704a645a720c825be9e51e18e46c6336" + }, + "app/src/ui_helper/verifier.rs|crate::impl Verifier<'a>::tx_method#free:930a971d8b27": { + "kind": "Free", + "file": "app/src/ui_helper/verifier.rs", + "item_path": "crate::impl Verifier<'a>::tx_method#free:930a971d8b27", + "comment_kind": "Line", + "comment_text": "// Nothing to verify while the text is empty", + "comment_hash": "930a971d8b274b64dcda1824ec11518d2cd1432a07cf462e3ddddc927fb0f700", + "body_text": "if user_method.is_empty() {}\nlet all_tx_methods = self.conn.cache().get_methods();\nfor method in &all_tx_methods {}\nlet user_method_names = all_tx_methods\n .iter()\n .map(|m| m.name.clone())\n .collect::>();\nlet best_match = get_best_match(user_method, &user_method_names);\n*user_method = best_match;\nErr(VerifierError::InvalidTxMethod)\n}", + "body_hash": "f7c169ec0d343df0088db5f96c34119433a2c342dac4f32fed60aeeea3f907b5" + }, + "app/src/ui_helper/verifier.rs|crate::impl Verifier<'a>::tx_type": { + "kind": "Function", + "file": "app/src/ui_helper/verifier.rs", + "item_path": "crate::impl Verifier<'a>::tx_type", + "comment_kind": "OuterDoc", + "comment_text": " Validates the user's transaction type input:\n\n\n\n - a 1 or 2 character input is expanded by its first letters, so `e` becomes\n\n Expense, `i` Income, `t` Transfer, `b` Borrow, `l` Lend and the `br` / `lr`\n\n variants their repay types\n\n - anything longer has to be a full type name\n\n\n\n Unrecognised input is fuzzy corrected to the closest type and rejected.\n", + "comment_hash": "9a6a90c1c91acbcfefac1f9eb01fbcb65d8aaf48928108cb1880b8833fa84ca6", + "body_text": "pub fn tx_type(&self, user_type: &mut String) -> Result {\n let trimmed_input = user_type.trim();\n\n if user_type.is_empty() {\n return Ok(Output::Nothing(Field::TxType));\n }\n\n let tx_types = TxType::iter()\n .map(|s| s.to_string())\n .collect::>();\n\n let return_best_match = || {\n let best_match = get_best_match(user_type, &tx_types);\n\n if best_match == trimmed_input {\n String::new()\n } else {\n best_match\n }\n };\n\n let lowercase = user_type.to_lowercase();\n\n if lowercase.len() <= 2 {\n if lowercase.starts_with('e') {\n *user_type = TxType::Expense.to_string();\n } else if lowercase.starts_with('i') {\n *user_type = TxType::Income.to_string();\n } else if lowercase.starts_with('t') {\n *user_type = TxType::Transfer.to_string();\n } else if lowercase.starts_with(\"br\") {\n *user_type = TxType::BorrowRepay.to_string();\n } else if lowercase.starts_with(\"lr\") {\n *user_type = TxType::LendRepay.to_string();\n } else if lowercase.starts_with('b') {\n *user_type = TxType::Borrow.to_string();\n } else if lowercase.starts_with('l') {\n *user_type = TxType::Lend.to_string();\n } else {\n *user_type = return_best_match();\n return Err(VerifierError::InvalidTxType);\n }\n } else {\n if tx_types.contains(user_type) {\n return Ok(Output::Accepted(Field::TxType));\n }\n *user_type = return_best_match();\n return Err(VerifierError::InvalidTxType);\n }\n\n Ok(Output::Accepted(Field::TxType))\n }", + "body_hash": "6919980b8549e9b48f580e866f2cd3b8e1b37d93898d0d3e5719555e0a0ef4fa" + }, + "app/src/utils.rs|crate::get_percentages": { + "kind": "Function", + "file": "app/src/utils.rs", + "item_path": "crate::get_percentages", + "comment_kind": "OuterDoc", + "comment_text": " Returns what percentage each of the two values is of their combined total\n", + "comment_hash": "ff342f4b7e3de5b2e3175924322e72adc589eb23a08480e4369de7aeaea594c7", + "body_text": "pub fn get_percentages(value1: f64, value2: f64) -> (f64, f64) {\n if value1 == 0.0 && value2 == 0.0 {\n return (0.0, 0.0);\n }\n let total = value1 + value2;\n let percentage1 = (value1 / total) * 100.0;\n let percentage2 = (value2 / total) * 100.0;\n (percentage1, percentage2)\n}", + "body_hash": "6fd8c54081e521ac661115bfea67ef3e2009290f7002134ccaa7e5fd4cad3042" + }, + "app/src/views/summary_view.rs|crate::SummaryView": { + "kind": "Struct", + "file": "app/src/views/summary_view.rs", + "item_path": "crate::SummaryView", + "comment_kind": "OuterDoc", + "comment_text": " Contains `FullTx` to generate summary data. Will always contain the exact number of txs from\n\n the month and year (or all txs) the summary was generated with\n", + "comment_hash": "1848cadb28374b738f39f185f3f233bd2b3ded447193e06dfdbe90a6eed7699d", + "body_text": "pub struct SummaryView {\n txs: Vec,\n nature: FetchNature,\n}", + "body_hash": "939ef9e02744f323ee199b3acf1b5a23acb7fc1367ebd8ffb066fd4abfc4dbb2" + }, + "app/src/views/summary_view.rs|crate::impl SummaryView::generate_summary#free:ed3895a0acef": { + "kind": "Free", + "file": "app/src/views/summary_view.rs", + "item_path": "crate::impl SummaryView::generate_summary#free:ed3895a0acef", + "comment_kind": "Line", + "comment_text": "// For % calculations, it's safe to directly cast to f64 before calculations", + "comment_hash": "ed3895a0acefdbe4169f6ff2d688fa02c1ed32fc5650ee2de15112a13850dfbe", + "body_text": "let earning_percentage = if method_earning[&method.name].value() == 0 {} else {};\nlet expense_percentage = if method_expense[&method.name] == 0 {} else {};\nlet mut average_earning = if method_earning[&method.name] == 0 {} else {};\nlet mut average_expense = if method_expense[&method.name] == 0 {} else {};\nlet mut mom_yoy_earning = None;\nlet mut mom_yoy_expense = None;\nif let FetchNature::Monthly = self.nature {}\nif let Some(last_summary) = last_summary\n && !no_mom_yoy\n {}\nif !no_mom_yoy && mom_yoy_expense.is_none() && mom_yoy_earning.is_none() {}\nlet method_summary = SummaryMethods::new(\n method.name.clone(),\n method_earning[&method.name].dollar(),\n method_expense[&method.name].dollar(),\n earning_percentage,\n expense_percentage,\n average_earning,\n average_expense,\n mom_yoy_earning,\n mom_yoy_expense,\n );\nmethod_data.push(method_summary);\n}", + "body_hash": "44ba4a6249b46f1cf78b278db83e55cb9f6399a3d821cd99e2bc0b7982f8ee3b" + }, + "app/src/views/tx_view.rs|crate::TxView::balance": { + "kind": "Field", + "file": "app/src/views/tx_view.rs", + "item_path": "crate::TxView::balance", + "comment_kind": "OuterDoc", + "comment_text": " Tx Method ID -> Balance of that method right after this tx was applied\n", + "comment_hash": "2ade2b7efe9843b2c88b8c9f807329011af86fbcfd4f391345005df4eea9db4a", + "body_text": "balance: HashMap", + "body_hash": "e23dd86a15f5d3c77a67fc2085474b9f67ee3363964e7ec88d4f9dec3e3abb09" + }, + "app/src/views/tx_view.rs|crate::get_txs#free:c9a7fe9f522e": { + "kind": "Free", + "file": "app/src/views/tx_view.rs", + "item_path": "crate::get_txs#free:c9a7fe9f522e", + "comment_kind": "Line", + "comment_text": "// Only monthly views are allowed to rewrite the stored balances", + "comment_hash": "c9a7fe9f522ed54869b4958fb330614a8b55e6bb1b0e847b16cdd7ea3f74feca", + "body_text": "if nature != FetchNature::Monthly {}\nlet mut to_insert_balance = Vec::new();\nfor mut balance in current_balance {}\nfor to_insert in to_insert_balance {}\nOk(TxViewGroup(all_tx_views))\n}", + "body_hash": "5587a5040b1cf20c9e4a046fc33b6f768eda7820ded543ba629f9887135adce5" + }, + "app/src/views/tx_view.rs|crate::impl TxViewGroup::switch_tx_index#free:1c8dc1d90e0c": { + "kind": "Free", + "file": "app/src/views/tx_view.rs", + "item_path": "crate::impl TxViewGroup::switch_tx_index#free:1c8dc1d90e0c", + "comment_kind": "Line", + "comment_text": "// Txs of the same day can still have no display order (0). Give that day's\n// txs an order of 1..N first, otherwise the swap below would mix unset values in", + "comment_hash": "1c8dc1d90e0c4cc25aea156049e10e49e78795fd37f38a5a203a917eee02359c", + "body_text": "let has_unset = self\n .0\n .iter()\n .any(|tv| tv.tx.date.date() == target_date && tv.tx.display_order == 0);\nif has_unset {}\nlet tx_1_order = self.0[index_1].tx.display_order;\nlet tx_2_order = self.0[index_2].tx.display_order;\nself.0[index_1].tx.display_order = tx_2_order;\nself.0[index_1].tx.set_display_order(db_conn)?;\nself.0[index_2].tx.display_order = tx_1_order;\nself.0[index_2].tx.set_display_order(db_conn)?;\nOk(true)\n}", + "body_hash": "36ed9a450996e5cea40361df8f050dd4a4e2c957a6b904022a46ebba6c9ae5a8" + }, + "app/tests/activity_view.rs|crate::activity_view_after_add_tx#free:09adfeddb40c": { + "kind": "Free", + "file": "app/tests/activity_view.rs", + "item_path": "crate::activity_view_after_add_tx#free:09adfeddb40c", + "comment_kind": "Line", + "comment_text": "// add_extra_field is false for AddTx", + "comment_hash": "09adfeddb40cb8d7816d8a6e2598f6a58f10e7ef7338eab41f8b0d600a2f8cb7", + "body_text": "assert!(!view.add_extra_field(0));\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "5559272e3ae951a9d5bb2ff06306a5bc8b64bfee2f81bb3de68c40dc47e4cdd4" + }, + "app/tests/activity_view.rs|crate::activity_view_after_add_tx#free:67dd667e7100": { + "kind": "Free", + "file": "app/tests/activity_view.rs", + "item_path": "crate::activity_view_after_add_tx#free:67dd667e7100", + "comment_kind": "Line", + "comment_text": "// Activity date is Local::now(), so query the current month", + "comment_hash": "67dd667e7100575170cc324afa0de5afcae796d7a9516457feb715d023d32ac4", + "body_text": "let now = Local::now().naive_local();\nlet month_names = [\n \"January\",\n \"February\",\n \"March\",\n \"April\",\n \"May\",\n \"June\",\n \"July\",\n \"August\",\n \"September\",\n \"October\",\n \"November\",\n \"December\",\n ];\nlet month = month_names[now.month0() as usize];\nlet year = now.year().to_string();\nlet view = db_conn.get_activity_view_with_str(month, &year).unwrap();\nassert!(!view.is_empty());\nassert!(view.total_activity() >= 1);", + "body_hash": "8ef4718021f98f4706608e968cb4e57637baa5d02a66834786ede867c8152335" + }, + "app/tests/activity_view.rs|crate::activity_view_after_add_tx#free:e73492996aef": { + "kind": "Free", + "file": "app/tests/activity_view.rs", + "item_path": "crate::activity_view_after_add_tx#free:e73492996aef", + "comment_kind": "Line", + "comment_text": "// get_activity_txs returns the transaction snapshots within the activity", + "comment_hash": "e73492996aef579b16c2c70031ae6025e3ef4e07f0c44b239566f8ded2047bda", + "body_text": "let txs = view.get_activity_txs(0);\nassert!(!txs.is_empty());", + "body_hash": "273758ecc8d86a03300981c6d05d30160a3008fcccff32df0651aa666e5b2b12" + }, + "app/tests/activity_view.rs|crate::activity_view_edit_has_extra_field#free:1aa0b2072489": { + "kind": "Free", + "file": "app/tests/activity_view.rs", + "item_path": "crate::activity_view_edit_has_extra_field#free:1aa0b2072489", + "comment_kind": "Line", + "comment_text": "// EditTx should have old and new tx displayed", + "comment_hash": "1aa0b207248900d93e2663c57e002a9966644efa67cab16f5805123d1e63dafb", + "body_text": "let table = view.get_activity_txs_table(Some(i));\nassert!(!table.is_empty());\nbreak;\n}", + "body_hash": "7738acca89363cd94deae119eecdf34917fcfa009bee7a3ba8104621e6da0d98" + }, + "app/tests/activity_view.rs|crate::activity_view_edit_has_extra_field#free:72b0528295fe": { + "kind": "Free", + "file": "app/tests/activity_view.rs", + "item_path": "crate::activity_view_edit_has_extra_field#free:72b0528295fe", + "comment_kind": "Line", + "comment_text": "// Find the EditTx activity", + "comment_hash": "72b0528295fec6649cd1aa646298a845c576955a01a8f24f698bec3f725a5175", + "body_text": "let mut found = false;\nfor i in 0..view.total_activity() {}\nassert!(found, \"Expected an EditTx activity with extra field\");\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "26404e3dca221f330617610fbaff34efe426c25cde1666eb857f373251c1f4ab" + }, + "app/tests/activity_view.rs|crate::activity_view_empty#free:bf26e6d2c84e": { + "kind": "Free", + "file": "app/tests/activity_view.rs", + "item_path": "crate::activity_view_empty#free:bf26e6d2c84e", + "comment_kind": "Line", + "comment_text": "// Query a month with no activity", + "comment_hash": "bf26e6d2c84e827e408cc0f181d3ab111a0610fd3108f230cf487dd8fb416910", + "body_text": "let view = db_conn\n .get_activity_view_with_str(\"January\", \"2020\")\n .unwrap();\nassert!(view.is_empty());\nassert_eq!(view.total_activity(), 0);\nassert!(view.get_activity_table().is_empty());\nassert_eq!(view.get_activity_txs_table(None).len(), 0);\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "5f3de1ed64250e0a86d88e2f95a6633c0c4cb005db059fcc405d3b0bb2b89222" + }, + "app/tests/activity_view.rs|crate::activity_view_get_table_returns_formatted#free:29b8df828d96": { + "kind": "Free", + "file": "app/tests/activity_view.rs", + "item_path": "crate::activity_view_get_table_returns_formatted#free:29b8df828d96", + "comment_kind": "Line", + "comment_text": "// Each activity row has date and activity type", + "comment_hash": "29b8df828d966510ccbe3123c1aa1904bcaf94212ef47423f199bf6a1e67473a", + "body_text": "assert!(\n table[0].len() >= 2,\n \"Activity table row should have date + type\"\n );\nlet txs_table = view.get_activity_txs_table(Some(0));\nassert!(!txs_table.is_empty());\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "951bab21285cc9f7227c660dd44b88256bba90ebbe5c1d040a45873f215d30c5" + }, + "app/tests/add_tx.rs|crate#free:79a409c50541": { + "kind": "Free", + "file": "app/tests/add_tx.rs", + "item_path": "crate#free:79a409c50541", + "comment_kind": "Line", + "comment_text": "// Local helper to reduce boilerplate", + "comment_hash": "79a409c50541c7c9eedcfbd6b79968384d9d6ac5a24929d0127e52ab3133976a", + "body_text": "fn add_tx_helper(\n db_conn: &mut DbConn,\n date: &str,\n details: &str,\n from_method: &str,\n to_method: &str,\n amount: &str,\n tx_type: &str,\n tags: &str,\n) {\n let new_tx = parse_tx_fields(\n date,\n details,\n from_method,\n to_method,\n amount,\n tx_type,\n db_conn,\n )\n .unwrap();\n db_conn.add_new_tx(new_tx, tags).unwrap();\n}", + "body_hash": "a7d99a995b4f72c7b67a5131cfcce6c995bd8ff81a7d127376398711030ea742" + }, + "app/tests/add_tx.rs|crate::add_tx_test#free:36d8d1bdf50d": { + "kind": "Free", + "file": "app/tests/add_tx.rs", + "item_path": "crate::add_tx_test#free:36d8d1bdf50d", + "comment_kind": "Line", + "comment_text": "// Verify individual tx fields after all inserts — October has 4 txs", + "comment_hash": "36d8d1bdf50ddc0a010180b4a3e9c5d11cdbae74a4f1bffaf01c3b4b27c3a4b5", + "body_text": "let oct = NaiveDate::from_ymd_opt(2022, 10, 1).unwrap();\nlet oct_txs = db_conn\n .fetch_txs_with_date(oct, FetchNature::Monthly)\n .unwrap();\nlet tx0 = oct_txs.get_tx(0);\nassert_eq!(tx0.details, Some(\"Inheritance\".into()));\nassert_eq!(tx0.amount.value(), 500000);\nassert_eq!(tx0.from_method.name, \"Cash\");\nassert_eq!(tx0.tags[0].name, \"Inheritance\");\nassert_eq!(tx0.to_array(false).len(), 6);\nlet tx1 = oct_txs.get_tx(1);\nassert_eq!(tx1.details, Some(\"Groceries\".into()));\nassert_eq!(tx1.tags[0].name, \"Groceries\");\nlet tx2 = oct_txs.get_tx(2);\nassert_eq!(tx2.details, Some(\"More Groceries\".into()));", + "body_hash": "b6cac09cda824f562ce5051120565fa50ebdca7bbdbfb4a0dcbe2215699009d2" + }, + "app/tests/add_tx.rs|crate::add_tx_test#free:4a5e89df5d68": { + "kind": "Free", + "file": "app/tests/add_tx.rs", + "item_path": "crate::add_tx_test#free:4a5e89df5d68", + "comment_kind": "Line", + "comment_text": "// Bank method should still be at zero — no txs touched it", + "comment_hash": "4a5e89df5d68478add1c1683d36d121e0102bb2d4a0589636a724dd0294ad487", + "body_text": "let jul_balance = jul_txs.get_tx_balance(0);\nassert_eq!(jul_balance[&bank_method].value(), 0);\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "13b17fb145a67830a659de53d849e4c212840b429cc652e50a4ea038e008b05e" + }, + "app/tests/add_tx.rs|crate::add_tx_test#free:d18816468485": { + "kind": "Free", + "file": "app/tests/add_tx.rs", + "item_path": "crate::add_tx_test#free:d18816468485", + "comment_kind": "Line", + "comment_text": "// July's single tx", + "comment_hash": "d1881646848590901ef26b55705fbe31a7be43fe3497eef13056f8e119cabdd5", + "body_text": "let jul = NaiveDate::from_ymd_opt(2022, 7, 1).unwrap();\nlet jul_txs = db_conn\n .fetch_txs_with_date(jul, FetchNature::Monthly)\n .unwrap();\nassert_eq!(jul_txs.get_tx(0).details, Some(\"Salary\".into()));\nassert_eq!(jul_txs.get_tx(0).tags[0].name, \"Salary\");", + "body_hash": "27941f6ebdb3deeabd8d925a15957fa47f54c1632c5a36585c6d42ad9eb7797a" + }, + "app/tests/add_tx.rs|crate::add_tx_test#free:ef71f9d910c1": { + "kind": "Free", + "file": "app/tests/add_tx.rs", + "item_path": "crate::add_tx_test#free:ef71f9d910c1", + "comment_kind": "Line", + "comment_text": "// Same tag \"Groceries\" reused", + "comment_hash": "ef71f9d910c1388e95fee4393796eb4c2ca0e89c107368d504d49d2069d0f1d0", + "body_text": "assert_eq!(tx2.tags[0].name, \"Groceries\");\nlet tx3 = oct_txs.get_tx(3);\nassert_eq!(tx3.details, Some(\"Rent\".into()));\nassert_eq!(tx3.tags[0].name, \"Rent\");", + "body_hash": "34513df1c448e2f25f8bb181ac2d5f41c359d04ee2c5f91777eba06edf984afd" + }, + "app/tests/add_tx.rs|crate::add_tx_to_array_search_format_differs#free:ac2ac9231ae4": { + "kind": "Free", + "file": "app/tests/add_tx.rs", + "item_path": "crate::add_tx_to_array_search_format_differs#free:ac2ac9231ae4", + "comment_kind": "Line", + "comment_text": "// Non-search format has weekday/time in date", + "comment_hash": "ac2ac9231ae4f43315f7ab9ede3f6135d582dd2e95aeb40e8ca93f45fd9edb6b", + "body_text": "let display_arr = tx.to_array(false);\nassert!(display_arr[0].contains(\"Wed\"));", + "body_hash": "31e341a785df450c7c771abc77696f1612869540d512798e4adee70fd12a71a9" + }, + "app/tests/add_tx.rs|crate::add_tx_to_array_search_format_differs#trailing:71e6581d8b2d": { + "kind": "Free", + "file": "app/tests/add_tx.rs", + "item_path": "crate::add_tx_to_array_search_format_differs#trailing:71e6581d8b2d", + "comment_kind": "Line", + "comment_text": "// 2024-05-15 is a Wednesday", + "comment_hash": "71e6581d8b2dbcbbacc30c450d6c15d7b3cacd358c1e83f3cc73ac57270e7b9e", + "body_text": "assert!(display_arr[0].contains(\"Wed\"));", + "body_hash": "e4a53c2f6d76ba2cf8c08ac8aebc9730bb8d71ba290982f75af8a5fd0e9b728f" + }, + "app/tests/add_tx.rs|crate::add_tx_transfer_sets_to_method#free:f442e92744a5": { + "kind": "Free", + "file": "app/tests/add_tx.rs", + "item_path": "crate::add_tx_transfer_sets_to_method#free:f442e92744a5", + "comment_kind": "Line", + "comment_text": "// to_array for non-search shows method as \"Cash → Bank\"", + "comment_hash": "f442e92744a5dfa3ae0ab39903ca73bad78a78d166d17b4caa0cd0a2e97e60ce", + "body_text": "let array = transfer.to_array(false);\nlet method_str = &array[2];\nassert!(method_str.contains(\"→\"));\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "a96d056c6beed314bee34ddc405b53a3af990bcf64bb7f1ffee8f774bb6af018" + }, + "app/tests/autofiller.rs|crate#free:009b02de2a93": { + "kind": "Free", + "file": "app/tests/autofiller.rs", + "item_path": "crate#free:009b02de2a93", + "comment_kind": "Line", + "comment_text": "// ---- Tx Method autofill ----", + "comment_hash": "009b02de2a930a57e0590353e2b81902a0586ef54e2cf470ea85e8ce79e91aad", + "body_text": "#[test]\nfn autofill_method_exact_returns_empty() {\n let file_name = \"test_autofill_method.sqlite\";\n let mut db_conn = create_test_db(file_name);\n \n let result = db_conn.autofill().tx_method(\"Cash\");\n assert_eq!(result, \"\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn autofill_method_fuzzy_suggests_match() {\n let file_name = \"test_autofill_method_fuzzy.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let result = db_conn.autofill().tx_method(\"Csh\");\n assert_eq!(result, \"Cash\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn autofill_method_empty_returns_empty() {\n let file_name = \"test_autofill_method_empty.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let result = db_conn.autofill().tx_method(\"\");\n assert_eq!(result, \"\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}", + "body_hash": "3de4d129fbf41c5a450cdd5061bcbe3d89e9f3b83bdf62c27471db2bd5405434" + }, + "app/tests/autofiller.rs|crate#free:28bf4ea649fc": { + "kind": "Free", + "file": "app/tests/autofiller.rs", + "item_path": "crate#free:28bf4ea649fc", + "comment_kind": "Line", + "comment_text": "// ---- Recurring: Frequency autofill ----", + "comment_hash": "28bf4ea649fc766f20427daba5211fd54e4d87460ae935149ff936bac2185e90", + "body_text": "#[test]\nfn autofill_frequency_empty_returns_empty() {\n let file_name = \"test_autofill_frequency_empty.sqlite\";\n let mut db_conn = create_test_db(file_name);\n assert_eq!(db_conn.autofill().frequency(\"\"), \"\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn autofill_frequency_single_letter_completes() {\n let file_name = \"test_autofill_frequency_letter.sqlite\";\n let mut db_conn = create_test_db(file_name);\n\n for (input, expected) in [\n (\"d\", \"Daily\"),\n (\"W\", \"Weekly\"),\n (\"m\", \"Monthly\"),\n (\"Y\", \"Yearly\"),\n ] {\n assert_eq!(db_conn.autofill().frequency(input), expected);\n }\n\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn autofill_frequency_exact_returns_empty() {\n let file_name = \"test_autofill_frequency_exact.sqlite\";\n let mut db_conn = create_test_db(file_name);\n assert_eq!(db_conn.autofill().frequency(\"Weekly\"), \"\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn autofill_frequency_typo_suggests_closest() {\n let file_name = \"test_autofill_frequency_typo.sqlite\";\n let mut db_conn = create_test_db(file_name);\n assert_eq!(db_conn.autofill().frequency(\"Mnthly\"), \"Monthly\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}", + "body_hash": "165680647cf2d1621a22491160803db56870a7fd62eae79c47e7b18a0ff91637" + }, + "app/tests/autofiller.rs|crate#free:2d1a3a77c6dc": { + "kind": "Free", + "file": "app/tests/autofiller.rs", + "item_path": "crate#free:2d1a3a77c6dc", + "comment_kind": "Line", + "comment_text": "// ---- Recurring: Day of week autofill ----", + "comment_hash": "2d1a3a77c6dcedd334198473af6b684e36831e8bbd41e3022938c14747c1d99a", + "body_text": "#[test]\nfn autofill_recur_value_weekly_unique_prefix_completes() {\n let file_name = \"test_autofill_recur_value_prefix.sqlite\";\n let mut db_conn = create_test_db(file_name);\n\n for (input, expected) in [\n (\"mon\", \"Monday\"),\n (\"Fri\", \"Friday\"),\n (\"w\", \"Wednesday\"),\n (\"sa\", \"Saturday\"),\n (\"sun\", \"Sunday\"),\n ] {\n assert_eq!(\n db_conn\n .autofill()\n .recur_value(input, RecurrenceFrequency::Weekly),\n expected\n );\n }\n\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn autofill_recur_value_weekly_ambiguous_prefix_still_suggests_a_day() {\n let file_name = \"test_autofill_recur_value_ambiguous.sqlite\";\n let mut db_conn = create_test_db(file_name);\n\n \n let suggestion = db_conn\n .autofill()\n .recur_value(\"t\", RecurrenceFrequency::Weekly);\n assert!(suggestion == \"Tuesday\" || suggestion == \"Thursday\");\n\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn autofill_recur_value_weekly_exact_returns_empty() {\n let file_name = \"test_autofill_recur_value_exact.sqlite\";\n let mut db_conn = create_test_db(file_name);\n assert_eq!(\n db_conn\n .autofill()\n .recur_value(\"Monday\", RecurrenceFrequency::Weekly),\n \"\"\n );\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn autofill_recur_value_non_weekly_never_suggests() {\n let file_name = \"test_autofill_recur_value_non_weekly.sqlite\";\n let mut db_conn = create_test_db(file_name);\n\n for frequency in [\n RecurrenceFrequency::Daily,\n RecurrenceFrequency::Monthly,\n RecurrenceFrequency::Yearly,\n ] {\n assert_eq!(db_conn.autofill().recur_value(\"mon\", frequency), \"\");\n assert_eq!(db_conn.autofill().recur_value(\"15\", frequency), \"\");\n }\n\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}", + "body_hash": "024699c185a1be2cefc6cfc90b7457aebe195599a414ed0a4278a232ebc63101" + }, + "app/tests/autofiller.rs|crate#free:b168afeb53c0": { + "kind": "Free", + "file": "app/tests/autofiller.rs", + "item_path": "crate#free:b168afeb53c0", + "comment_kind": "Line", + "comment_text": "// ---- Details autofill ----", + "comment_hash": "b168afeb53c0097f9024cf3f9c5c2bc21bbb3d342a85131af42b36cf02cde453", + "body_text": "#[test]\nfn autofill_details_fuzzy_suggests_match() {\n let file_name = \"test_autofill_details.sqlite\";\n let mut db_conn = create_test_db(file_name);\n add_tx(\n &mut db_conn,\n \"2024-05-01\",\n \"Amazon purchase\",\n \"Cash\",\n \"\",\n \"50.00\",\n \"Expense\",\n \"Shopping\",\n );\n\n let result = db_conn.autofill().details(\"Amaz\");\n assert_eq!(result, \"Amazon purchase\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn autofill_details_exact_returns_empty() {\n let file_name = \"test_autofill_details_exact.sqlite\";\n let mut db_conn = create_test_db(file_name);\n add_tx(\n &mut db_conn,\n \"2024-06-01\",\n \"Netflix\",\n \"Cash\",\n \"\",\n \"15.00\",\n \"Expense\",\n \"Subscriptions\",\n );\n\n let result = db_conn.autofill().details(\"Netflix\");\n assert_eq!(result, \"\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn autofill_details_empty_returns_empty() {\n let file_name = \"test_autofill_details_empty.sqlite\";\n let mut db_conn = create_test_db(file_name);\n add_tx(\n &mut db_conn,\n \"2024-07-01\",\n \"Some detail\",\n \"Cash\",\n \"\",\n \"10.00\",\n \"Expense\",\n \"Tag\",\n );\n\n let result = db_conn.autofill().details(\"\");\n assert_eq!(result, \"\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn autofill_details_cached_after_edit() {\n let file_name = \"test_autofill_details_edit.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let old_tx = add_tx(\n &mut db_conn,\n \"2024-08-01\",\n \"Old detail\",\n \"Cash\",\n \"\",\n \"10.00\",\n \"Expense\",\n \"Tag\",\n );\n\n \n let new_tx = rex_app::modifier::parse_tx_fields(\n \"2024-08-01\",\n \"Brand new detail\",\n \"Cash\",\n \"\",\n \"10.00\",\n \"Expense\",\n &db_conn,\n )\n .unwrap();\n db_conn.edit_tx(&old_tx, new_tx, \"Tag\").unwrap();\n\n let result = db_conn.autofill().details(\"Brand\");\n assert_eq!(result, \"Brand new detail\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn autofill_details_on_empty_db_returns_empty() {\n let file_name = \"test_autofill_details_empty_db.sqlite\";\n let mut db_conn = create_test_db(file_name);\n \n let result = db_conn.autofill().details(\"anything\");\n assert_eq!(result, \"\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}", + "body_hash": "9de71e7963f358fdbadfb1a5be8fd5d49cc5ac6377f07901b6aded6217323379" + }, + "app/tests/autofiller.rs|crate#free:bf6c47165ba5": { + "kind": "Free", + "file": "app/tests/autofiller.rs", + "item_path": "crate#free:bf6c47165ba5", + "comment_kind": "Line", + "comment_text": "// ---- Unique prefixes win over fuzzy matching on the older fields too ----", + "comment_hash": "bf6c47165ba58dfbc977a8ec1923728f6a523fcf80c3db83a67a9ad60d0ad24e", + "body_text": "#[test]\nfn autofill_method_unique_prefix_completes() {\n let file_name = \"test_autofill_method_prefix.sqlite\";\n let mut db_conn = create_test_db(file_name);\n assert_eq!(db_conn.autofill().tx_method(\"ba\"), \"Bank\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn autofill_details_unique_prefix_beats_a_closer_fuzzy_match() {\n let file_name = \"test_autofill_details_prefix.sqlite\";\n let mut db_conn = create_test_db(file_name);\n\n add_tx(\n &mut db_conn,\n \"2024-06-01\",\n \"Groceries\",\n \"Cash\",\n \"\",\n \"10.00\",\n \"Expense\",\n \"Food\",\n );\n add_tx(\n &mut db_conn,\n \"2024-06-02\",\n \"Gym membership\",\n \"Cash\",\n \"\",\n \"30.00\",\n \"Expense\",\n \"Health\",\n );\n\n \n assert_eq!(db_conn.autofill().details(\"gy\"), \"Gym membership\");\n assert_eq!(db_conn.autofill().details(\"gr\"), \"Groceries\");\n\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn autofill_tags_completes_only_the_last_tag() {\n let file_name = \"test_autofill_tags_prefix.sqlite\";\n let mut db_conn = create_test_db(file_name);\n\n add_tx(\n &mut db_conn,\n \"2024-06-01\",\n \"\",\n \"Cash\",\n \"\",\n \"10.00\",\n \"Expense\",\n \"Food, Gym\",\n );\n\n assert_eq!(db_conn.autofill().tags(\"Food, gy\"), \"Gym\");\n \n assert_eq!(db_conn.autofill().tags(\"Food, \"), \"\");\n\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn autofill_tx_type_long_input_uses_prefix() {\n let file_name = \"test_autofill_type_prefix.sqlite\";\n let mut db_conn = create_test_db(file_name);\n assert_eq!(db_conn.autofill().tx_type(\"Inc\"), \"Income\");\n assert_eq!(db_conn.autofill().tx_type(\"Borrow R\"), \"Borrow Repay\");\n \n assert_eq!(db_conn.autofill().tx_type(\"lr\"), \"Lend Repay\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}", + "body_hash": "aecacb5be8c349990b203314ababda506906e70c3dbcf8241a5a3aff2775d586" + }, + "app/tests/autofiller.rs|crate#free:c32c5f6313f6": { + "kind": "Free", + "file": "app/tests/autofiller.rs", + "item_path": "crate#free:c32c5f6313f6", + "comment_kind": "Line", + "comment_text": "// ---- Tags autofill ----", + "comment_hash": "c32c5f6313f6d1de59ae23136916b18a6ff18e6af0206df2c9dfbbece0646dc7", + "body_text": "#[test]\nfn autofill_tags_fuzzy_suggests_match() {\n let file_name = \"test_autofill_tags.sqlite\";\n let mut db_conn = create_test_db(file_name);\n add_tx(\n &mut db_conn,\n \"2024-01-01\",\n \"T1\",\n \"Cash\",\n \"\",\n \"10.00\",\n \"Expense\",\n \"Groceries\",\n );\n\n let result = db_conn.autofill().tags(\"Gro\");\n assert_eq!(result, \"Groceries\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn autofill_tags_exact_returns_empty() {\n let file_name = \"test_autofill_tags_exact.sqlite\";\n let mut db_conn = create_test_db(file_name);\n add_tx(\n &mut db_conn,\n \"2024-02-01\",\n \"T1\",\n \"Cash\",\n \"\",\n \"10.00\",\n \"Expense\",\n \"Rent\",\n );\n\n let result = db_conn.autofill().tags(\"Rent\");\n assert_eq!(result, \"\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn autofill_tags_empty_returns_empty() {\n let file_name = \"test_autofill_tags_empty.sqlite\";\n let mut db_conn = create_test_db(file_name);\n add_tx(\n &mut db_conn,\n \"2024-03-01\",\n \"T1\",\n \"Cash\",\n \"\",\n \"10.00\",\n \"Expense\",\n \"Tag\",\n );\n\n let result = db_conn.autofill().tags(\"\");\n assert_eq!(result, \"\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn autofill_tags_only_last_tag_suggested() {\n let file_name = \"test_autofill_tags_multi.sqlite\";\n let mut db_conn = create_test_db(file_name);\n add_tx(\n &mut db_conn,\n \"2024-04-01\",\n \"T1\",\n \"Cash\",\n \"\",\n \"10.00\",\n \"Expense\",\n \"Groceries, Salary\",\n );\n\n \n let result = db_conn.autofill().tags(\"Groceries, Sal\");\n assert_eq!(result, \"Salary\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}", + "body_hash": "f4cb5cf51efc9183f9cb1ee1c277a9e04109906ab469d522759815551ea2c66a" + }, + "app/tests/autofiller.rs|crate#free:f967ea2697b1": { + "kind": "Free", + "file": "app/tests/autofiller.rs", + "item_path": "crate#free:f967ea2697b1", + "comment_kind": "Line", + "comment_text": "// ---- Recurring: Month autofill ----", + "comment_hash": "f967ea2697b1e8b96742393ae654b513a32aaab7826fd33ddad5a071fce259aa", + "body_text": "#[test]\nfn autofill_recur_month_unique_prefix_completes() {\n let file_name = \"test_autofill_recur_month_prefix.sqlite\";\n let mut db_conn = create_test_db(file_name);\n\n for (input, expected) in [\n (\"jan\", \"January\"),\n (\"Feb\", \"February\"),\n (\"sep\", \"September\"),\n (\"dec\", \"December\"),\n (\"Mar\", \"March\"),\n ] {\n assert_eq!(db_conn.autofill().recur_month(input), expected);\n }\n\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn autofill_recur_month_ambiguous_prefix_still_suggests_a_month() {\n let file_name = \"test_autofill_recur_month_ambiguous.sqlite\";\n let mut db_conn = create_test_db(file_name);\n\n \n assert!([\"June\", \"July\"].contains(&db_conn.autofill().recur_month(\"ju\").as_str()));\n assert!([\"March\", \"May\"].contains(&db_conn.autofill().recur_month(\"ma\").as_str()));\n\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn autofill_recur_month_exact_and_empty_return_empty() {\n let file_name = \"test_autofill_recur_month_exact.sqlite\";\n let mut db_conn = create_test_db(file_name);\n assert_eq!(db_conn.autofill().recur_month(\"January\"), \"\");\n assert_eq!(db_conn.autofill().recur_month(\"\"), \"\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}", + "body_hash": "314e60f5340d83fd2b63ea04bae5c72d36eecb3ec1e0bf9a203441db54fb1a42" + }, + "app/tests/autofiller.rs|crate#free:fb7cdb47fa08": { + "kind": "Free", + "file": "app/tests/autofiller.rs", + "item_path": "crate#free:fb7cdb47fa08", + "comment_kind": "Line", + "comment_text": "// ---- Tx Type autofill ----", + "comment_hash": "fb7cdb47fa08b36424ae09ec55728bec59f1fd732844c7f9f23cd446f3004ad8", + "body_text": "#[test]\nfn autofill_tx_type_short_e() {\n let file_name = \"test_autofill_type_e.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let result = db_conn.autofill().tx_type(\"e\");\n assert_eq!(result, \"Expense\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn autofill_tx_type_short_i() {\n let file_name = \"test_autofill_type_i.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let result = db_conn.autofill().tx_type(\"i\");\n assert_eq!(result, \"Income\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn autofill_tx_type_short_t() {\n let file_name = \"test_autofill_type_t.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let result = db_conn.autofill().tx_type(\"t\");\n assert_eq!(result, \"Transfer\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn autofill_tx_type_short_b() {\n let file_name = \"test_autofill_type_b.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let result = db_conn.autofill().tx_type(\"b\");\n assert_eq!(result, \"Borrow\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn autofill_tx_type_short_l() {\n let file_name = \"test_autofill_type_l.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let result = db_conn.autofill().tx_type(\"l\");\n assert_eq!(result, \"Lend\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn autofill_tx_type_short_br() {\n let file_name = \"test_autofill_type_br.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let result = db_conn.autofill().tx_type(\"br\");\n assert_eq!(result, \"Borrow Repay\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn autofill_tx_type_short_lr() {\n let file_name = \"test_autofill_type_lr.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let result = db_conn.autofill().tx_type(\"lr\");\n assert_eq!(result, \"Lend Repay\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn autofill_tx_type_exact_returns_empty() {\n let file_name = \"test_autofill_type_exact.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let result = db_conn.autofill().tx_type(\"Expense\");\n assert_eq!(result, \"\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn autofill_tx_type_fuzzy_suggests_match() {\n let file_name = \"test_autofill_type_fuzzy.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let result = db_conn.autofill().tx_type(\"Epense\");\n assert_eq!(result, \"Expense\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn autofill_tx_type_empty_returns_empty() {\n let file_name = \"test_autofill_type_empty.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let result = db_conn.autofill().tx_type(\"\");\n assert_eq!(result, \"\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}", + "body_hash": "b91925a6a7cc05e5df9c7f5c8434fb46d58aab75ce18bb89e1084cd7b6de0126" + }, + "app/tests/autofiller.rs|crate::autofill_details_cached_after_edit#free:a8ef6f55f5d3": { + "kind": "Free", + "file": "app/tests/autofiller.rs", + "item_path": "crate::autofill_details_cached_after_edit#free:a8ef6f55f5d3", + "comment_kind": "Line", + "comment_text": "// Replace with a new detail", + "comment_hash": "a8ef6f55f5d35d8f7a117a72ce817034e254d8e254f95dbf9ff7f32d47c89404", + "body_text": "let new_tx = rex_app::modifier::parse_tx_fields(\n \"2024-08-01\",\n \"Brand new detail\",\n \"Cash\",\n \"\",\n \"10.00\",\n \"Expense\",\n &db_conn,\n )\n .unwrap();\ndb_conn.edit_tx(&old_tx, new_tx, \"Tag\").unwrap();\nlet result = db_conn.autofill().details(\"Brand\");\nassert_eq!(result, \"Brand new detail\");\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "60e5255d19ee0593d7812156825df8c30a5fe3514801fedbcb46ac6eda0c9244" + }, + "app/tests/autofiller.rs|crate::autofill_details_on_empty_db_returns_empty#free:5fb94f8cb745": { + "kind": "Free", + "file": "app/tests/autofiller.rs", + "item_path": "crate::autofill_details_on_empty_db_returns_empty#free:5fb94f8cb745", + "comment_kind": "Line", + "comment_text": "// No transactions = no details to match", + "comment_hash": "5fb94f8cb7458ed36ed972347a461f9eeabc1a78ef26ca0ed3b35d7c1771cee2", + "body_text": "let result = db_conn.autofill().details(\"anything\");\nassert_eq!(result, \"\");\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "b62b7ff326aa5f5885e4fb1ab28f352d167bf4dbb075399ac3e1504f151810fa" + }, + "app/tests/autofiller.rs|crate::autofill_details_unique_prefix_beats_a_closer_fuzzy_match#free:306796105fce": { + "kind": "Free", + "file": "app/tests/autofiller.rs", + "item_path": "crate::autofill_details_unique_prefix_beats_a_closer_fuzzy_match#free:306796105fce", + "comment_kind": "Line", + "comment_text": "// \"gy\" is a prefix of only one entry, even though it's fuzzily closer to the shorter one", + "comment_hash": "306796105fce11b794a5ede750f8dbe299b506b5cff45581ed8fa7ff1a83a12b", + "body_text": "assert_eq!(db_conn.autofill().details(\"gy\"), \"Gym membership\");\nassert_eq!(db_conn.autofill().details(\"gr\"), \"Groceries\");\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "24f28d3c1522f1dddd8a2d568cde34751d180834427fabbc996d0a0893ad73f9" + }, + "app/tests/autofiller.rs|crate::autofill_method_exact_returns_empty#free:c7a8a3181b56": { + "kind": "Free", + "file": "app/tests/autofiller.rs", + "item_path": "crate::autofill_method_exact_returns_empty#free:c7a8a3181b56", + "comment_kind": "Line", + "comment_text": "// Exact match produces no suggestion", + "comment_hash": "c7a8a3181b568e4f5c77d2e5ca00f13bc4d28ac76e0e3be17ae37e28a0759301", + "body_text": "let result = db_conn.autofill().tx_method(\"Cash\");\nassert_eq!(result, \"\");\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "1744f961a5de0020aee67443aa124941a2dc02f45f72da5ac9693febfa1a5be0" + }, + "app/tests/autofiller.rs|crate::autofill_recur_month_ambiguous_prefix_still_suggests_a_month#free:cf69a55edb91": { + "kind": "Free", + "file": "app/tests/autofiller.rs", + "item_path": "crate::autofill_recur_month_ambiguous_prefix_still_suggests_a_month#free:cf69a55edb91", + "comment_kind": "Line", + "comment_text": "// \"ju\" is June and July, \"ma\" is March and May", + "comment_hash": "cf69a55edb91debfce704a6954f9763d541697c378104d216c3d952432a87fcb", + "body_text": "assert!([\"June\", \"July\"].contains(&db_conn.autofill().recur_month(\"ju\").as_str()));\nassert!([\"March\", \"May\"].contains(&db_conn.autofill().recur_month(\"ma\").as_str()));\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "f137647ff51d325dde16b29730670722af5cf67578adef19de71dc2c8ca05dbf" + }, + "app/tests/autofiller.rs|crate::autofill_recur_value_weekly_ambiguous_prefix_still_suggests_a_day#free:011ae7bd03fe": { + "kind": "Free", + "file": "app/tests/autofiller.rs", + "item_path": "crate::autofill_recur_value_weekly_ambiguous_prefix_still_suggests_a_day#free:011ae7bd03fe", + "comment_kind": "Line", + "comment_text": "// \"t\" is both Tuesday and Thursday - falls back to a fuzzy pick rather than nothing", + "comment_hash": "011ae7bd03feb1fea7b7e54a8a3c5f949113d9583a50ca416d30651aef3f2352", + "body_text": "let suggestion = db_conn\n .autofill()\n .recur_value(\"t\", RecurrenceFrequency::Weekly);\nassert!(suggestion == \"Tuesday\" || suggestion == \"Thursday\");\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "2f8e55bafd59641555862076470e545d7e94ae057cdbd32133539af9a0df15aa" + }, + "app/tests/autofiller.rs|crate::autofill_tags_completes_only_the_last_tag#free:7220d1247ce3": { + "kind": "Free", + "file": "app/tests/autofiller.rs", + "item_path": "crate::autofill_tags_completes_only_the_last_tag#free:7220d1247ce3", + "comment_kind": "Line", + "comment_text": "// Nothing being typed for the next tag yet", + "comment_hash": "7220d1247ce3743f7800765c3042d65b4418c01695b9d2ef81208fd1b26fb37e", + "body_text": "assert_eq!(db_conn.autofill().tags(\"Food, \"), \"\");\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "546f2f59dcd6ca8ce2d8884eafc0a1a97b875fec1c7f5477c40ddb39d3863d7a" + }, + "app/tests/autofiller.rs|crate::autofill_tags_only_last_tag_suggested#free:85be1438acf4": { + "kind": "Free", + "file": "app/tests/autofiller.rs", + "item_path": "crate::autofill_tags_only_last_tag_suggested#free:85be1438acf4", + "comment_kind": "Line", + "comment_text": "// Only the last tag (after comma) is matched", + "comment_hash": "85be1438acf4ef8d8426f185b46d309a7774e228cfda17d9a7578f216ce9ae0e", + "body_text": "let result = db_conn.autofill().tags(\"Groceries, Sal\");\nassert_eq!(result, \"Salary\");\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "0a082c693a1d10b04b87b5557607e6d7920aab03bf4d2a435c067506318620e8" + }, + "app/tests/autofiller.rs|crate::autofill_tx_type_long_input_uses_prefix#free:0c202e141db5": { + "kind": "Free", + "file": "app/tests/autofiller.rs", + "item_path": "crate::autofill_tx_type_long_input_uses_prefix#free:0c202e141db5", + "comment_kind": "Line", + "comment_text": "// The short hand-written shortcuts still take priority", + "comment_hash": "0c202e141db5b299908622958e51788882f97d2da0cd88c4f60292f24e32c092", + "body_text": "assert_eq!(db_conn.autofill().tx_type(\"lr\"), \"Lend Repay\");\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "709c8ba423282033fd2c499f86df516c7a938582cf36ee3595212c9f0485cd91" + }, + "app/tests/chart_view.rs|crate::chart_view_get_tx_and_balance#free:327b2b0f5e06": { + "kind": "Free", + "file": "app/tests/chart_view.rs", + "item_path": "crate::chart_view_get_tx_and_balance#free:327b2b0f5e06", + "comment_kind": "Line", + "comment_text": "// Running balances: Income(+1000 Cash), Transfer(-300 Cash, +300 Bank), Expense(-100 Cash)", + "comment_hash": "327b2b0f5e068d61d0d349bb51e60aad48044f59c7e1e8131526b6a5f4299615", + "body_text": "let b0 = chart.get_balance(0);\nlet b1 = chart.get_balance(1);\nlet b2 = chart.get_balance(2);\nassert_eq!(b0[&cash_id].value(), 100000);\nassert_eq!(b0[&bank_id].value(), 0);\nassert_eq!(b1[&cash_id].value(), 70000);\nassert_eq!(b1[&bank_id].value(), 30000);\nassert_eq!(b2[&cash_id].value(), 60000);\nassert_eq!(b2[&bank_id].value(), 30000);\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "b20b0b7448152f81c96259c92eba75e6baedd1b4b29fb9d77d7440f22a26e087" + }, + "app/tests/delete_tx.rs|crate::delete_expense_reverses_balance#free:15db21900ec2": { + "kind": "Free", + "file": "app/tests/delete_tx.rs", + "item_path": "crate::delete_expense_reverses_balance#free:15db21900ec2", + "comment_kind": "Line", + "comment_text": "// First add income so we have a balance to spend from", + "comment_hash": "15db21900ec208f879da1fd6c58b70ce76caba751c64d722407ec0ec7d9183e0", + "body_text": "add_tx(\n &mut db_conn,\n \"2024-07-01\",\n \"Income\",\n \"Cash\",\n \"\",\n \"1000.00\",\n \"Income\",\n \"Salary\",\n );", + "body_hash": "a0d1f31211ab97ab645b1c96248f41699aa219138988bbdd576dd59d9ea36d2a" + }, + "app/tests/delete_tx.rs|crate::delete_expense_reverses_balance#free:b537b6ffec93": { + "kind": "Free", + "file": "app/tests/delete_tx.rs", + "item_path": "crate::delete_expense_reverses_balance#free:b537b6ffec93", + "comment_kind": "Line", + "comment_text": "// Delete the expense", + "comment_hash": "b537b6ffec9378bb19a96e0cb9d2ec17beb31bb09ec66ed5eaeae4afb0bf4f45", + "body_text": "db_conn.delete_tx(&tx).unwrap();\nlet tx_view = db_conn\n .fetch_txs_with_date(date, FetchNature::Monthly)\n .unwrap();\nassert_eq!(tx_view.len(), 1, \"Only the income should remain\");", + "body_hash": "093f5e574781269389475fb806d4813bb4e54509465e3ab71a17624dee70dcb3" + }, + "app/tests/delete_tx.rs|crate::delete_expense_reverses_balance#free:c6a269f3c863": { + "kind": "Free", + "file": "app/tests/delete_tx.rs", + "item_path": "crate::delete_expense_reverses_balance#free:c6a269f3c863", + "comment_kind": "Line", + "comment_text": "// Add an expense of $200", + "comment_hash": "c6a269f3c863b3a2a4d8c25dcb24b971898d64d0d7501dafa6eed0ece6766bf6", + "body_text": "let tx = add_tx(\n &mut db_conn,\n \"2024-07-15\",\n \"Rent\",\n \"Cash\",\n \"\",\n \"200.00\",\n \"Expense\",\n \"Rent\",\n );\nlet date = NaiveDate::from_ymd_opt(2024, 7, 1).unwrap();\nlet tx_view = db_conn\n .fetch_txs_with_date(date, FetchNature::Monthly)\n .unwrap();\nassert_eq!(tx_view.len(), 2);", + "body_hash": "c1e921696e586ef93fbc33745017c040419ff601ae2ab99ad4c79d4a551e8c7b" + }, + "app/tests/delete_tx.rs|crate::delete_expense_reverses_balance#free:db0fa599acea": { + "kind": "Free", + "file": "app/tests/delete_tx.rs", + "item_path": "crate::delete_expense_reverses_balance#free:db0fa599acea", + "comment_kind": "Line", + "comment_text": "// Balance should revert to 1000", + "comment_hash": "db0fa599acea8333087d4764337f24b96ce68db28de04c0ed2b5ba7947c9fcdd", + "body_text": "assert_eq!(tx_view.get_tx_balance(0)[&cash_id].value(), 100000);\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "7fefad52bf5fc5785d886537cbadafe8f705f9e4e513390897437b6e5d46e371" + }, + "app/tests/delete_tx.rs|crate::delete_expense_reverses_balance#free:e33e0403f7aa": { + "kind": "Free", + "file": "app/tests/delete_tx.rs", + "item_path": "crate::delete_expense_reverses_balance#free:e33e0403f7aa", + "comment_kind": "Line", + "comment_text": "// Balance after both txs: 1000 - 200 = 800", + "comment_hash": "e33e0403f7aa083bad9fd5583a01b09f0ae8c47fb454f6c245808fa80d56c2cb", + "body_text": "assert_eq!(tx_view.get_tx_balance(1)[&cash_id].value(), 80000);", + "body_hash": "b4dda3322bf4b4c0ebb23cc173f8b5cb9a760534ca324c8e0580a81e12282a57" + }, + "app/tests/delete_tx.rs|crate::delete_first_of_multiple_txs_maintains_other_balances#free:8c9e06f65adb": { + "kind": "Free", + "file": "app/tests/delete_tx.rs", + "item_path": "crate::delete_first_of_multiple_txs_maintains_other_balances#free:8c9e06f65adb", + "comment_kind": "Line", + "comment_text": "// Without the income: 0 - 100 - 50 = -150", + "comment_hash": "8c9e06f65adb5d8cb4c3976851182a9ba8784576cb93538cf88d894fc23b2184", + "body_text": "assert_eq!(tx_view.get_tx_balance(1)[&cash_id].value(), -15000);\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "ca5079b47eb4f990384aa12611019de017fa983b25911d7e07227d7238009730" + }, + "app/tests/delete_tx.rs|crate::delete_first_of_multiple_txs_maintains_other_balances#free:ed5fe1e6314a": { + "kind": "Free", + "file": "app/tests/delete_tx.rs", + "item_path": "crate::delete_first_of_multiple_txs_maintains_other_balances#free:ed5fe1e6314a", + "comment_kind": "Line", + "comment_text": "// Balance: 500 - 100 - 50 = 350", + "comment_hash": "ed5fe1e6314afd68ad8419c234439ad2595ac531c1c8c49f17f5f093ad601f11", + "body_text": "assert_eq!(tx_view.get_tx_balance(2)[&cash_id].value(), 35000);\ndb_conn.delete_tx(&tx1).unwrap();\nlet tx_view = db_conn\n .fetch_txs_with_date(date, FetchNature::Monthly)\n .unwrap();\nassert_eq!(tx_view.len(), 2);", + "body_hash": "24d35436950a215e2f391084a260913c71b572a5ce0369e65f70926f9fd1b53a" + }, + "app/tests/delete_tx.rs|crate::delete_middle_tx_maintains_remaining_order#free:2a7c4f36a92a": { + "kind": "Free", + "file": "app/tests/delete_tx.rs", + "item_path": "crate::delete_middle_tx_maintains_remaining_order#free:2a7c4f36a92a", + "comment_kind": "Line", + "comment_text": "// Details should be Salary and Food (Rent removed)", + "comment_hash": "2a7c4f36a92a53a182d7124f43f37e3629c21c7668191d9e309e003a1a67f66f", + "body_text": "assert_eq!(tx_view.get_tx(0).details, Some(\"Salary\".to_string()));\nassert_eq!(tx_view.get_tx(1).details, Some(\"Food\".to_string()));\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "6e2dc8242f5545b5adc66ace3e958a6096e5d9e3e3b0e269e93ef5ceb2023081" + }, + "app/tests/delete_tx.rs|crate::delete_transfer_reverses_both_methods#free:187e7215bfc2": { + "kind": "Free", + "file": "app/tests/delete_tx.rs", + "item_path": "crate::delete_transfer_reverses_both_methods#free:187e7215bfc2", + "comment_kind": "Line", + "comment_text": "// Cash back to 1000, Bank back to 0", + "comment_hash": "187e7215bfc2d4d607619ab66e347c13d6f05d73fd6e11adf68a805fa822ab92", + "body_text": "assert_eq!(tx_view.get_tx_balance(0)[&cash_id].value(), 100000);\nassert_eq!(tx_view.get_tx_balance(0)[&bank_id].value(), 0);\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "a53e009c6cc134dae030532622455016141be2ba4a5c7b8015227586b8043d63" + }, + "app/tests/delete_tx.rs|crate::delete_transfer_reverses_both_methods#free:9e3cbc261885": { + "kind": "Free", + "file": "app/tests/delete_tx.rs", + "item_path": "crate::delete_transfer_reverses_both_methods#free:9e3cbc261885", + "comment_kind": "Line", + "comment_text": "// Cash: 1000 - 300 = 700, Bank: 0 + 300 = 300", + "comment_hash": "9e3cbc2618850c30def04ade9c524844a21d25840389b35a8357b3c50ac2c949", + "body_text": "assert_eq!(tx_view.get_tx_balance(1)[&cash_id].value(), 70000);\nassert_eq!(tx_view.get_tx_balance(1)[&bank_id].value(), 30000);\ndb_conn.delete_tx(&tx).unwrap();\nlet tx_view = db_conn\n .fetch_txs_with_date(date, FetchNature::Monthly)\n .unwrap();\nassert_eq!(tx_view.len(), 1);", + "body_hash": "f709cb2989280a1ea37dcca5a73edc27b04a9ad886f3f8907e99b280a8f07614" + }, + "app/tests/delete_tx.rs|crate::delete_transfer_reverses_both_methods#free:a0bdcf8d9646": { + "kind": "Free", + "file": "app/tests/delete_tx.rs", + "item_path": "crate::delete_transfer_reverses_both_methods#free:a0bdcf8d9646", + "comment_kind": "Line", + "comment_text": "// Add income to Cash first", + "comment_hash": "a0bdcf8d96460857f11ec8c58590f641fdbd363389f402afa6defcf8aa5d4311", + "body_text": "add_tx(\n &mut db_conn,\n \"2024-08-01\",\n \"Income\",\n \"Cash\",\n \"\",\n \"1000.00\",\n \"Income\",\n \"Salary\",\n );\nlet tx = add_tx(\n &mut db_conn,\n \"2024-08-10\",\n \"Move to bank\",\n \"Cash\",\n \"Bank\",\n \"300.00\",\n \"Transfer\",\n \"Transfer\",\n );\nlet date = NaiveDate::from_ymd_opt(2024, 8, 1).unwrap();\nlet tx_view = db_conn\n .fetch_txs_with_date(date, FetchNature::Monthly)\n .unwrap();\nassert_eq!(tx_view.len(), 2);", + "body_hash": "dfbc4008cfd85f00cb1d9c440f28d8a021cf57f6f614cd1c35b05fcb49bf7700" + }, + "app/tests/edit_tx.rs|crate::edit_change_method_moves_balance#free:253402eb6e12": { + "kind": "Free", + "file": "app/tests/edit_tx.rs", + "item_path": "crate::edit_change_method_moves_balance#free:253402eb6e12", + "comment_kind": "Line", + "comment_text": "// Add income to Cash", + "comment_hash": "253402eb6e127ed0f830a1d7bdbfbc2140b8838d4df014cbe7c7f888cf7be0b3", + "body_text": "add_tx(\n &mut db_conn,\n \"2024-09-01\",\n \"Income\",\n \"Cash\",\n \"\",\n \"1000.00\",\n \"Income\",\n \"Salary\",\n );\nlet old_tx = add_tx(\n &mut db_conn,\n \"2024-09-10\",\n \"Coffee\",\n \"Cash\",\n \"\",\n \"50.00\",\n \"Expense\",\n \"Coffee\",\n );\nlet tx_view = db_conn\n .fetch_txs_with_date(date, FetchNature::Monthly)\n .unwrap();\nassert_eq!(tx_view.get_tx_balance(1)[&cash_id].value(), 95000);\nlet new_tx = parse_tx_fields(\n \"2024-09-10\",\n \"Coffee\",\n \"Bank\",\n \"\",\n \"50.00\",\n \"Expense\",\n &db_conn,\n )\n .unwrap();\ndb_conn.edit_tx(&old_tx, new_tx, \"Coffee\").unwrap();\nlet tx_view = db_conn\n .fetch_txs_with_date(date, FetchNature::Monthly)\n .unwrap();", + "body_hash": "142e0e6b733e7398f078d9dce375a2700ab3e61c7f4d882f378bc0d7a56b4bb2" + }, + "app/tests/edit_tx.rs|crate::edit_change_method_moves_balance#free:f3cbbf311c9e": { + "kind": "Free", + "file": "app/tests/edit_tx.rs", + "item_path": "crate::edit_change_method_moves_balance#free:f3cbbf311c9e", + "comment_kind": "Line", + "comment_text": "// Cash: 1000 (unchanged), Bank: -50", + "comment_hash": "f3cbbf311c9e52888710590c77d9a17b7a77a4f75a4b118b834edf43ca4567c2", + "body_text": "assert_eq!(tx_view.get_tx_balance(1)[&cash_id].value(), 100000);\nassert_eq!(tx_view.get_tx_balance(1)[&bank_id].value(), -5000);\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "4ce9eff68943eae9561df6207dce48bc864c1386b85e79b1374d13c7ec10ff78" + }, + "app/tests/edit_tx.rs|crate::edit_expense_amount_updates_balance#free:00c1f1a19833": { + "kind": "Free", + "file": "app/tests/edit_tx.rs", + "item_path": "crate::edit_expense_amount_updates_balance#free:00c1f1a19833", + "comment_kind": "Line", + "comment_text": "// 1000 - 300 = 700", + "comment_hash": "00c1f1a19833c8736c1e3fd6fd5b243cf39f01d8dc967d50f7d3b1d7833a2b78", + "body_text": "assert_eq!(tx_view.get_tx_balance(1)[&cash_id].value(), 70000);\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "4f2b917551ed5b53ac720b845399948d07ef9dbd34c05061e8d723446365cc74" + }, + "app/tests/edit_tx.rs|crate::edit_income_to_expense_reverses_balance#free:690e2ffa0c56": { + "kind": "Free", + "file": "app/tests/edit_tx.rs", + "item_path": "crate::edit_income_to_expense_reverses_balance#free:690e2ffa0c56", + "comment_kind": "Line", + "comment_text": "// Changed from +500 to -500", + "comment_hash": "690e2ffa0c56b2e3365d872ea9527e5f0f786f42845074c8d67c31260adf5984", + "body_text": "assert_eq!(tx_view.get_tx_balance(0)[&cash_id].value(), -50000);\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "d95ff84ec5719d13af7dd0fb48531281e8ef440d8cc747155a47363ec599f419" + }, + "app/tests/parse.rs|crate::parse_search_fields_year_only#free:0e1bb844bf73": { + "kind": "Free", + "file": "app/tests/parse.rs", + "item_path": "crate::parse_search_fields_year_only#free:0e1bb844bf73", + "comment_kind": "Line", + "comment_text": "// Year search should find nothing on empty DB", + "comment_hash": "0e1bb844bf73279fd609bb2324c46514f2ecb5a1f0236169c7ee54181be450d6", + "body_text": "let results = search.search_txs(&mut db_conn).unwrap();\nassert!(results.is_empty());\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "f9889d8f0a9f0e26a7598c44de3d2ba40e2f35f60799484ef0381daa101f079c" + }, + "app/tests/position_swap.rs|crate::swap_different_dates_returns_false#free:61692f40be74": { + "kind": "Free", + "file": "app/tests/position_swap.rs", + "item_path": "crate::swap_different_dates_returns_false#free:61692f40be74", + "comment_kind": "Line", + "comment_text": "// Fetch all txs for October to get both in one TxViewGroup", + "comment_hash": "61692f40be746c2105c0834659f0899953e873c2f6da140718c3315306183575", + "body_text": "let date = NaiveDate::from_ymd_opt(2024, 10, 1).unwrap();\nlet mut tx_view = db_conn\n .fetch_txs_with_date(date, FetchNature::Monthly)\n .unwrap();\nassert_eq!(tx_view.len(), 2);\nlet swapped = db_conn.swap_tx_position(0, 1, &mut tx_view).unwrap();\nassert!(!swapped, \"Cross-date swap should return false\");", + "body_hash": "74ee2d660218475ebd99792daabd37119ad50e8ee0597df48db02e869377fa57" + }, + "app/tests/position_swap.rs|crate::swap_different_dates_returns_false#free:8c66519dcb78": { + "kind": "Free", + "file": "app/tests/position_swap.rs", + "item_path": "crate::swap_different_dates_returns_false#free:8c66519dcb78", + "comment_kind": "Line", + "comment_text": "// Order unchanged — both still in October monthly view in original order", + "comment_hash": "8c66519dcb784627cc9d9c39beaf9615c10e6ae1572747a08901c27f9f989f9d", + "body_text": "assert_eq!(\n tx_details_order(&mut db_conn, date),\n vec![Some(\"Oct\".into()), Some(\"Oct2\".into())]\n );\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "d6febde5143aeb15fdfb4cd7020ca96045bddfd95d87436cacd58940f0ec6c42" + }, + "app/tests/position_swap.rs|crate::swap_four_txs_all_pairwise#free:373a3ab57c89": { + "kind": "Free", + "file": "app/tests/position_swap.rs", + "item_path": "crate::swap_four_txs_all_pairwise#free:373a3ab57c89", + "comment_kind": "Line", + "comment_text": "// A.do=2, B.do=3, D.do=1, C.do=4, so every order is set and the\n// resulting order is D, A, B, C", + "comment_hash": "373a3ab57c891ca5a5a9a27d5e8d6d218ff950d8eb6edb0f061ce1201acc7104", + "body_text": "let final_order = tx_details_order(&mut db_conn, date);\nassert_eq!(final_order.len(), 4);", + "body_hash": "15d99c4c5e1304a23a132f1a6a156c98da7086a04c8eb943d6328e336a5913b8" + }, + "app/tests/position_swap.rs|crate::swap_four_txs_all_pairwise#free:7e8c0cfde256": { + "kind": "Free", + "file": "app/tests/position_swap.rs", + "item_path": "crate::swap_four_txs_all_pairwise#free:7e8c0cfde256", + "comment_kind": "Line", + "comment_text": "// A.do=C.id, C.do=4, D.do=1, B.do=0\n// non-zero: D(1), C(4), A(3); zero: B(2) -> Order: D, C, A, B", + "comment_hash": "7e8c0cfde25687e5e448950b383b19e3d6e40210dea594945ad9692e41a800fc", + "body_text": "let mut tx_view = db_conn\n .fetch_txs_with_date(date, FetchNature::Monthly)\n .unwrap();", + "body_hash": "773669e1c28899c0b57c7059fd47cd1043c540a5338f270d35ecaa26ce1a6401" + }, + "app/tests/position_swap.rs|crate::swap_four_txs_all_pairwise#free:81109c064ed6": { + "kind": "Free", + "file": "app/tests/position_swap.rs", + "item_path": "crate::swap_four_txs_all_pairwise#free:81109c064ed6", + "comment_kind": "Line", + "comment_text": "// Three swaps, tracing the display orders after each one\n// Step 1: swap A(0) and D(3) -> B, C, A, D", + "comment_hash": "81109c064ed6c28107c0ecdf45895883de9419c24e05893f3b3b5bedc419175f", + "body_text": "let mut tx_view = db_conn\n .fetch_txs_with_date(date, FetchNature::Monthly)\n .unwrap();\ndb_conn.swap_tx_position(0, 3, &mut tx_view).unwrap();", + "body_hash": "4f2f6892dc1146de669ad0c061c1658057751cda5eb9af8db618cbbcc1a31091" + }, + "app/tests/position_swap.rs|crate::swap_four_txs_all_pairwise#free:95b2fc49338d": { + "kind": "Free", + "file": "app/tests/position_swap.rs", + "item_path": "crate::swap_four_txs_all_pairwise#free:95b2fc49338d", + "comment_kind": "Line", + "comment_text": "// A.do=D.id, B.do=0, C.do=0, D.do=A.id\n// non-zero: D(1), A(4); zero: B(2), C(3) -> Order: D, A, B, C", + "comment_hash": "95b2fc49338d6a58802f929d948de45e3048867da0d32964692350cca02b107d", + "body_text": "let mut tx_view = db_conn\n .fetch_txs_with_date(date, FetchNature::Monthly)\n .unwrap();", + "body_hash": "773669e1c28899c0b57c7059fd47cd1043c540a5338f270d35ecaa26ce1a6401" + }, + "app/tests/position_swap.rs|crate::swap_four_txs_all_pairwise#free:9985cf8bb826": { + "kind": "Free", + "file": "app/tests/position_swap.rs", + "item_path": "crate::swap_four_txs_all_pairwise#free:9985cf8bb826", + "comment_kind": "Line", + "comment_text": "// Step 2: swap A(1) and C(3)", + "comment_hash": "9985cf8bb82632657368ec7ba4641e4c171af15f21bf229253fd1f40cba7ee6f", + "body_text": "db_conn.swap_tx_position(1, 3, &mut tx_view).unwrap();", + "body_hash": "1240aa88286d5118431b16180bc4924517310ada1ebcd08099f05ec3c921a33a" + }, + "app/tests/position_swap.rs|crate::swap_four_txs_all_pairwise#free:a5b0d68bf690": { + "kind": "Free", + "file": "app/tests/position_swap.rs", + "item_path": "crate::swap_four_txs_all_pairwise#free:a5b0d68bf690", + "comment_kind": "Line", + "comment_text": "// All 4 should still be present", + "comment_hash": "a5b0d68bf690dc6d8dc23d0c3883074da682fb478b2d5be611164b2c44f2cbe0", + "body_text": "let all_details: Vec<&str> = final_order.iter().map(|d| d.as_deref().unwrap()).collect();\nassert!(all_details.contains(&\"A\"));\nassert!(all_details.contains(&\"B\"));\nassert!(all_details.contains(&\"C\"));\nassert!(all_details.contains(&\"D\"));\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "a43ff2398d4b992e6e28d8676d0fe8335de12267add647dd83fc486c985d2179" + }, + "app/tests/position_swap.rs|crate::swap_four_txs_all_pairwise#free:c71c73dc3c07": { + "kind": "Free", + "file": "app/tests/position_swap.rs", + "item_path": "crate::swap_four_txs_all_pairwise#free:c71c73dc3c07", + "comment_kind": "Line", + "comment_text": "// Step 3: swap A(2) and B(3)", + "comment_hash": "c71c73dc3c07870eb043c31313ba26c6fe3b9849450da89c86e89a7c126c8e0f", + "body_text": "db_conn.swap_tx_position(2, 3, &mut tx_view).unwrap();", + "body_hash": "3c590daf0fc06827985d937377181b48e794a1ee33ae75670f1dc911ce10d7a4" + }, + "app/tests/position_swap.rs|crate::swap_maintains_tx_integrity#free:123dd7bb69ac": { + "kind": "Free", + "file": "app/tests/position_swap.rs", + "item_path": "crate::swap_maintains_tx_integrity#free:123dd7bb69ac", + "comment_kind": "Line", + "comment_text": "// Re-fetch and verify each tx's data (type, amount, method, tags) is intact", + "comment_hash": "123dd7bb69accde95708f2ffec5cc73fd01cfa522cc958dc98188d2c20059a68", + "body_text": "let tx_view = db_conn\n .fetch_txs_with_date(date, FetchNature::Monthly)\n .unwrap();", + "body_hash": "dfc894826232cd9fdfca38564df53b15ed208e302e7a93257b23dfdbb6ebf904" + }, + "app/tests/position_swap.rs|crate::swap_maintains_tx_integrity#free:24b55b9830a1": { + "kind": "Free", + "file": "app/tests/position_swap.rs", + "item_path": "crate::swap_maintains_tx_integrity#free:24b55b9830a1", + "comment_kind": "Line", + "comment_text": "// After swap, order should be: Expense1, Income1, Expense2", + "comment_hash": "24b55b9830a154901b7fc1c934c432daf9de2519b09063c39c941ca0146eb1c2", + "body_text": "assert_eq!(tx_view.get_tx(0).details, Some(\"Expense1\".into()));\nassert_eq!(tx_view.get_tx(0).tags[0].name, \"Food\");\nassert_eq!(tx_view.get_tx(1).details, Some(\"Income1\".into()));\nassert_eq!(tx_view.get_tx(1).tags[0].name, \"Salary\");\nassert_eq!(tx_view.get_tx(2).details, Some(\"Expense2\".into()));\nassert_eq!(tx_view.get_tx(2).tags[0].name, \"Coffee\");", + "body_hash": "45f9aa6df6d70ac7a6a30cc559fdf10c62b7ebb38bcccde42a63a6ba734b0a35" + }, + "app/tests/position_swap.rs|crate::swap_maintains_tx_integrity#free:399b71a1d1e8": { + "kind": "Free", + "file": "app/tests/position_swap.rs", + "item_path": "crate::swap_maintains_tx_integrity#free:399b71a1d1e8", + "comment_kind": "Line", + "comment_text": "// Swap first two", + "comment_hash": "399b71a1d1e8d40cf84d9596b6a414fe57bd563e1df0d26f719ba6753d58c22c", + "body_text": "let mut tx_view = db_conn\n .fetch_txs_with_date(date, FetchNature::Monthly)\n .unwrap();\ndb_conn.swap_tx_position(0, 1, &mut tx_view).unwrap();", + "body_hash": "75578602736cd7f82d8698cf64bde5dac9bf44fbb84db3b009f2646432b3f5c3" + }, + "app/tests/position_swap.rs|crate::swap_maintains_tx_integrity#free:7ad390115958": { + "kind": "Free", + "file": "app/tests/position_swap.rs", + "item_path": "crate::swap_maintains_tx_integrity#free:7ad390115958", + "comment_kind": "Line", + "comment_text": "// After swap order: Expense1(-30), Income1(+100), Expense2(-20)\n// Running balances: -30, +70, +50", + "comment_hash": "7ad3901159588a206a4ccabb8db0e7d78befe68f8b718682057773cff5e123ee", + "body_text": "let cash_id = db_conn.cache().get_method_id(\"Cash\").unwrap();\nlet balances: Vec = (0..3)\n .map(|i| tx_view.get_tx_balance(i)[&cash_id].value())\n .collect();\nassert_eq!(balances, vec![-3000, 7000, 5000]);\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "9e464b49f2db26d1fd076dff2b0c7731742514dc3cb913dd82d7b7d371f6938f" + }, + "app/tests/position_swap.rs|crate::swap_persists_after_multiple_swaps#free:06f11e2a3cbd": { + "kind": "Free", + "file": "app/tests/position_swap.rs", + "item_path": "crate::swap_persists_after_multiple_swaps#free:06f11e2a3cbd", + "comment_kind": "Line", + "comment_text": "// Step 3: D↔A — swap do: D(2),A(4) → C, D, B, A", + "comment_hash": "06f11e2a3cbd3b09d487ca98dacd6583b930047cb3eaad48c86b42ad070b5b4a", + "body_text": "let mut tx_view = db_conn\n .fetch_txs_with_date(date, FetchNature::Monthly)\n .unwrap();\ndb_conn.swap_tx_position(1, 3, &mut tx_view).unwrap();\nassert_eq!(\n tx_details_order(&mut db_conn, date),\n vec![\n Some(\"C\".into()),\n Some(\"D\".into()),\n Some(\"B\".into()),\n Some(\"A\".into()),\n ]\n );\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "ba6cb567b37c579d9801c066f5327803afc91e5922227765705a8077fe434ad3" + }, + "app/tests/position_swap.rs|crate::swap_persists_after_multiple_swaps#free:3b37ef934495": { + "kind": "Free", + "file": "app/tests/position_swap.rs", + "item_path": "crate::swap_persists_after_multiple_swaps#free:3b37ef934495", + "comment_kind": "Line", + "comment_text": "// Swap A(0) and B(1)", + "comment_hash": "3b37ef9344956cedae3f96fa3e7afa132ba84d4787b8513621e71cae77b970bb", + "body_text": "let mut tx_view = db_conn\n .fetch_txs_with_date(date, FetchNature::Monthly)\n .unwrap();\ndb_conn.swap_tx_position(0, 1, &mut tx_view).unwrap();", + "body_hash": "75578602736cd7f82d8698cf64bde5dac9bf44fbb84db3b009f2646432b3f5c3" + }, + "app/tests/position_swap.rs|crate::swap_persists_after_multiple_swaps#free:ad3e1dab4fac": { + "kind": "Free", + "file": "app/tests/position_swap.rs", + "item_path": "crate::swap_persists_after_multiple_swaps#free:ad3e1dab4fac", + "comment_kind": "Line", + "comment_text": "// Step 1: A↔B — normalize: A(1),B(2),C(3),D(4); swap: A(2),B(1) → B, A, C, D", + "comment_hash": "ad3e1dab4fac20c513b7cba3d92776fc00977376e23f7e8f384048793f6ae745", + "body_text": "assert_eq!(\n tx_details_order(&mut db_conn, date),\n vec![\n Some(\"B\".into()),\n Some(\"A\".into()),\n Some(\"C\".into()),\n Some(\"D\".into()),\n ]\n );", + "body_hash": "cdfbc8fa58c2831ecf7375b68ad6acb07bd23af556b7eddbb3c0e44344d56ffc" + }, + "app/tests/position_swap.rs|crate::swap_persists_after_multiple_swaps#free:c5ea0baac82f": { + "kind": "Free", + "file": "app/tests/position_swap.rs", + "item_path": "crate::swap_persists_after_multiple_swaps#free:c5ea0baac82f", + "comment_kind": "Line", + "comment_text": "// Step 2: B↔C — swap do: B(3),C(1) → C, A, B, D", + "comment_hash": "c5ea0baac82fb467f8afd20ca5a998a101031c65adbb4073e15e26e1510f7094", + "body_text": "let mut tx_view = db_conn\n .fetch_txs_with_date(date, FetchNature::Monthly)\n .unwrap();\ndb_conn.swap_tx_position(0, 2, &mut tx_view).unwrap();\nassert_eq!(\n tx_details_order(&mut db_conn, date),\n vec![\n Some(\"C\".into()),\n Some(\"A\".into()),\n Some(\"B\".into()),\n Some(\"D\".into()),\n ]\n );", + "body_hash": "2aec997f80ecbe347920af6f266094edca213ed29901dcd576b61a73ac4f4e58" + }, + "app/tests/position_swap.rs|crate::swap_persists_after_multiple_swaps#free:ca29bc38603d": { + "kind": "Free", + "file": "app/tests/position_swap.rs", + "item_path": "crate::swap_persists_after_multiple_swaps#free:ca29bc38603d", + "comment_kind": "Line", + "comment_text": "// Add 4 txs all on same day", + "comment_hash": "ca29bc38603de97c68eb26ab58dd4aff45ebb4c29a7ed81eca86b11176bbdc32", + "body_text": "add_tx(\n &mut db_conn,\n \"2024-12-01\",\n \"A\",\n \"Cash\",\n \"\",\n \"10.00\",\n \"Expense\",\n \"A\",\n );\nadd_tx(\n &mut db_conn,\n \"2024-12-01\",\n \"B\",\n \"Cash\",\n \"\",\n \"20.00\",\n \"Expense\",\n \"B\",\n );\nadd_tx(\n &mut db_conn,\n \"2024-12-01\",\n \"C\",\n \"Cash\",\n \"\",\n \"30.00\",\n \"Expense\",\n \"C\",\n );\nadd_tx(\n &mut db_conn,\n \"2024-12-01\",\n \"D\",\n \"Cash\",\n \"\",\n \"40.00\",\n \"Expense\",\n \"D\",\n );", + "body_hash": "94a5dba5d2e0b0deec7599de9a59034aef1e8c9427dc55a3cc842a4d969da1b8" + }, + "app/tests/position_swap.rs|crate::swap_persists_after_multiple_swaps#free:fc96b5346e46": { + "kind": "Free", + "file": "app/tests/position_swap.rs", + "item_path": "crate::swap_persists_after_multiple_swaps#free:fc96b5346e46", + "comment_kind": "Line", + "comment_text": "// Initial: A, B, C, D", + "comment_hash": "fc96b5346e46cabb87bc3958fad77936e5fdfbac8a4fee31fe67fea00db74f12", + "body_text": "assert_eq!(\n tx_details_order(&mut db_conn, date),\n vec![\n Some(\"A\".into()),\n Some(\"B\".into()),\n Some(\"C\".into()),\n Some(\"D\".into()),\n ]\n );", + "body_hash": "39a2a6d466f413f998a625e0336f42387fd8c3300821f92170d7cfbca5b7498e" + }, + "app/tests/position_swap.rs|crate::swap_single_tx_noop#free:3fc7e90036dd": { + "kind": "Free", + "file": "app/tests/position_swap.rs", + "item_path": "crate::swap_single_tx_noop#free:3fc7e90036dd", + "comment_kind": "Line", + "comment_text": "// Swapping the only tx with itself succeeds", + "comment_hash": "3fc7e90036dd20593084031c527cd3b5f2ba0dac75710bb26bef6d07c220331a", + "body_text": "let swapped = db_conn.swap_tx_position(0, 0, &mut tx_view).unwrap();\nassert!(swapped);\nassert_eq!(\n tx_details_order(&mut db_conn, date),\n vec![Some(\"Only\".into())]\n );\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "4191f36b6375a44802303175e29243228d2630209c686cd8a7879796899c8f45" + }, + "app/tests/position_swap.rs|crate::swap_three_txs_adjacent_middle_pair#free:26d630f5171d": { + "kind": "Free", + "file": "app/tests/position_swap.rs", + "item_path": "crate::swap_three_txs_adjacent_middle_pair#free:26d630f5171d", + "comment_kind": "Line", + "comment_text": "// Swap Middle(1) and Last(2)", + "comment_hash": "26d630f5171dce2dc986c7af60f58d58d59ad1ee3aeb7d37796531a138015dd5", + "body_text": "let mut tx_view = db_conn\n .fetch_txs_with_date(date, FetchNature::Monthly)\n .unwrap();\ndb_conn.swap_tx_position(1, 2, &mut tx_view).unwrap();", + "body_hash": "3669da0b9c4aaa368d7e3d7f8eba4c82a19488a1ec5a60064e736e0dffb1934d" + }, + "app/tests/position_swap.rs|crate::swap_three_txs_adjacent_middle_pair#free:3b8ce670070f": { + "kind": "Free", + "file": "app/tests/position_swap.rs", + "item_path": "crate::swap_three_txs_adjacent_middle_pair#free:3b8ce670070f", + "comment_kind": "Line", + "comment_text": "// Initial: First, Middle, Last", + "comment_hash": "3b8ce670070f7804cae6829aa4a158104e60a6a151de42a528a43437a85f92a5", + "body_text": "assert_eq!(\n tx_details_order(&mut db_conn, date),\n vec![\n Some(\"First\".into()),\n Some(\"Middle\".into()),\n Some(\"Last\".into())\n ]\n );", + "body_hash": "b0e9c852c321dd8918970a9626f33436514a17c1d312d881e7117eb8e06b57ca" + }, + "app/tests/position_swap.rs|crate::swap_three_txs_adjacent_middle_pair#free:caf2bc95ffa7": { + "kind": "Free", + "file": "app/tests/position_swap.rs", + "item_path": "crate::swap_three_txs_adjacent_middle_pair#free:caf2bc95ffa7", + "comment_kind": "Line", + "comment_text": "// Normalized: First(1), Middle(2), Last(3) → swap Middle↔Last: First(1), Last(2), Middle(3)", + "comment_hash": "caf2bc95ffa760d883ad31e0eb4994b21f59f6a5e96f0705780cf11955f4cd72", + "body_text": "assert_eq!(\n tx_details_order(&mut db_conn, date),\n vec![\n Some(\"First\".into()),\n Some(\"Last\".into()),\n Some(\"Middle\".into())\n ]\n );\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "2d59cb654ac7255a47b84542fd4cf5cfaff360982d098629c172bb61aa843b7d" + }, + "app/tests/position_swap.rs|crate::swap_three_txs_first_and_last#free:3b8ce670070f": { + "kind": "Free", + "file": "app/tests/position_swap.rs", + "item_path": "crate::swap_three_txs_first_and_last#free:3b8ce670070f", + "comment_kind": "Line", + "comment_text": "// Initial: First, Middle, Last", + "comment_hash": "3b8ce670070f7804cae6829aa4a158104e60a6a151de42a528a43437a85f92a5", + "body_text": "assert_eq!(\n tx_details_order(&mut db_conn, date),\n vec![\n Some(\"First\".into()),\n Some(\"Middle\".into()),\n Some(\"Last\".into())\n ]\n );", + "body_hash": "b0e9c852c321dd8918970a9626f33436514a17c1d312d881e7117eb8e06b57ca" + }, + "app/tests/position_swap.rs|crate::swap_three_txs_first_and_last#free:49878c4d15f7": { + "kind": "Free", + "file": "app/tests/position_swap.rs", + "item_path": "crate::swap_three_txs_first_and_last#free:49878c4d15f7", + "comment_kind": "Line", + "comment_text": "// Swap first(0) and last(2)", + "comment_hash": "49878c4d15f760eee19c134a5f18ff402687ebf1f8fdd5e4a4c0b9ce2bf5ecdd", + "body_text": "let mut tx_view = db_conn\n .fetch_txs_with_date(date, FetchNature::Monthly)\n .unwrap();\ndb_conn.swap_tx_position(0, 2, &mut tx_view).unwrap();", + "body_hash": "b09436ad4812f07c451f2f43087cd48162e07e3a50daea1c16c8dfc3ba13e524" + }, + "app/tests/position_swap.rs|crate::swap_three_txs_first_and_last#free:763fb0f19f16": { + "kind": "Free", + "file": "app/tests/position_swap.rs", + "item_path": "crate::swap_three_txs_first_and_last#free:763fb0f19f16", + "comment_kind": "Line", + "comment_text": "// Normalized: First(1), Middle(2), Last(3) → swap First↔Last: Last(1), Middle(2), First(3)", + "comment_hash": "763fb0f19f16505159518f2d97e5887e6a0e001957daa7d2cdd331f8bebdc0bb", + "body_text": "assert_eq!(\n tx_details_order(&mut db_conn, date),\n vec![\n Some(\"Last\".into()),\n Some(\"Middle\".into()),\n Some(\"First\".into())\n ]\n );\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "2075e1df93bd5455269542a012e422f0e5777555ed4a48a98cdd3fbb96cc81e8" + }, + "app/tests/position_swap.rs|crate::swap_three_txs_first_and_middle#free:ecb3f50baec4": { + "kind": "Free", + "file": "app/tests/position_swap.rs", + "item_path": "crate::swap_three_txs_first_and_middle#free:ecb3f50baec4", + "comment_kind": "Line", + "comment_text": "// First.do=Middle.id, Middle.do=First.id, Last.do=0\n// Sort: non-zero orders first, then the zero ones. With First.id=1 and\n// Middle.id=2 that gives Middle(1), First(2), Last(0)", + "comment_hash": "ecb3f50baec453e48d2647180038c4254a5f345429179241fef428a8d92613a0", + "body_text": "assert_eq!(\n tx_details_order(&mut db_conn, date),\n vec![\n Some(\"Middle\".into()),\n Some(\"First\".into()),\n Some(\"Last\".into())\n ]\n );\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "3136e0f116d34ef91f77c2a6893fef5ac3af45be966919ef0d52361fd0ea9da9" + }, + "app/tests/position_swap.rs|crate::swap_three_txs_first_and_middle#free:f4a5c4401da0": { + "kind": "Free", + "file": "app/tests/position_swap.rs", + "item_path": "crate::swap_three_txs_first_and_middle#free:f4a5c4401da0", + "comment_kind": "Line", + "comment_text": "// Swap First(0) and Middle(1)", + "comment_hash": "f4a5c4401da0155f71e8abd6800d397354f65e664ade1229c27f8347478fb788", + "body_text": "let mut tx_view = db_conn\n .fetch_txs_with_date(date, FetchNature::Monthly)\n .unwrap();\ndb_conn.swap_tx_position(0, 1, &mut tx_view).unwrap();", + "body_hash": "75578602736cd7f82d8698cf64bde5dac9bf44fbb84db3b009f2646432b3f5c3" + }, + "app/tests/position_swap.rs|crate::swap_two_txs_twice_restores_original_order#free:4d89486d6007": { + "kind": "Free", + "file": "app/tests/position_swap.rs", + "item_path": "crate::swap_two_txs_twice_restores_original_order#free:4d89486d6007", + "comment_kind": "Line", + "comment_text": "// First swap", + "comment_hash": "4d89486d6007545e17303893e22be2f8a9e84c41d998cfac522cd2c32d96f06a", + "body_text": "let mut tx_view = db_conn\n .fetch_txs_with_date(date, FetchNature::Monthly)\n .unwrap();\ndb_conn.swap_tx_position(0, 1, &mut tx_view).unwrap();\nassert_eq!(\n tx_details_order(&mut db_conn, date),\n vec![Some(\"Second\".into()), Some(\"First\".into())]\n );", + "body_hash": "15c1e41d4a286642763b39edbeb5b25fc9f9603b98ddf407cc00f5fe23e8ceb2" + }, + "app/tests/position_swap.rs|crate::swap_two_txs_twice_restores_original_order#free:4e401713e90c": { + "kind": "Free", + "file": "app/tests/position_swap.rs", + "item_path": "crate::swap_two_txs_twice_restores_original_order#free:4e401713e90c", + "comment_kind": "Line", + "comment_text": "// Second swap restores", + "comment_hash": "4e401713e90cdae2205c9908a2d78809bc9585dfa3f6f0e2f2d0f29ef4e8c517", + "body_text": "let mut tx_view = db_conn\n .fetch_txs_with_date(date, FetchNature::Monthly)\n .unwrap();\ndb_conn.swap_tx_position(0, 1, &mut tx_view).unwrap();\nassert_eq!(\n tx_details_order(&mut db_conn, date),\n vec![Some(\"First\".into()), Some(\"Second\".into())]\n );\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "cbd0ebb602e4d4f431a892cc1a5883a6938c88b07886eb4094980915a214ec72" + }, + "app/tests/recurring_tx.rs|crate::add_daily_recurring_tx_shows_up_in_list#free:2da64563c83e": { + "kind": "Free", + "file": "app/tests/recurring_tx.rs", + "item_path": "crate::add_daily_recurring_tx_shows_up_in_list#free:2da64563c83e", + "comment_kind": "Line", + "comment_text": "// Start date is in the past, so adding the rule already created its due occurrences\n// and advanced the schedule past today", + "comment_hash": "2da64563c83e0c065aac5b8d6f14e7cc1836b5e9cad81433b4980cbecd1f8ade", + "body_text": "let today = Local::now().date_naive();\nassert_eq!(all[0].last_recurred_date, Some(today));\nassert!(all[0].next_recurring_date > today);\nassert!(!all[0].is_paused);\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "bf3e6e1fe77e50d112f330d779b2ec7314f9e15e81002d38c89f1e5ac3298715" + }, + "app/tests/recurring_tx.rs|crate::biweekly_recurring_tx_spaces_occurrences_two_weeks_apart#free:d81ed8523513": { + "kind": "Free", + "file": "app/tests/recurring_tx.rs", + "item_path": "crate::biweekly_recurring_tx_spaces_occurrences_two_weeks_apart#free:d81ed8523513", + "comment_kind": "Line", + "comment_text": "// 2024-01-01 is already a Monday, so it satisfies the rule immediately", + "comment_hash": "d81ed8523513ea5074fb4b8c5af7e052307f1e290fb51d0363e00db912be376c", + "body_text": "let new_recurring = parse_recurring_tx_fields(\n \"2024-01-01\",\n \"Paycheck\",\n \"Cash\",\n \"\",\n \"500.00\",\n \"Income\",\n \"Weekly\",\n \"2\",\n \"Monday\",\n \"\",\n \"\",\n &db_conn,\n )\n .unwrap();\nassert_eq!(\n new_recurring.next_recurring_date,\n NaiveDate::from_ymd_opt(2024, 1, 1).unwrap()\n );\ndb_conn.add_recurring_tx(new_recurring, \"Work\").unwrap();\nlet created = materialized_count(&mut db_conn);\nassert!(created > 0);\nlet tx_view = db_conn\n .fetch_txs_with_date(\n NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),\n FetchNature::All,\n )\n .unwrap();\nlet dates: Vec = (0..tx_view.len())\n .map(|i| tx_view.get_tx(i).date.date())\n .collect();\nfor date in &dates {}", + "body_hash": "7864c7b955c4ecdc3ce372821ebeb37b5c6f37bd1b825992413780b48ddc4998" + }, + "app/tests/recurring_tx.rs|crate::biweekly_recurring_tx_spaces_occurrences_two_weeks_apart#free:da42bb0cf43a": { + "kind": "Free", + "file": "app/tests/recurring_tx.rs", + "item_path": "crate::biweekly_recurring_tx_spaces_occurrences_two_weeks_apart#free:da42bb0cf43a", + "comment_kind": "Line", + "comment_text": "// Every-2-weeks: exactly 14 days apart, never 7", + "comment_hash": "da42bb0cf43a9de594ff091d342078ea81a40a2ebe52c48502bde65e8a53e722", + "body_text": "for pair in dates.windows(2) {}\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "fcb1918e6941870f078ca95ec2181dc99620940cbe5e3231140b386d141dcfba" + }, + "app/tests/recurring_tx.rs|crate::end_date_stops_generating_further_occurrences#free:bbda90fee22f": { + "kind": "Free", + "file": "app/tests/recurring_tx.rs", + "item_path": "crate::end_date_stops_generating_further_occurrences#free:bbda90fee22f", + "comment_kind": "Line", + "comment_text": "// Running it again should not create any more", + "comment_hash": "bbda90fee22fbbbc6c03ec586066e30e1bf55c0e06a20f9f524fca3931930a90", + "body_text": "let created_again = db_conn.process_due_recurring_txs().unwrap();\nassert_eq!(created_again, 0);\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "d68a5516c2cc300ea30908d3f67327bfe0c597e08f4c8bb82e20a04812b0c20f" + }, + "app/tests/recurring_tx.rs|crate::end_date_stops_generating_further_occurrences#free:c9eaa2756cae": { + "kind": "Free", + "file": "app/tests/recurring_tx.rs", + "item_path": "crate::end_date_stops_generating_further_occurrences#free:c9eaa2756cae", + "comment_kind": "Line", + "comment_text": "// 2024-01-01 through 2024-01-05 inclusive = 5 occurrences, regardless of how long\n// ago that end date is relative to \"today\"", + "comment_hash": "c9eaa2756caeb7f898de8bdab0313c03288a4a7c11fda6e4ca01886e36912527", + "body_text": "assert_eq!(created, 5);", + "body_hash": "8ed7d3a8eb70cc281542c83f8518cb138dfae4f44a9e484bfd265e76af5c83bb" + }, + "app/tests/recurring_tx.rs|crate::materialized_count": { + "kind": "Function", + "file": "app/tests/recurring_tx.rs", + "item_path": "crate::materialized_count", + "comment_kind": "OuterDoc", + "comment_text": " Adding or editing a rule already processes everything due, so the number of\n\n materialized transactions is simply the number of transactions present. A follow-up\n\n explicit run must then have nothing left to do.\n", + "comment_hash": "d9789dc3ab73c7898eee050ab7ea627c0e169045f36a42d5d4019347ac9d9245", + "body_text": "fn materialized_count(db_conn: &mut DbConn) -> usize {\n let count = db_conn\n .fetch_txs_with_date(\n NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),\n FetchNature::All,\n )\n .unwrap()\n .len();\n\n assert_eq!(db_conn.process_due_recurring_txs().unwrap(), 0);\n\n count\n}", + "body_hash": "e71d8ec03b957f2eb1aaacd548bdd124c23d32c61e0a5fb51ede341ed1ef9117" + }, + "app/tests/recurring_tx.rs|crate::paused_recurring_tx_is_skipped_by_processing#free:1ec7be99c83c": { + "kind": "Free", + "file": "app/tests/recurring_tx.rs", + "item_path": "crate::paused_recurring_tx_is_skipped_by_processing#free:1ec7be99c83c", + "comment_kind": "Line", + "comment_text": "// Adding already processed everything due. Pause it, then rewind its schedule so it is\n// due again - a paused rule must still be skipped.", + "comment_hash": "1ec7be99c83ca858fc6e8281015eb3dac232afdc2f8fccd7e54a5ce1ab37a5ed", + "body_text": "let before = materialized_count(&mut db_conn);\nassert!(before > 0);\nlet id = db_conn.get_recurring_txs().unwrap()[0].id;\ndb_conn.set_recurring_paused(id, true).unwrap();\nRecurringTx::update_schedule(\n id,\n None,\n NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),\n &mut db_conn,\n )\n .unwrap();\nlet created = db_conn.process_due_recurring_txs().unwrap();\nassert_eq!(created, 0);\nassert_eq!(materialized_count(&mut db_conn), before);\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "34e8222ff67c407833bdad2e54cf588900d649e4aceebb5d95380b7b100bd2be" + }, + "app/tests/recurring_tx.rs|crate::process_due_recurring_txs_catches_up_multiple_missed_daily_occurrences#free:75e5edf432d3": { + "kind": "Free", + "file": "app/tests/recurring_tx.rs", + "item_path": "crate::process_due_recurring_txs_catches_up_multiple_missed_daily_occurrences#free:75e5edf432d3", + "comment_kind": "Line", + "comment_text": "// From 2024-01-01 to today is a lot more than a handful of days", + "comment_hash": "75e5edf432d3f26d60fd4ac19108aed4de0fab0bc31d3ef9b99beb6620b59c41", + "body_text": "assert!(created > 30);\nlet tx_view = db_conn\n .fetch_txs_with_date(\n NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),\n FetchNature::All,\n )\n .unwrap();\nassert_eq!(tx_view.len(), created);\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "f6ad0e7f191230e1c97bf117dc3970cc3b10f0669a7af599e431feddeaa7f96f" + }, + "app/tests/recurring_tx.rs|crate::weekly_recurring_tx_fires_only_on_the_chosen_weekday#free:23e3a3919633": { + "kind": "Free", + "file": "app/tests/recurring_tx.rs", + "item_path": "crate::weekly_recurring_tx_fires_only_on_the_chosen_weekday#free:23e3a3919633", + "comment_kind": "Line", + "comment_text": "// Every occurrence exactly 7 days after the previous one - no drift", + "comment_hash": "23e3a391963305be7edbad53e97d38ad66cdd19a5d80d5e1186c2a652f16e4aa", + "body_text": "for pair in dates.windows(2) {}\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "fcb1918e6941870f078ca95ec2181dc99620940cbe5e3231140b386d141dcfba" + }, + "app/tests/recurring_tx.rs|crate::weekly_recurring_tx_fires_only_on_the_chosen_weekday#free:3fe65fd13463": { + "kind": "Free", + "file": "app/tests/recurring_tx.rs", + "item_path": "crate::weekly_recurring_tx_fires_only_on_the_chosen_weekday#free:3fe65fd13463", + "comment_kind": "Line", + "comment_text": "// 2024-01-01 is a Monday; choosing Friday should roll the first occurrence\n// forward to 2024-01-05, then every 7 days after that.", + "comment_hash": "3fe65fd13463a0a3eb3e8c8dab10a29850aa06c5b12bc5c88b742bb941857c8b", + "body_text": "let new_recurring = parse_recurring_tx_fields(\n \"2024-01-01\",\n \"Standing order\",\n \"Cash\",\n \"\",\n \"20.00\",\n \"Expense\",\n \"Weekly\",\n \"1\",\n \"Friday\",\n \"\",\n \"\",\n &db_conn,\n )\n .unwrap();\nassert_eq!(\n new_recurring.next_recurring_date,\n NaiveDate::from_ymd_opt(2024, 1, 5).unwrap()\n );\ndb_conn.add_recurring_tx(new_recurring, \"Bills\").unwrap();\nlet created = materialized_count(&mut db_conn);\nassert!(created > 0);\nlet tx_view = db_conn\n .fetch_txs_with_date(\n NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),\n FetchNature::All,\n )\n .unwrap();\nlet dates: Vec = (0..tx_view.len())\n .map(|i| tx_view.get_tx(i).date.date())\n .collect();\nassert_eq!(dates.len(), created);\nassert_eq!(dates[0], NaiveDate::from_ymd_opt(2024, 1, 5).unwrap());\nfor date in &dates {}", + "body_hash": "6f073007aa26674c2a2db97b7869ed73e0598c6954349311abe04ac57b0f59cd" + }, + "app/tests/recurring_tx.rs|crate::weekly_recurring_tx_rolls_forward_when_start_date_is_a_different_weekday#free:7aad2a5f1c67": { + "kind": "Free", + "file": "app/tests/recurring_tx.rs", + "item_path": "crate::weekly_recurring_tx_rolls_forward_when_start_date_is_a_different_weekday#free:7aad2a5f1c67", + "comment_kind": "Line", + "comment_text": "// The start date is just an anchor, not a constraint on which weekday is picked.\n// 2024-06-01 is a Saturday - choosing Monday should roll the first occurrence\n// forward to 2024-06-03, not error.", + "comment_hash": "7aad2a5f1c67a5a4bbe58989026fec682436076aa7b27fef46b994ba162fb615", + "body_text": "let new_recurring = parse_recurring_tx_fields(\n \"2024-06-01\",\n \"\",\n \"Cash\",\n \"\",\n \"10.00\",\n \"Expense\",\n \"Weekly\",\n \"1\",\n \"Monday\",\n \"\",\n \"\",\n &db_conn,\n )\n .unwrap();\nassert_eq!(\n new_recurring.next_recurring_date,\n NaiveDate::from_ymd_opt(2024, 6, 3).unwrap()\n );\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "8bcfbce6b74355496211c6f17323f578fc3a6cc40fd5d2cb92c20bafcd8e7d7f" + }, + "app/tests/search_amount.rs|crate::search_amount_combined_with_other_filters#free:e093b564b3f8": { + "kind": "Free", + "file": "app/tests/search_amount.rs", + "item_path": "crate::search_amount_combined_with_other_filters#free:e093b564b3f8", + "comment_kind": "Line", + "comment_text": "// Expense Groceries > $50 in June 2024", + "comment_hash": "e093b564b3f8551d0153fe512d8091ec306d3956d7b7f1166c78722a204a8436", + "body_text": "let search = parse_search_fields(\n \"2024-06\",\n \"Groceries\",\n \"Cash\",\n \"\",\n \">50.00\",\n \"Expense\",\n \"Food\",\n &db_conn,\n )\n .unwrap();\nlet results = search.search_txs(&mut db_conn).unwrap();\nassert_eq!(results.len(), 1);\nassert_eq!(results[0].amount.value(), 8000);\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "a525cdc41ec4ce46d79347ad08b063cffc0d11440dd435160348e00a4885fabd" + }, + "app/tests/search_view.rs|crate::search_view_no_results#free:593171d76358": { + "kind": "Free", + "file": "app/tests/search_view.rs", + "item_path": "crate::search_view_no_results#free:593171d76358", + "comment_kind": "Line", + "comment_text": "// Search for something that doesn't match", + "comment_hash": "593171d76358beedc045358bfda62ccb390fd4c1d270ac72c20539294dcec2ca", + "body_text": "let search = parse_search_fields(\"\", \"\", \"\", \"\", \"\", \"Income\", \"\", &db_conn).unwrap();\nlet view = db_conn.search_txs(search).unwrap();\nassert!(view.is_empty());\nassert_eq!(view.tx_array().len(), 0);\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "95fda9368a9e1074229191e31e0698646f699168fdcf50f81a2f03054c4e3a47" + }, + "app/tests/search_view.rs|crate::search_view_with_results#free:ddb9db2a2406": { + "kind": "Free", + "file": "app/tests/search_view.rs", + "item_path": "crate::search_view_with_results#free:ddb9db2a2406", + "comment_kind": "Line", + "comment_text": "// Each row has 6 columns: date, details, method, amount, tx_type, tags", + "comment_hash": "ddb9db2a24069048351f0279aff60a1b8ce4392196aceee63397350cfa11e9ea", + "body_text": "assert_eq!(arr[0].len(), 6);\nassert_eq!(arr[0][1], \"Rent\");\nassert_eq!(arr[0][4], \"Expense\");\nassert_eq!(arr[1][1], \"Salary\");\nassert_eq!(arr[1][4], \"Income\");\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "f9c50c0fe7eb01d22c355a5d02b0f414ea11afeb4233e81990a3b9cdf361c2d1" + }, + "app/tests/stepper.rs|crate#free:0483c3e673cc": { + "kind": "Free", + "file": "app/tests/stepper.rs", + "item_path": "crate#free:0483c3e673cc", + "comment_kind": "Line", + "comment_text": "// ---- Recurring: Recur Value stepping ----", + "comment_hash": "0483c3e673cc053e15c48c760f58c29c602eff472b8b3066649c37d9bb46506e", + "body_text": "#[test]\nfn step_recur_value_daily_is_a_no_op() {\n let file_name = \"test_step_recur_value_daily.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = String::new();\n db_conn\n .step()\n .recur_value(&mut s, RecurrenceFrequency::Daily, StepType::StepUp)\n .unwrap();\n assert_eq!(s, \"\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn step_recur_value_weekly_from_empty_defaults_to_sunday() {\n let file_name = \"test_step_recur_value_weekly_empty.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = String::new();\n db_conn\n .step()\n .recur_value(&mut s, RecurrenceFrequency::Weekly, StepType::StepUp)\n .unwrap();\n assert_eq!(s, \"Sunday\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn step_recur_value_weekly_cycles_all_days() {\n let file_name = \"test_step_recur_value_weekly_cycle.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"Sunday\".to_string();\n\n for expected in [\n \"Monday\",\n \"Tuesday\",\n \"Wednesday\",\n \"Thursday\",\n \"Friday\",\n \"Saturday\",\n \"Sunday\",\n ] {\n db_conn\n .step()\n .recur_value(&mut s, RecurrenceFrequency::Weekly, StepType::StepUp)\n .unwrap();\n assert_eq!(s, expected);\n }\n\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn step_recur_value_weekly_down_wraps() {\n let file_name = \"test_step_recur_value_weekly_wrap.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"Sunday\".to_string();\n db_conn\n .step()\n .recur_value(&mut s, RecurrenceFrequency::Weekly, StepType::StepDown)\n .unwrap();\n assert_eq!(s, \"Saturday\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn step_recur_value_monthly_from_empty_defaults_to_one() {\n let file_name = \"test_step_recur_value_monthly_empty.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = String::new();\n db_conn\n .step()\n .recur_value(&mut s, RecurrenceFrequency::Monthly, StepType::StepUp)\n .unwrap();\n assert_eq!(s, \"1\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn step_recur_value_monthly_up_wraps_at_31() {\n let file_name = \"test_step_recur_value_monthly_wrap.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"31\".to_string();\n db_conn\n .step()\n .recur_value(&mut s, RecurrenceFrequency::Monthly, StepType::StepUp)\n .unwrap();\n assert_eq!(s, \"1\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn step_recur_value_monthly_down_wraps_at_one() {\n let file_name = \"test_step_recur_value_monthly_wrap_down.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"1\".to_string();\n db_conn\n .step()\n .recur_value(&mut s, RecurrenceFrequency::Monthly, StepType::StepDown)\n .unwrap();\n assert_eq!(s, \"31\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}", + "body_hash": "143db7bef1f01e4c955826444e726b589ffec37201ae1f76317e578a9404ca2d" + }, + "app/tests/stepper.rs|crate#free:24944d53db60": { + "kind": "Free", + "file": "app/tests/stepper.rs", + "item_path": "crate#free:24944d53db60", + "comment_kind": "Line", + "comment_text": "// ---- Amount stepping ----", + "comment_hash": "24944d53db60ec46c6bd0a9ae1e008b95ac1ac0c9a7ed96f2bb077858a3d898b", + "body_text": "#[test]\nfn step_amount_up() {\n let file_name = \"test_step_amount.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"5.00\".to_string();\n db_conn.step().amount(&mut s, StepType::StepUp).unwrap();\n assert_eq!(s, \"6.00\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn step_amount_down() {\n let file_name = \"test_step_amount_down.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"5.00\".to_string();\n db_conn.step().amount(&mut s, StepType::StepDown).unwrap();\n assert_eq!(s, \"4.00\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn step_amount_down_hits_floor() {\n let file_name = \"test_step_amount_floor.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"0.00\".to_string();\n db_conn.step().amount(&mut s, StepType::StepDown).unwrap();\n assert_eq!(s, \"0.00\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn step_amount_from_empty_defaults() {\n let file_name = \"test_step_amount_empty.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = String::new();\n db_conn.step().amount(&mut s, StepType::StepUp).unwrap();\n assert_eq!(s, \"0.00\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn step_amount_negative_restores_to_one() {\n let file_name = \"test_step_amount_negative.sqlite\";\n let mut db_conn = create_test_db(file_name);\n \n \n \n let mut s = \"0.00\".to_string();\n db_conn.step().amount(&mut s, StepType::StepUp).unwrap();\n assert_eq!(s, \"1.00\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}", + "body_hash": "a7e09ce75eee4811c22326e978c010acde3daf3ad15560c303107e4d5b8795e7" + }, + "app/tests/stepper.rs|crate#free:35750c7a8835": { + "kind": "Free", + "file": "app/tests/stepper.rs", + "item_path": "crate#free:35750c7a8835", + "comment_kind": "Line", + "comment_text": "// ---- Recurring: Recur Interval stepping ----", + "comment_hash": "35750c7a88359e68bf9099ad75482dbd41c64a5dea22e8d14e8f7a84b7092c16", + "body_text": "#[test]\nfn step_recur_interval_from_empty_defaults_to_one() {\n let file_name = \"test_step_recur_interval_empty.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = String::new();\n db_conn\n .step()\n .recur_interval(&mut s, StepType::StepUp)\n .unwrap();\n assert_eq!(s, \"1\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn step_recur_interval_up() {\n let file_name = \"test_step_recur_interval_up.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"5\".to_string();\n db_conn\n .step()\n .recur_interval(&mut s, StepType::StepUp)\n .unwrap();\n assert_eq!(s, \"6\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn step_recur_interval_down_floors_at_one() {\n let file_name = \"test_step_recur_interval_floor.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"1\".to_string();\n db_conn\n .step()\n .recur_interval(&mut s, StepType::StepDown)\n .unwrap();\n assert_eq!(s, \"1\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn step_recur_interval_up_caps_at_9999() {\n let file_name = \"test_step_recur_interval_cap.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"9999\".to_string();\n db_conn\n .step()\n .recur_interval(&mut s, StepType::StepUp)\n .unwrap();\n assert_eq!(s, \"9999\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}", + "body_hash": "8cf5cecf35a71648e6c5af6a9ccb500a56ca82224731c50d98a6d41ed580cad1" + }, + "app/tests/stepper.rs|crate#free:6437fe9269ef": { + "kind": "Free", + "file": "app/tests/stepper.rs", + "item_path": "crate#free:6437fe9269ef", + "comment_kind": "Line", + "comment_text": "// ---- Date stepping ----", + "comment_hash": "6437fe9269ef008bd238edce67e604c1c7ca7f4d545bbb14e64fa4092cbcd95d", + "body_text": "#[test]\nfn step_date_exact_up() {\n let file_name = \"test_step_date.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"2024-06-15\".to_string();\n db_conn\n .step()\n .date(&mut s, StepType::StepUp, DateType::Exact)\n .unwrap();\n assert_eq!(s, \"2024-06-16\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn step_date_exact_down() {\n let file_name = \"test_step_date_down.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"2024-06-15\".to_string();\n db_conn\n .step()\n .date(&mut s, StepType::StepDown, DateType::Exact)\n .unwrap();\n assert_eq!(s, \"2024-06-14\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn step_date_monthly_up() {\n let file_name = \"test_step_date_monthly.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"2024-06\".to_string();\n db_conn\n .step()\n .date(&mut s, StepType::StepUp, DateType::Monthly)\n .unwrap();\n assert_eq!(s, \"2024-07\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn step_date_monthly_down() {\n let file_name = \"test_step_date_monthly_down.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"2024-06\".to_string();\n db_conn\n .step()\n .date(&mut s, StepType::StepDown, DateType::Monthly)\n .unwrap();\n assert_eq!(s, \"2024-05\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn step_date_yearly_up() {\n let file_name = \"test_step_date_yearly.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"2024\".to_string();\n db_conn\n .step()\n .date(&mut s, StepType::StepUp, DateType::Yearly)\n .unwrap();\n assert_eq!(s, \"2025\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn step_date_yearly_down() {\n let file_name = \"test_step_date_yearly_down.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"2024\".to_string();\n db_conn\n .step()\n .date(&mut s, StepType::StepDown, DateType::Yearly)\n .unwrap();\n assert_eq!(s, \"2023\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn step_date_from_empty_defaults() {\n let file_name = \"test_step_date_empty.sqlite\";\n let mut db_conn = create_test_db(file_name);\n\n let today = Local::now().naive_local();\n\n let mut s = String::new();\n db_conn\n .step()\n .date(&mut s, StepType::StepUp, DateType::Exact)\n .unwrap();\n assert_eq!(s, today.format(\"%Y-%m-%d\").to_string());\n\n let mut s = String::new();\n db_conn\n .step()\n .date(&mut s, StepType::StepUp, DateType::Monthly)\n .unwrap();\n assert_eq!(s, today.format(\"%Y-%m\").to_string());\n\n let mut s = String::new();\n db_conn\n .step()\n .date(&mut s, StepType::StepUp, DateType::Yearly)\n .unwrap();\n assert_eq!(s, today.format(\"%Y\").to_string());\n\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn step_date_month_boundary_wraps_year() {\n let file_name = \"test_step_date_month_wrap.sqlite\";\n let mut db_conn = create_test_db(file_name);\n\n let mut s = \"2024-12\".to_string();\n db_conn\n .step()\n .date(&mut s, StepType::StepUp, DateType::Monthly)\n .unwrap();\n assert_eq!(s, \"2025-01\");\n\n let mut s = \"2024-01\".to_string();\n db_conn\n .step()\n .date(&mut s, StepType::StepDown, DateType::Monthly)\n .unwrap();\n assert_eq!(s, \"2023-12\");\n\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}", + "body_hash": "2b0fee8aad726c027460c792e70fe703a4ada96a39687791e20b956efc28b585" + }, + "app/tests/stepper.rs|crate#free:77e10879cd09": { + "kind": "Free", + "file": "app/tests/stepper.rs", + "item_path": "crate#free:77e10879cd09", + "comment_kind": "Line", + "comment_text": "// ---- Recurring: Recur Month stepping ----", + "comment_hash": "77e10879cd0946f33378592137bfb36d4b591ce6383cee976b5d807322b75238", + "body_text": "#[test]\nfn step_recur_month_from_empty_defaults_to_january() {\n let file_name = \"test_step_recur_month_empty.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = String::new();\n db_conn\n .step()\n .recur_month(&mut s, StepType::StepUp)\n .unwrap();\n assert_eq!(s, \"January\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn step_recur_month_down_wraps_to_december() {\n let file_name = \"test_step_recur_month_wrap.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"January\".to_string();\n db_conn\n .step()\n .recur_month(&mut s, StepType::StepDown)\n .unwrap();\n assert_eq!(s, \"December\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn step_recur_month_cycles_all() {\n let file_name = \"test_step_recur_month_cycle.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"November\".to_string();\n db_conn\n .step()\n .recur_month(&mut s, StepType::StepUp)\n .unwrap();\n assert_eq!(s, \"December\");\n db_conn\n .step()\n .recur_month(&mut s, StepType::StepUp)\n .unwrap();\n assert_eq!(s, \"January\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}", + "body_hash": "9a676033a95d3dd7a7b84f4505dcb9349dadbb4e38413a3921185e748f796c1a" + }, + "app/tests/stepper.rs|crate#free:b29110e384f4": { + "kind": "Free", + "file": "app/tests/stepper.rs", + "item_path": "crate#free:b29110e384f4", + "comment_kind": "Line", + "comment_text": "// ---- Tx Type stepping ----", + "comment_hash": "b29110e384f4d5bcc680616a3be7319aeb1d7bdbb2591d4a4c9aed1697a4f0af", + "body_text": "#[test]\nfn step_tx_type_up() {\n let file_name = \"test_step_type.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"Income\".to_string();\n db_conn.step().tx_type(&mut s, StepType::StepUp).unwrap();\n assert_eq!(s, \"Expense\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn step_tx_type_down_wraps() {\n let file_name = \"test_step_type_wrap.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"Income\".to_string();\n db_conn.step().tx_type(&mut s, StepType::StepDown).unwrap();\n \n \n assert_eq!(s, \"Lend Repay\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn step_tx_type_cycles_all() {\n let file_name = \"test_step_type_cycle.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let expected = [\n \"Income\",\n \"Expense\",\n \"Transfer\",\n \"Borrow\",\n \"Lend\",\n \"Borrow Repay\",\n \"Lend Repay\",\n ];\n\n let mut s = \"Income\".to_string();\n (1..expected.len()).for_each(|i| {\n db_conn.step().tx_type(&mut s, StepType::StepUp).unwrap();\n assert_eq!(s, expected[i]);\n });\n\n \n db_conn.step().tx_type(&mut s, StepType::StepUp).unwrap();\n assert_eq!(s, \"Income\");\n\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn step_tx_type_from_empty() {\n let file_name = \"test_step_type_empty.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = String::new();\n db_conn.step().tx_type(&mut s, StepType::StepUp).unwrap();\n assert_eq!(s, \"Income\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}", + "body_hash": "2fa7a0a6f8c8a9d74707766429ba47e7b1bb1edb78ebf50fa5ba58dd03c314b5" + }, + "app/tests/stepper.rs|crate#free:c0cb5ba8f8ec": { + "kind": "Free", + "file": "app/tests/stepper.rs", + "item_path": "crate#free:c0cb5ba8f8ec", + "comment_kind": "Line", + "comment_text": "// ---- Tx Method stepping ----", + "comment_hash": "c0cb5ba8f8ec4e6dfd053245427e25c5edb3cc966afb82713dc94a121564f941", + "body_text": "#[test]\nfn step_tx_method_up() {\n let file_name = \"test_step_method.sqlite\";\n let mut db_conn = create_test_db(file_name);\n \n let mut s = \"Cash\".to_string();\n db_conn.step().tx_method(&mut s, StepType::StepUp).unwrap();\n assert_eq!(s, \"Bank\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn step_tx_method_wraps_at_end() {\n let file_name = \"test_step_method_wrap.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"Other\".to_string();\n db_conn.step().tx_method(&mut s, StepType::StepUp).unwrap();\n assert_eq!(s, \"Cash\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn step_tx_method_down_wraps_at_start() {\n let file_name = \"test_step_method_wrap_down.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"Cash\".to_string();\n db_conn\n .step()\n .tx_method(&mut s, StepType::StepDown)\n .unwrap();\n assert_eq!(s, \"Other\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn step_tx_method_from_empty() {\n let file_name = \"test_step_method_empty.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = String::new();\n db_conn.step().tx_method(&mut s, StepType::StepUp).unwrap();\n assert_eq!(s, \"Cash\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}", + "body_hash": "13b4ffda348ab473f0428435f5712c61a56e58c0a3af4d04911b3e70d5670d8e" + }, + "app/tests/stepper.rs|crate#free:d91df176ef37": { + "kind": "Free", + "file": "app/tests/stepper.rs", + "item_path": "crate#free:d91df176ef37", + "comment_kind": "Line", + "comment_text": "// ---- Recurring: Frequency stepping ----", + "comment_hash": "d91df176ef379a0178bb465a18bff0b22e14b2985300c75a747509284bca064f", + "body_text": "#[test]\nfn step_frequency_from_empty_defaults_to_daily() {\n let file_name = \"test_step_frequency_empty.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = String::new();\n db_conn.step().frequency(&mut s, StepType::StepUp).unwrap();\n assert_eq!(s, \"Daily\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn step_frequency_cycles_all() {\n let file_name = \"test_step_frequency_cycle.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"Daily\".to_string();\n\n for expected in [\"Weekly\", \"Monthly\", \"Yearly\", \"Daily\"] {\n db_conn.step().frequency(&mut s, StepType::StepUp).unwrap();\n assert_eq!(s, expected);\n }\n\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn step_frequency_down_wraps() {\n let file_name = \"test_step_frequency_wrap.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"Daily\".to_string();\n db_conn\n .step()\n .frequency(&mut s, StepType::StepDown)\n .unwrap();\n assert_eq!(s, \"Yearly\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}", + "body_hash": "b30fcbf2f155b011b84e1215d7b7862d8cdcbe3d44971f23a240b459e057e1e7" + }, + "app/tests/stepper.rs|crate#free:e1777bf35412": { + "kind": "Free", + "file": "app/tests/stepper.rs", + "item_path": "crate#free:e1777bf35412", + "comment_kind": "Line", + "comment_text": "// ---- Stepper error paths ----", + "comment_hash": "e1777bf3541257f6dac277fbbf0a4360439f6b29c7d8568ea4469b75ba7b3a41", + "body_text": "#[test]\nfn step_date_invalid_format_errors() {\n let file_name = \"test_step_date_err.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"not-a-date\".to_string();\n let result = db_conn\n .step()\n .date(&mut s, StepType::StepUp, DateType::Exact);\n assert!(result.is_err());\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn step_amount_non_numeric_errors() {\n let file_name = \"test_step_amount_err.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"abc\".to_string();\n let result = db_conn.step().amount(&mut s, StepType::StepUp);\n assert!(matches!(result, Err(SteppingError::InvalidAmount)));\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn step_tx_method_unknown_errors() {\n let file_name = \"test_step_method_err.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"NonExistentMethod\".to_string();\n let result = db_conn.step().tx_method(&mut s, StepType::StepUp);\n assert!(matches!(result, Err(SteppingError::InvalidTxMethod)));\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn step_tx_type_unknown_errors() {\n let file_name = \"test_step_type_err.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"UnknownType\".to_string();\n let result = db_conn.step().tx_type(&mut s, StepType::StepUp);\n assert!(matches!(result, Err(SteppingError::InvalidTxType)));\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn step_tag_empty_db_errors() {\n let file_name = \"test_step_tag_err.sqlite\";\n let mut db_conn = create_test_db(file_name);\n \n \n let mut s = \"NonExistent\".to_string();\n let result = db_conn.step().tag(&mut s, StepType::StepUp);\n \n assert!(result.is_err());\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}", + "body_hash": "637e160ee439c0448f8d72972cdc7e55e51903aa9022edd60e3902b9fde86fc5" + }, + "app/tests/stepper.rs|crate#free:f6cb36fcb286": { + "kind": "Free", + "file": "app/tests/stepper.rs", + "item_path": "crate#free:f6cb36fcb286", + "comment_kind": "Line", + "comment_text": "// ---- Tag stepping ----", + "comment_hash": "f6cb36fcb2866b1d1000ec3d7062fdb3b6b7c95a60ff38d94610ca29516d1c1d", + "body_text": "#[test]\nfn step_tag_up() {\n let file_name = \"test_step_tag.sqlite\";\n let mut db_conn = create_test_db(file_name);\n\n \n add_tx(\n &mut db_conn,\n \"2024-01-01\",\n \"T1\",\n \"Cash\",\n \"\",\n \"10.00\",\n \"Expense\",\n \"Alpha\",\n );\n add_tx(\n &mut db_conn,\n \"2024-01-01\",\n \"T2\",\n \"Cash\",\n \"\",\n \"20.00\",\n \"Expense\",\n \"Beta\",\n );\n add_tx(\n &mut db_conn,\n \"2024-01-01\",\n \"T3\",\n \"Cash\",\n \"\",\n \"30.00\",\n \"Expense\",\n \"Gamma\",\n );\n\n let mut s = \"Alpha\".to_string();\n db_conn.step().tag(&mut s, StepType::StepUp).unwrap();\n assert_eq!(s, \"Beta\");\n\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn step_tag_wraps_at_end() {\n let file_name = \"test_step_tag_wrap.sqlite\";\n let mut db_conn = create_test_db(file_name);\n\n add_tx(\n &mut db_conn,\n \"2024-02-01\",\n \"T1\",\n \"Cash\",\n \"\",\n \"10.00\",\n \"Expense\",\n \"Alpha\",\n );\n add_tx(\n &mut db_conn,\n \"2024-02-01\",\n \"T2\",\n \"Cash\",\n \"\",\n \"20.00\",\n \"Expense\",\n \"Zulu\",\n );\n\n let mut s = \"Zulu\".to_string();\n db_conn.step().tag(&mut s, StepType::StepUp).unwrap();\n assert_eq!(s, \"Alpha\");\n\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn step_tag_down_wraps() {\n let file_name = \"test_step_tag_wrap_down.sqlite\";\n let mut db_conn = create_test_db(file_name);\n\n add_tx(\n &mut db_conn,\n \"2024-03-01\",\n \"T1\",\n \"Cash\",\n \"\",\n \"10.00\",\n \"Expense\",\n \"Alpha\",\n );\n add_tx(\n &mut db_conn,\n \"2024-03-01\",\n \"T2\",\n \"Cash\",\n \"\",\n \"20.00\",\n \"Expense\",\n \"Beta\",\n );\n\n \n \n let mut s = \"Alpha\".to_string();\n db_conn.step().tag(&mut s, StepType::StepDown).unwrap();\n assert_eq!(s, \"Unknown\");\n\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn step_tag_from_empty() {\n let file_name = \"test_step_tag_empty.sqlite\";\n let mut db_conn = create_test_db(file_name);\n\n add_tx(\n &mut db_conn,\n \"2024-04-01\",\n \"T1\",\n \"Cash\",\n \"\",\n \"10.00\",\n \"Expense\",\n \"Alpha\",\n );\n\n let mut s = String::new();\n db_conn.step().tag(&mut s, StepType::StepUp).unwrap();\n assert_eq!(s, \"Alpha\");\n\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn step_tag_fuzzy_corrects_then_steps() {\n let file_name = \"test_step_tag_fuzzy.sqlite\";\n let mut db_conn = create_test_db(file_name);\n\n add_tx(\n &mut db_conn,\n \"2024-05-01\",\n \"T1\",\n \"Cash\",\n \"\",\n \"10.00\",\n \"Expense\",\n \"Groceries\",\n );\n add_tx(\n &mut db_conn,\n \"2024-05-01\",\n \"T2\",\n \"Cash\",\n \"\",\n \"20.00\",\n \"Expense\",\n \"Rent\",\n );\n\n \n let mut s = \"Groce\".to_string();\n let result = db_conn.step().tag(&mut s, StepType::StepUp);\n assert!(result.is_err()); \n assert_eq!(s, \"Groceries\");\n\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn step_tag_multiple_preserves_other_tags() {\n let file_name = \"test_step_tag_multi.sqlite\";\n let mut db_conn = create_test_db(file_name);\n\n add_tx(\n &mut db_conn,\n \"2024-06-01\",\n \"T1\",\n \"Cash\",\n \"\",\n \"10.00\",\n \"Expense\",\n \"Alpha\",\n );\n add_tx(\n &mut db_conn,\n \"2024-06-01\",\n \"T2\",\n \"Cash\",\n \"\",\n \"20.00\",\n \"Expense\",\n \"Beta\",\n );\n add_tx(\n &mut db_conn,\n \"2024-06-01\",\n \"T3\",\n \"Cash\",\n \"\",\n \"30.00\",\n \"Expense\",\n \"Gamma\",\n );\n\n \n let mut s = \"Alpha, Beta\".to_string();\n db_conn.step().tag(&mut s, StepType::StepUp).unwrap();\n assert_eq!(s, \"Alpha, Gamma\");\n\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}", + "body_hash": "a73634dd5afaa7db231751ff11c90b2470ad313020873e18eb709ae2efc3ee2d" + }, + "app/tests/stepper.rs|crate::step_amount_negative_restores_to_one#free:d8b0a41b7858": { + "kind": "Free", + "file": "app/tests/stepper.rs", + "item_path": "crate::step_amount_negative_restores_to_one#free:d8b0a41b7858", + "comment_kind": "Line", + "comment_text": "// Verification rejects a zero or negative amount with AmountBelowZero, and\n// stepping up from that state falls back to 1.00. \"0.00\" is the input that\n// reaches the branch", + "comment_hash": "d8b0a41b78586d1b8dc1428d3df4b2868b2b62a84f6a46b4b9ab5083379eec19", + "body_text": "let mut s = \"0.00\".to_string();\ndb_conn.step().amount(&mut s, StepType::StepUp).unwrap();\nassert_eq!(s, \"1.00\");\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "57e30242facbba29b86c35b28a3142ac09bab5669fff356600a5f157f7c8a025" + }, + "app/tests/stepper.rs|crate::step_tag_down_wraps#free:514b5691cf4b": { + "kind": "Free", + "file": "app/tests/stepper.rs", + "item_path": "crate::step_tag_down_wraps#free:514b5691cf4b", + "comment_kind": "Line", + "comment_text": "// Tags include pre-seeded \"Unknown\": [\"Alpha\", \"Beta\", \"Unknown\"]\n// Step down from Alpha (index 0) wraps to last", + "comment_hash": "514b5691cf4bf72d36dd6a5371c2ac826a81d61d78540434a6a93bb72b8889cb", + "body_text": "let mut s = \"Alpha\".to_string();\ndb_conn.step().tag(&mut s, StepType::StepDown).unwrap();\nassert_eq!(s, \"Unknown\");\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "11873e50d71fe1be35333a71a5069581d43cf99e0b9619e45c887a8463db38ae" + }, + "app/tests/stepper.rs|crate::step_tag_empty_db_errors#free:5ce9df1c8680": { + "kind": "Free", + "file": "app/tests/stepper.rs", + "item_path": "crate::step_tag_empty_db_errors#free:5ce9df1c8680", + "comment_kind": "Line", + "comment_text": "// Fuzzy-corrects to best match and errors", + "comment_hash": "5ce9df1c868091379d700d59c06cb53807ef6c696ff4c99807b149af082bda15", + "body_text": "assert!(result.is_err());\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "fc9c2ad657c0807d39acde9aab7ac6f0c305aa1d25d1050d6ead5b785fb8fa8e" + }, + "app/tests/stepper.rs|crate::step_tag_empty_db_errors#free:ad73c0bfaba9": { + "kind": "Free", + "file": "app/tests/stepper.rs", + "item_path": "crate::step_tag_empty_db_errors#free:ad73c0bfaba9", + "comment_kind": "Line", + "comment_text": "// No user tags were added, so \"Unknown\" (always seeded by the migrations) is\n// all there is to fuzzy correct towards", + "comment_hash": "ad73c0bfaba9bf73af224d1d28dba4176e6cd3a88fcce5e9e67cc6ecd2385afa", + "body_text": "let mut s = \"NonExistent\".to_string();\nlet result = db_conn.step().tag(&mut s, StepType::StepUp);", + "body_hash": "0a652ebd1265e19b275cbec1818b5a59f501649373c9fe9ac0c474aae8be590e" + }, + "app/tests/stepper.rs|crate::step_tag_fuzzy_corrects_then_steps#free:0a1c3ed8361b": { + "kind": "Free", + "file": "app/tests/stepper.rs", + "item_path": "crate::step_tag_fuzzy_corrects_then_steps#free:0a1c3ed8361b", + "comment_kind": "Line", + "comment_text": "// Invalid tag gets fuzzy-corrected, then stepped", + "comment_hash": "0a1c3ed8361bd4643a1be4522489a9dc0006ba3a5889fd552bb8df059283a0e3", + "body_text": "let mut s = \"Groce\".to_string();\nlet result = db_conn.step().tag(&mut s, StepType::StepUp);\nassert!(result.is_err());", + "body_hash": "9a16f849e97c87ec7211459fcf839f128e61b942a370d43a3b79b119d0ba9f79" + }, + "app/tests/stepper.rs|crate::step_tag_fuzzy_corrects_then_steps#trailing:d7cb11dcb7a6": { + "kind": "Free", + "file": "app/tests/stepper.rs", + "item_path": "crate::step_tag_fuzzy_corrects_then_steps#trailing:d7cb11dcb7a6", + "comment_kind": "Line", + "comment_text": "// Invalid tag error", + "comment_hash": "d7cb11dcb7a6368593ac839295321de22af824f523f78ea4cba7db95b307dc10", + "body_text": "assert!(result.is_err());", + "body_hash": "07de3a44e6597391ef7daa99ea9e3f97300878d97459bfb21427c93d00ad914d" + }, + "app/tests/stepper.rs|crate::step_tag_multiple_preserves_other_tags#free:db70bf325b44": { + "kind": "Free", + "file": "app/tests/stepper.rs", + "item_path": "crate::step_tag_multiple_preserves_other_tags#free:db70bf325b44", + "comment_kind": "Line", + "comment_text": "// Only the last tag in a comma-separated list gets stepped", + "comment_hash": "db70bf325b44c3ecb06eb9798ea25302471494c9e3f9ea51994f5c48c65f61b4", + "body_text": "let mut s = \"Alpha, Beta\".to_string();\ndb_conn.step().tag(&mut s, StepType::StepUp).unwrap();\nassert_eq!(s, \"Alpha, Gamma\");\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "59b45bd854574dd0f7d0862027455ccc3858d311b8209944d7b8012e25b369fb" + }, + "app/tests/stepper.rs|crate::step_tag_up#free:b1ed384b9e53": { + "kind": "Free", + "file": "app/tests/stepper.rs", + "item_path": "crate::step_tag_up#free:b1ed384b9e53", + "comment_kind": "Line", + "comment_text": "// Add tags to the DB", + "comment_hash": "b1ed384b9e5352b75668ce2ca99ae1f98abb8d52038c2a47e7048bc7d54ea784", + "body_text": "add_tx(\n &mut db_conn,\n \"2024-01-01\",\n \"T1\",\n \"Cash\",\n \"\",\n \"10.00\",\n \"Expense\",\n \"Alpha\",\n );\nadd_tx(\n &mut db_conn,\n \"2024-01-01\",\n \"T2\",\n \"Cash\",\n \"\",\n \"20.00\",\n \"Expense\",\n \"Beta\",\n );\nadd_tx(\n &mut db_conn,\n \"2024-01-01\",\n \"T3\",\n \"Cash\",\n \"\",\n \"30.00\",\n \"Expense\",\n \"Gamma\",\n );\nlet mut s = \"Alpha\".to_string();\ndb_conn.step().tag(&mut s, StepType::StepUp).unwrap();\nassert_eq!(s, \"Beta\");\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "e03ea12c86ce65114852007c6e3269f643b8c552b64a07209605b48eebb8cc66" + }, + "app/tests/stepper.rs|crate::step_tx_method_up#free:bcd0fa8895be": { + "kind": "Free", + "file": "app/tests/stepper.rs", + "item_path": "crate::step_tx_method_up#free:bcd0fa8895be", + "comment_kind": "Line", + "comment_text": "// Methods: Cash(1), Bank(2), Other(3)", + "comment_hash": "bcd0fa8895be8c37ce93c7bd7810a144fe22a40c049ac59e1abe306ac28bbbf1", + "body_text": "let mut s = \"Cash\".to_string();\ndb_conn.step().tx_method(&mut s, StepType::StepUp).unwrap();\nassert_eq!(s, \"Bank\");\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "6ec9d0aabc3eab5a7d39d38fc2fc95c4858f0906c4c0ebf6a6eca357dfdf0691" + }, + "app/tests/stepper.rs|crate::step_tx_type_cycles_all#free:9d2e89c9e600": { + "kind": "Free", + "file": "app/tests/stepper.rs", + "item_path": "crate::step_tx_type_cycles_all#free:9d2e89c9e600", + "comment_kind": "Line", + "comment_text": "// Wrap back to first", + "comment_hash": "9d2e89c9e600c6fb66125cf42fe39356a5ab9343a3951c755cee8f54c9b96bd8", + "body_text": "db_conn.step().tx_type(&mut s, StepType::StepUp).unwrap();\nassert_eq!(s, \"Income\");\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "94080f27a7f803f92c66e9ec3c337de828bcf2ef3a683283a0ff8420c339c089" + }, + "app/tests/stepper.rs|crate::step_tx_type_down_wraps#free:ad612204eb35": { + "kind": "Free", + "file": "app/tests/stepper.rs", + "item_path": "crate::step_tx_type_down_wraps#free:ad612204eb35", + "comment_kind": "Line", + "comment_text": "// TxType order: Income, Expense, Transfer, Borrow, Lend, BorrowRepay, LendRepay\n// Wrapping down from first goes to last", + "comment_hash": "ad612204eb352973740c1098ea4344f6de4f4338ac3a7a9ae1aacdec8c352824", + "body_text": "assert_eq!(s, \"Lend Repay\");\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "893da992ac627a731385117535408f525ce41439fa73b88119edb6c005ec9ae3" + }, + "app/tests/summary.rs|crate::summary_all_fetch_nature#trailing:40363f1ada96": { + "kind": "Free", + "file": "app/tests/summary.rs", + "item_path": "crate::summary_all_fetch_nature#trailing:40363f1ada96", + "comment_kind": "Line", + "comment_text": "// total expense", + "comment_hash": "40363f1ada96b1f54ed43d1a8771d8b4deba87c5a9d66da6997ce24bb1e04726", + "body_text": "assert_eq!(net[0][2], \"200.00\");", + "body_hash": "9d83a43698931a7733c69c25a520cbe1c84b39526e6cfb1b2b02dd92e034cab6" + }, + "app/tests/summary.rs|crate::summary_all_fetch_nature#trailing:c148bcd6f3da": { + "kind": "Free", + "file": "app/tests/summary.rs", + "item_path": "crate::summary_all_fetch_nature#trailing:c148bcd6f3da", + "comment_kind": "Line", + "comment_text": "// total income", + "comment_hash": "c148bcd6f3daff54e55ed2486edd3567beea4349d23ad1c47e18f22f7296d173", + "body_text": "assert_eq!(net[0][1], \"1000.00\");", + "body_hash": "801200eb50be193ef694ad2c62a68fa3b398efe262bd011b92138002835feebb" + }, + "app/tests/summary.rs|crate::summary_largest_single_tx_each#trailing:38bcb061872b": { + "kind": "Free", + "file": "app/tests/summary.rs", + "item_path": "crate::summary_largest_single_tx_each#trailing:38bcb061872b", + "comment_kind": "Line", + "comment_text": "// largest expense", + "comment_hash": "38bcb061872b5aeae75130a380fc51a1547b30561ad756149d54b6bc4c7cf072", + "body_text": "assert_eq!(largest[1][2], \"2000.00\");", + "body_hash": "b6fe9ac960322393497bda125e820ea9eb29a54f121db1c4229e4c2e14acf022" + }, + "app/tests/summary.rs|crate::summary_largest_single_tx_each#trailing:94d5234310d5": { + "kind": "Free", + "file": "app/tests/summary.rs", + "item_path": "crate::summary_largest_single_tx_each#trailing:94d5234310d5", + "comment_kind": "Line", + "comment_text": "// largest earning", + "comment_hash": "94d5234310d50c90baf10269a76a24e1f2074c62c5800c1f448e39a3ee119946", + "body_text": "assert_eq!(largest[0][2], \"5000.00\");", + "body_hash": "7975250b6d8f7be52ed2965c394317d9f1987f24814758c6835cfbecd904284a" + }, + "app/tests/summary.rs|crate::summary_lend_borrows#free:a819f0280961": { + "kind": "Free", + "file": "app/tests/summary.rs", + "item_path": "crate::summary_lend_borrows#free:a819f0280961", + "comment_kind": "Line", + "comment_text": "// Net borrows: 500 - 250 = 250\n// Net lends: 200 - 100 = 100", + "comment_hash": "a819f0280961123ea7f2818bb73a3b73b4c842737886c3db4c46a9132197e5a1", + "body_text": "let lb = full.lend_borrows_array();\nassert_eq!(lb[0][0], \"250.00\");\nassert_eq!(lb[0][1], \"100.00\");\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "a9141164ac36e50828706854ea0de5f9607867512925687a309b2b2d9bb96181" + }, + "app/tests/summary.rs|crate::summary_lend_borrows_with_mom#free:04f7629c984d": { + "kind": "Free", + "file": "app/tests/summary.rs", + "item_path": "crate::summary_lend_borrows_with_mom#free:04f7629c984d", + "comment_kind": "Line", + "comment_text": "// August: borrow 500", + "comment_hash": "04f7629c984df0efe4e24136ca4c40f043b9ab5c5c21b87de74cc9abe9d6f370", + "body_text": "add_tx(\n &mut db_conn,\n \"2024-08-01\",\n \"Borrow\",\n \"Cash\",\n \"\",\n \"500.00\",\n \"Borrow\",\n \"Loan\",\n );\nlet aug_view = db_conn\n .get_summary_with_str(\"August\", \"2024\", FetchNature::Monthly)\n .unwrap();\nlet aug_full = aug_view.generate_summary(None, &db_conn);", + "body_hash": "6e07f8b311ca6c842967bd7815eb8bbb1bf2149eb21ca5d1335114894a012f94" + }, + "app/tests/summary.rs|crate::summary_lend_borrows_with_mom#free:5daab6aa60a6": { + "kind": "Free", + "file": "app/tests/summary.rs", + "item_path": "crate::summary_lend_borrows_with_mom#free:5daab6aa60a6", + "comment_kind": "Line", + "comment_text": "// Lend borrows should have MoM columns", + "comment_hash": "5daab6aa60a6fef96162defbeca068e449805c50a8dce188e7695d186d1ed858", + "body_text": "let lb = sep_full.lend_borrows_array();\nassert!(\n lb[0].len() >= 2,\n \"Lend borrows array should have value columns\"\n );\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "eb87dc71fd29836459ccad7edfce989f11af54d723bf99cfd05cf80a8948e44e" + }, + "app/tests/summary.rs|crate::summary_lend_borrows_with_mom#free:986f70b1f5a7": { + "kind": "Free", + "file": "app/tests/summary.rs", + "item_path": "crate::summary_lend_borrows_with_mom#free:986f70b1f5a7", + "comment_kind": "Line", + "comment_text": "// September: borrow more, repay some", + "comment_hash": "986f70b1f5a76bf72c883cfda7f4569c20f769dc2690d3a45b7a5740c869f2fd", + "body_text": "add_tx(\n &mut db_conn,\n \"2024-09-01\",\n \"Borrow more\",\n \"Cash\",\n \"\",\n \"300.00\",\n \"Borrow\",\n \"Loan\",\n );\nadd_tx(\n &mut db_conn,\n \"2024-09-15\",\n \"Repay\",\n \"Cash\",\n \"\",\n \"200.00\",\n \"Borrow Repay\",\n \"Loan\",\n );\nlet sep_view = db_conn\n .get_summary_with_str(\"September\", \"2024\", FetchNature::Monthly)\n .unwrap();\nlet sep_full = sep_view.generate_summary(Some(&aug_full), &db_conn);", + "body_hash": "e45d868049d8d6e84936765e11d1deb7161c063f17a74d2b3558d3092e5a767f" + }, + "app/tests/summary.rs|crate::summary_monthly_basic_income_expense#free:78d07547cf0b": { + "kind": "Free", + "file": "app/tests/summary.rs", + "item_path": "crate::summary_monthly_basic_income_expense#free:78d07547cf0b", + "comment_kind": "Line", + "comment_text": "// Peak earning = 3000 (only one month with income)", + "comment_hash": "78d07547cf0be8a355506bfb5abffbd24139b6583fc30cd27d540c760c73a8bf", + "body_text": "let peak = full.peak_array();\nassert_eq!(peak[0][0], \"Peak Earning\");\nassert_eq!(peak[0][2], \"3000.00\");\nassert_eq!(peak[1][0], \"Peak Expense\");\nassert_eq!(peak[1][2], \"1500.00\");\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "4c10e2334e16dd3c3a8ce4c874dd2df1f9d1ec5a884f39cd293e4e57a60ec8e0" + }, + "app/tests/summary.rs|crate::summary_monthly_basic_income_expense#free:91ef2b725c8c": { + "kind": "Free", + "file": "app/tests/summary.rs", + "item_path": "crate::summary_monthly_basic_income_expense#free:91ef2b725c8c", + "comment_kind": "Line", + "comment_text": "// Net: income=3000, expense=1500", + "comment_hash": "91ef2b725c8c70733daf42b5e4d4f3cd918f28b67e91efb024d3351d70bb04f8", + "body_text": "let net = full.net_array();\nassert_eq!(net[0][1], \"3000.00\");\nassert_eq!(net[0][2], \"1500.00\");", + "body_hash": "33c1219e2db83f4de2f5c0ef48b8b23199c9d0ba0e97ebd5c416889065c42059" + }, + "app/tests/summary.rs|crate::summary_monthly_basic_income_expense#free:9ce10281c92c": { + "kind": "Free", + "file": "app/tests/summary.rs", + "item_path": "crate::summary_monthly_basic_income_expense#free:9ce10281c92c", + "comment_kind": "Line", + "comment_text": "// Per-method breakdown: only Cash has data", + "comment_hash": "9ce10281c92cc49f48ea4795a0d0ab1fba73ed304fe002f82df63c5717c29a9a", + "body_text": "let methods = full.method_array();\nlet cash_row = methods.iter().find(|r| r[0] == \"Cash\").unwrap();\nassert_eq!(cash_row[1], \"3000.00\");", + "body_hash": "2651e7c1910ab6c0dc3a503eb0e2bd668abb3e2f72c67f94a213e57478e02e7c" + }, + "app/tests/summary.rs|crate::summary_monthly_basic_income_expense#trailing:a67068c2574a": { + "kind": "Free", + "file": "app/tests/summary.rs", + "item_path": "crate::summary_monthly_basic_income_expense#trailing:a67068c2574a", + "comment_kind": "Line", + "comment_text": "// earning", + "comment_hash": "a67068c2574a5f02d29127b44a2d8e467594fa57735579efe10941f4482fc2ba", + "body_text": "assert_eq!(cash_row[1], \"3000.00\");", + "body_hash": "b859f4dedf482f5e7df885c1e7f4274abe747e8df483e36fc5bd98568307e3a2" + }, + "app/tests/summary.rs|crate::summary_monthly_basic_income_expense#trailing:cdd1ca078df5": { + "kind": "Free", + "file": "app/tests/summary.rs", + "item_path": "crate::summary_monthly_basic_income_expense#trailing:cdd1ca078df5", + "comment_kind": "Line", + "comment_text": "// expense\n// Largest earning: Salary 3000 on 01-06-2024", + "comment_hash": "cdd1ca078df5277616f7c8cdf6a92e695cb01660b1343bfeaa2484ae412b9f68", + "body_text": "assert_eq!(cash_row[2], \"1500.00\");", + "body_hash": "ec9d03c1a9d8241d9a1e43b5c70d3c2c72399aaf6cd1012b227a28e9539a2e6f" + }, + "app/tests/summary.rs|crate::summary_multiple_methods#trailing:2f17c6490ad5": { + "kind": "Free", + "file": "app/tests/summary.rs", + "item_path": "crate::summary_multiple_methods#trailing:2f17c6490ad5", + "comment_kind": "Line", + "comment_text": "// Cash earning", + "comment_hash": "2f17c6490ad5eb8e9b95c794c243a32b0cb040c59255c562c1bdbe56e9b3a73e", + "body_text": "assert_eq!(cash_row[1], \"1000.00\");", + "body_hash": "5adc394d29010ceb38c2da0e35763f0e1f3071a305f92adf704577ed6c0f84e1" + }, + "app/tests/summary.rs|crate::summary_multiple_methods#trailing:455eacf612bf": { + "kind": "Free", + "file": "app/tests/summary.rs", + "item_path": "crate::summary_multiple_methods#trailing:455eacf612bf", + "comment_kind": "Line", + "comment_text": "// Cash expense", + "comment_hash": "455eacf612bfafcad6eb1676283463d767855e07c6dca92d6329b3b6bcbf3080", + "body_text": "assert_eq!(cash_row[2], \"50.00\");", + "body_hash": "10cf31581134a0d86914bc582304abba8368e6e2e81d7039d1a8c154266cfd8e" + }, + "app/tests/summary.rs|crate::summary_multiple_methods#trailing:51e12949261a": { + "kind": "Free", + "file": "app/tests/summary.rs", + "item_path": "crate::summary_multiple_methods#trailing:51e12949261a", + "comment_kind": "Line", + "comment_text": "// Bank earning", + "comment_hash": "51e12949261a46dbf720aeb73fa364259d2afb98b05c0f33154ecdc8729148c6", + "body_text": "assert_eq!(bank_row[1], \"500.00\");", + "body_hash": "a5e37478be6f7a8e62300c4c050a763cbeadf56028d6c06e769116f8f3c4ebfd" + }, + "app/tests/summary.rs|crate::summary_multiple_methods#trailing:57d8b52aadb3": { + "kind": "Free", + "file": "app/tests/summary.rs", + "item_path": "crate::summary_multiple_methods#trailing:57d8b52aadb3", + "comment_kind": "Line", + "comment_text": "// Bank expense", + "comment_hash": "57d8b52aadb348a13e2cff898d77b6ce6dfc3cf32e4455c11664210adf8ae48a", + "body_text": "assert_eq!(bank_row[2], \"0.00\");", + "body_hash": "be12aae0b0ccba4b76ea53d645b60308f32d7cd1e0c7d83b1d1827a15306f0ca" + }, + "app/tests/summary.rs|crate::summary_multiple_methods#trailing:ce75c755fe01": { + "kind": "Free", + "file": "app/tests/summary.rs", + "item_path": "crate::summary_multiple_methods#trailing:ce75c755fe01", + "comment_kind": "Line", + "comment_text": "// Other: no activity", + "comment_hash": "ce75c755fe018d20d88408dc68b8768c47a2494561c224e60e00eb19afbfc3ce", + "body_text": "assert_eq!(other_row[1], \"0.00\");", + "body_hash": "f69908b36c7423e70d52c33c58a76a6727d9aaca226d99893da122f30b2c1147" + }, + "app/tests/summary.rs|crate::summary_peak_across_months#free:a75d38b6cece": { + "kind": "Free", + "file": "app/tests/summary.rs", + "item_path": "crate::summary_peak_across_months#free:a75d38b6cece", + "comment_kind": "Line", + "comment_text": "// Month 2: high earning", + "comment_hash": "a75d38b6cece7c8c34e3ac37af1d991ced62bc1c3f4995aacbed35366c1e6e53", + "body_text": "add_tx(\n &mut db_conn,\n \"2024-02-15\",\n \"Feb\",\n \"Cash\",\n \"\",\n \"5000.00\",\n \"Income\",\n \"B\",\n );", + "body_hash": "e9dd6027059c5d9146a3efe07d51694d71ca095b9a107fd64056f806bd4b2fac" + }, + "app/tests/summary.rs|crate::summary_peak_across_months#free:bf6c5a515c37": { + "kind": "Free", + "file": "app/tests/summary.rs", + "item_path": "crate::summary_peak_across_months#free:bf6c5a515c37", + "comment_kind": "Line", + "comment_text": "// Month 3: medium earning", + "comment_hash": "bf6c5a515c37a713c4341a5ed3f3d2c875c0b907e3cc284f0915cec96a853ad2", + "body_text": "add_tx(\n &mut db_conn,\n \"2024-03-15\",\n \"Mar\",\n \"Cash\",\n \"\",\n \"1000.00\",\n \"Income\",\n \"C\",\n );\nlet summary_view = db_conn\n .get_summary_with_str(\"January\", \"2024\", FetchNature::Yearly)\n .unwrap();\nlet full = summary_view.generate_summary(None, &db_conn);\nlet peak = full.peak_array();\nassert_eq!(peak[0][0], \"Peak Earning\");\nassert_eq!(peak[0][2], \"5000.00\");\nassert_eq!(peak[0][1], \"02-2024\");", + "body_hash": "78fb61580b808d84d809c385052a8de94e1b3bb3b3964cab59410cab85b8979b" + }, + "app/tests/summary.rs|crate::summary_peak_across_months#free:d53ee6290327": { + "kind": "Free", + "file": "app/tests/summary.rs", + "item_path": "crate::summary_peak_across_months#free:d53ee6290327", + "comment_kind": "Line", + "comment_text": "// Month 1: low earning", + "comment_hash": "d53ee629032716af19bd45944c8394b50d6fc1ffce4902b172d7b4880df4e176", + "body_text": "add_tx(\n &mut db_conn,\n \"2024-01-15\",\n \"Jan\",\n \"Cash\",\n \"\",\n \"100.00\",\n \"Income\",\n \"A\",\n );", + "body_hash": "306a4b2643f146b0efc1bb6e274ca5ceda08b0251d78e3bb69be7a2b1361d5e1" + }, + "app/tests/summary.rs|crate::summary_peak_across_months#trailing:95b9211a35ba": { + "kind": "Free", + "file": "app/tests/summary.rs", + "item_path": "crate::summary_peak_across_months#trailing:95b9211a35ba", + "comment_kind": "Line", + "comment_text": "// Feb date format", + "comment_hash": "95b9211a35ba78a07023413961784faa0e034e6e14c5dbbabebeb79ee2dd4e42", + "body_text": "assert_eq!(peak[0][1], \"02-2024\");", + "body_hash": "9b2e86281185ab5d2ca14653aec13b698cc175f500475b7c3df00ad42ad33b27" + }, + "app/tests/summary.rs|crate::summary_tags_array_groups_by_tag#free:3cd7abbda88b": { + "kind": "Free", + "file": "app/tests/summary.rs", + "item_path": "crate::summary_tags_array_groups_by_tag#free:3cd7abbda88b", + "comment_kind": "Line", + "comment_text": "// Find row for \"Work\" tag", + "comment_hash": "3cd7abbda88b068ff35382e8ac9affc613224a91f02c3af9bf5306988cfdb800", + "body_text": "let work_row = tags.iter().find(|r| r[0] == \"Work\").unwrap();\nassert_eq!(work_row[1], \"2000.00\");", + "body_hash": "eff15b7960111b69c7d108ee3d766de2d89b7b146249ed3ae9250982d5d68d84" + }, + "app/tests/summary.rs|crate::summary_tags_array_groups_by_tag#trailing:346819ea8977": { + "kind": "Free", + "file": "app/tests/summary.rs", + "item_path": "crate::summary_tags_array_groups_by_tag#trailing:346819ea8977", + "comment_kind": "Line", + "comment_text": "// income amount", + "comment_hash": "346819ea89774bd2bd517a675db1147525495dadab487a7931d8cbbf871ce7f8", + "body_text": "assert_eq!(work_row[1], \"2000.00\");", + "body_hash": "13135099d0bc25e2684c01755f90605bf8ef53eb7ad145c50a9c2ac2a18fac27" + }, + "app/tests/summary.rs|crate::summary_tags_array_groups_by_tag#trailing:7518cce48c57": { + "kind": "Free", + "file": "app/tests/summary.rs", + "item_path": "crate::summary_tags_array_groups_by_tag#trailing:7518cce48c57", + "comment_kind": "Line", + "comment_text": "// expense", + "comment_hash": "7518cce48c57e86b047890245cde5d7971085c620090ede54f389964128f6491", + "body_text": "assert_eq!(food_row[2], \"200.00\");", + "body_hash": "63922c7565692ee261f5098c382678a56f1ac15342210d234bc676d56ab0e8c1" + }, + "app/tests/summary.rs|crate::summary_tags_array_groups_by_tag#trailing:b0a20d563513": { + "kind": "Free", + "file": "app/tests/summary.rs", + "item_path": "crate::summary_tags_array_groups_by_tag#trailing:b0a20d563513", + "comment_kind": "Line", + "comment_text": "// expense amount", + "comment_hash": "b0a20d5635134e41da176559ed582ba54e598e327fb1cb76c1b228e9bcea019c", + "body_text": "assert_eq!(work_row[2], \"0.00\");", + "body_hash": "e24981d14cd5fa9d323fb1b4b1ae5d895327865ec33895f88e446bedf15f7241" + }, + "app/tests/summary.rs|crate::summary_tags_array_with_compare#free:1825da5e3bc4": { + "kind": "Free", + "file": "app/tests/summary.rs", + "item_path": "crate::summary_tags_array_with_compare#free:1825da5e3bc4", + "comment_kind": "Line", + "comment_text": "// Income amount for Work tag", + "comment_hash": "1825da5e3bc47117f78ba6f910720f2f8d3ff16d4ea25aea8dffaaa325a3db59", + "body_text": "assert_eq!(work_row[1], \"2500.00\");", + "body_hash": "c433acda84aa49879a35bdb0200880ce8b7ef8f462319ba96d57d83a80ac3862" + }, + "app/tests/summary.rs|crate::summary_tags_array_with_compare#free:2d35cf319cb3": { + "kind": "Free", + "file": "app/tests/summary.rs", + "item_path": "crate::summary_tags_array_with_compare#free:2d35cf319cb3", + "comment_kind": "Line", + "comment_text": "// September: different amounts", + "comment_hash": "2d35cf319cb367d66087768a713a3ad28453f69b30c5dc79921466be99a89a87", + "body_text": "add_tx(\n &mut db_conn,\n \"2024-09-01\",\n \"Salary\",\n \"Cash\",\n \"\",\n \"2500.00\",\n \"Income\",\n \"Work\",\n );\nadd_tx(\n &mut db_conn,\n \"2024-09-15\",\n \"Rent\",\n \"Cash\",\n \"\",\n \"800.00\",\n \"Expense\",\n \"Housing\",\n );\nlet sep = db_conn\n .get_summary_with_str(\"September\", \"2024\", FetchNature::Monthly)\n .unwrap();\nlet aug = db_conn\n .get_summary_with_str(\"August\", \"2024\", FetchNature::Monthly)\n .unwrap();\nlet tags = sep.tags_array(Some(&aug), &db_conn);", + "body_hash": "f8ed141745e809283a756ab9b091aa85299a0a695a2f752244b50050a021ac37" + }, + "app/tests/summary.rs|crate::summary_tags_array_with_compare#free:30a91048a590": { + "kind": "Free", + "file": "app/tests/summary.rs", + "item_path": "crate::summary_tags_array_with_compare#free:30a91048a590", + "comment_kind": "Line", + "comment_text": "// August: income + expense", + "comment_hash": "30a91048a5902ec7ca425b16a8d9a2c7091da38af323dc1515e53d3b5a41b98c", + "body_text": "add_tx(\n &mut db_conn,\n \"2024-08-01\",\n \"Salary\",\n \"Cash\",\n \"\",\n \"2000.00\",\n \"Income\",\n \"Work\",\n );\nadd_tx(\n &mut db_conn,\n \"2024-08-15\",\n \"Rent\",\n \"Cash\",\n \"\",\n \"1000.00\",\n \"Expense\",\n \"Housing\",\n );", + "body_hash": "d4a02aa199fda9c39ab51257358e7a580515f5f7195cb9bf9342da5d1ee901e7" + }, + "app/tests/summary.rs|crate::summary_tags_array_with_compare#free:8de4c3afb154": { + "kind": "Free", + "file": "app/tests/summary.rs", + "item_path": "crate::summary_tags_array_with_compare#free:8de4c3afb154", + "comment_kind": "Line", + "comment_text": "// Should include MoM/YoY comparison columns with ↑/↓ percentages", + "comment_hash": "8de4c3afb154730851e3d063b6c096bd90a5654055bd112c5db414d00d1a4171", + "body_text": "let work_row = tags.iter().find(|r| r[0] == \"Work\").unwrap();", + "body_hash": "0bd8e8a26e3a257155e2fa453f99499c6a227c5f8af2f27d8b559be1e22eeafb" + }, + "app/tests/summary.rs|crate::summary_tags_array_with_compare#free:9627acc7e173": { + "kind": "Free", + "file": "app/tests/summary.rs", + "item_path": "crate::summary_tags_array_with_compare#free:9627acc7e173", + "comment_kind": "Line", + "comment_text": "// Should have compare columns (positions 6-7 after income/expense %)", + "comment_hash": "9627acc7e173d39ae79857131d876f5e3b715a827b23b4063680a22803fb96ae", + "body_text": "assert!(work_row.len() > 6, \"Should have compare columns\");\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "4065bd3ef4314b7c9fe7714dbe37c806dd6e7f6f32978e2b58318144469e47a8" + }, + "app/tests/summary.rs|crate::summary_with_last_summary_mom_comparison#free:409a9a62de5a": { + "kind": "Free", + "file": "app/tests/summary.rs", + "item_path": "crate::summary_with_last_summary_mom_comparison#free:409a9a62de5a", + "comment_kind": "Line", + "comment_text": "// Net should have MoM comparison strings (↑/↓ with percentages)", + "comment_hash": "409a9a62de5abd7d479f6fb10e1172a6682d09c56426b1097a5e061d32e2c29b", + "body_text": "let net = sep_full.net_array();\nassert!(\n net[0].len() >= 7,\n \"Net array should have MoM columns, got len {}\",\n net[0].len()\n );", + "body_hash": "651b566ff9a844bc9ba81a92acaa25318ef1cee61b5de3a345524d37a51ff2d1" + }, + "app/tests/summary.rs|crate::summary_with_last_summary_mom_comparison#free:8e3b7b55c72c": { + "kind": "Free", + "file": "app/tests/summary.rs", + "item_path": "crate::summary_with_last_summary_mom_comparison#free:8e3b7b55c72c", + "comment_kind": "Line", + "comment_text": "// September — higher numbers", + "comment_hash": "8e3b7b55c72c635cda2dfa1b846aad9ae9033cce907594900cba9eb769f01511", + "body_text": "add_tx(\n &mut db_conn,\n \"2024-09-01\",\n \"Income\",\n \"Cash\",\n \"\",\n \"2000.00\",\n \"Income\",\n \"Work\",\n );\nadd_tx(\n &mut db_conn,\n \"2024-09-15\",\n \"Expense\",\n \"Cash\",\n \"\",\n \"300.00\",\n \"Expense\",\n \"Food\",\n );\nlet aug_view = db_conn\n .get_summary_with_str(\"August\", \"2024\", FetchNature::Monthly)\n .unwrap();\nlet aug_full = aug_view.generate_summary(None, &db_conn);\nlet sep_view = db_conn\n .get_summary_with_str(\"September\", \"2024\", FetchNature::Monthly)\n .unwrap();\nlet sep_full = sep_view.generate_summary(Some(&aug_full), &db_conn);", + "body_hash": "b693c6c11937f403cf8aa79b59458d32f533feb06d19486eebb32bfb7bd2cfe5" + }, + "app/tests/summary.rs|crate::summary_with_last_summary_mom_comparison#free:b04af7e7f8a0": { + "kind": "Free", + "file": "app/tests/summary.rs", + "item_path": "crate::summary_with_last_summary_mom_comparison#free:b04af7e7f8a0", + "comment_kind": "Line", + "comment_text": "// Methods should have MoM comparison", + "comment_hash": "b04af7e7f8a0fe8a0e9a78cebe46a7b4e082cb991508351e4493c43b73bf2c80", + "body_text": "let methods = sep_full.method_array();\nlet cash_row = methods.iter().find(|r| r[0] == \"Cash\").unwrap();\nassert!(cash_row.len() >= 7, \"Method row should have MoM columns\");\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "08134d37301da141e897fd6a97106957e314b220c76f0261e74c0090d894436b" + }, + "app/tests/summary.rs|crate::summary_with_last_summary_mom_comparison#free:dc2941925151": { + "kind": "Free", + "file": "app/tests/summary.rs", + "item_path": "crate::summary_with_last_summary_mom_comparison#free:dc2941925151", + "comment_kind": "Line", + "comment_text": "// August", + "comment_hash": "dc2941925151d72f98c1477f557befe6ded15bddd6e4af3fce9428e275d18885", + "body_text": "add_tx(\n &mut db_conn,\n \"2024-08-01\",\n \"Income\",\n \"Cash\",\n \"\",\n \"1000.00\",\n \"Income\",\n \"Work\",\n );\nadd_tx(\n &mut db_conn,\n \"2024-08-15\",\n \"Expense\",\n \"Cash\",\n \"\",\n \"200.00\",\n \"Expense\",\n \"Food\",\n );", + "body_hash": "3ba916c0fb68440cb9d0286cb1fc93ee869052bc56f49ead6d733c91b5e38dcb" + }, + "app/tests/summary.rs|crate::summary_yearly_with_monthly_averages#free:624c1470c7dc": { + "kind": "Free", + "file": "app/tests/summary.rs", + "item_path": "crate::summary_yearly_with_monthly_averages#free:624c1470c7dc", + "comment_kind": "Line", + "comment_text": "// Total income: 400, over 2 months, avg = 200", + "comment_hash": "624c1470c7dc60e55e407ff5dccd22b92a8154ebe76f2f0945a3074d8bbc3129", + "body_text": "let net = full.net_array();\nassert_eq!(net[0][1], \"400.00\");", + "body_hash": "d2c0d7e38dbfe7a6331f0806059435c987cb16c4633f72ad92f7ebb3949459ff" + }, + "app/tests/summary.rs|crate::summary_yearly_with_monthly_averages#free:dd207f64cf68": { + "kind": "Free", + "file": "app/tests/summary.rs", + "item_path": "crate::summary_yearly_with_monthly_averages#free:dd207f64cf68", + "comment_kind": "Line", + "comment_text": "// Feb: income 300", + "comment_hash": "dd207f64cf6815ebab17649572633dcbaa7aec484c8115f228692a9bdac09484", + "body_text": "add_tx(\n &mut db_conn,\n \"2024-02-15\",\n \"Feb\",\n \"Cash\",\n \"\",\n \"300.00\",\n \"Income\",\n \"B\",\n );\nlet summary_view = db_conn\n .get_summary_with_str(\"January\", \"2024\", FetchNature::Yearly)\n .unwrap();\nlet full = summary_view.generate_summary(None, &db_conn);", + "body_hash": "1d81e603c77e57fb8e39211062f69ae90e32dc605a348a3a88833675d04700a8" + }, + "app/tests/summary.rs|crate::summary_yearly_with_monthly_averages#free:e3b0c1db0e6b": { + "kind": "Free", + "file": "app/tests/summary.rs", + "item_path": "crate::summary_yearly_with_monthly_averages#free:e3b0c1db0e6b", + "comment_kind": "Line", + "comment_text": "// Jan: income 100", + "comment_hash": "e3b0c1db0e6bda9b3640efab688f2dde5304a24be3b7ce4051d519e0469df420", + "body_text": "add_tx(\n &mut db_conn,\n \"2024-01-15\",\n \"Jan\",\n \"Cash\",\n \"\",\n \"100.00\",\n \"Income\",\n \"A\",\n );", + "body_hash": "306a4b2643f146b0efc1bb6e274ca5ceda08b0251d78e3bb69be7a2b1361d5e1" + }, + "app/tests/summary.rs|crate::summary_yearly_with_monthly_averages#trailing:c148bcd6f3da": { + "kind": "Free", + "file": "app/tests/summary.rs", + "item_path": "crate::summary_yearly_with_monthly_averages#trailing:c148bcd6f3da", + "comment_kind": "Line", + "comment_text": "// total income", + "comment_hash": "c148bcd6f3daff54e55ed2486edd3567beea4349d23ad1c47e18f22f7296d173", + "body_text": "assert_eq!(net[0][1], \"400.00\");", + "body_hash": "86831923bf0f5918286d944368ea177dfeacaac7e5fb700ac7dd4a1b1de05bb3" + }, + "app/tests/tx_methods.rs|crate::add_new_method_increments_position#free:3d86fc25819a": { + "kind": "Free", + "file": "app/tests/tx_methods.rs", + "item_path": "crate::add_new_method_increments_position#free:3d86fc25819a", + "comment_kind": "Line", + "comment_text": "// Initial: Cash(1), Bank(2), Other(3)", + "comment_hash": "3d86fc25819a09ec506b776f9b5120b77a9777a9e8910ccfd7ba1d9166f0e605", + "body_text": "let methods = db_conn.get_tx_methods_sorted();\nlet names: Vec<&str> = methods.iter().map(|m| m.name.as_str()).collect();\nassert_eq!(names, vec![\"Cash\", \"Bank\", \"Other\"]);\nassert_eq!(methods[0].position, 1);\nassert_eq!(methods[1].position, 2);\nassert_eq!(methods[2].position, 3);", + "body_hash": "b2d77f08e4841ca8a899eb53a1d1d810d8261c902b21416df7c6c2edeb722f95" + }, + "app/tests/tx_methods.rs|crate::add_new_method_increments_position#free:ae6dca5e58be": { + "kind": "Free", + "file": "app/tests/tx_methods.rs", + "item_path": "crate::add_new_method_increments_position#free:ae6dca5e58be", + "comment_kind": "Line", + "comment_text": "// Add one method", + "comment_hash": "ae6dca5e58beeb7cc46185862a592e6e96b60786878a5c43d008fc1b5426ffaa", + "body_text": "let new = vec![\"Wallet\".to_string()];\ndb_conn.add_new_methods(&new).unwrap();\nlet methods = db_conn.get_tx_methods_sorted();\nlet names: Vec<&str> = methods.iter().map(|m| m.name.as_str()).collect();\nassert_eq!(names, vec![\"Cash\", \"Bank\", \"Other\", \"Wallet\"]);\nassert_eq!(methods[3].position, 4);\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "39f7facf7497e7f846f7488e382b1333a7ec0f50365af86006dfb6be411daa3f" + }, + "app/tests/tx_methods.rs|crate::get_final_balances_returns_all_methods#free:c40f5d44892d": { + "kind": "Free", + "file": "app/tests/tx_methods.rs", + "item_path": "crate::get_final_balances_returns_all_methods#free:c40f5d44892d", + "comment_kind": "Line", + "comment_text": "// Fresh methods should have zero final balance", + "comment_hash": "c40f5d44892d4b00ecb083431c88487af44af756933e754bfea5f2227cd9cacf", + "body_text": "assert_eq!(balances[&cash_id].balance, 0);\nassert_eq!(balances[&bank_id].balance, 0);\nassert_eq!(balances[&other_id].balance, 0);\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "4f2eeebfa3cbb80055109308a8267f0b134cc2aeee20346e7a56a879e1d848fc" + }, + "app/tests/tx_methods.rs|crate::is_tx_method_empty#free:54afa39d455c": { + "kind": "Free", + "file": "app/tests/tx_methods.rs", + "item_path": "crate::is_tx_method_empty#free:54afa39d455c", + "comment_kind": "Line", + "comment_text": "// create_test_db already adds 3 methods, so test with a fresh conn", + "comment_hash": "54afa39d455c28332ad1c00b2df356aa46f7ee1bf333e629751e892869c40f88", + "body_text": "let file_name = \"test_method_empty.sqlite\";\nlet db_conn = create_test_db(file_name);\nassert!(!db_conn.is_tx_method_empty());\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "29c9333292365f3409ab56505fba0c1275db46c5d891885591e4df17a45f5bc6" + }, + "app/tests/tx_methods.rs|crate::rename_method_updates_name_preserves_position#free:dfc77d031c05": { + "kind": "Free", + "file": "app/tests/tx_methods.rs", + "item_path": "crate::rename_method_updates_name_preserves_position#free:dfc77d031c05", + "comment_kind": "Line", + "comment_text": "// Old name no longer accessible via cache", + "comment_hash": "dfc77d031c0544d8b7f79aa17b5a8b1672bd1cb300d8c4acd1feb877b0716bdc", + "body_text": "assert!(db_conn.get_tx_method_by_name(\"Cash\").is_err());\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "f7cc5bd2d0407be72cbcfa0bce326196b9ef2af6c22e4e4e84b08d40d70a14b9" + }, + "app/tests/tx_methods.rs|crate::reorder_methods_changes_positions#free:063bdfd45c46": { + "kind": "Free", + "file": "app/tests/tx_methods.rs", + "item_path": "crate::reorder_methods_changes_positions#free:063bdfd45c46", + "comment_kind": "Line", + "comment_text": "// Reverse order: Other, Bank, Cash", + "comment_hash": "063bdfd45c46926bb158e0111a7ff7b106a48aa5c5ea42901bb47cc60a0be566", + "body_text": "let new_order: Vec = vec![\"Other\", \"Bank\", \"Cash\"]\n .into_iter()\n .map(String::from)\n .collect();\ndb_conn.set_new_tx_method_positions(&new_order).unwrap();\nlet methods = db_conn.get_tx_methods_sorted();\nlet names: Vec<&str> = methods.iter().map(|m| m.name.as_str()).collect();\nassert_eq!(names, vec![\"Other\", \"Bank\", \"Cash\"]);", + "body_hash": "1b1d4958d5552009e423c24647661970c43a6a04090cd5b70ee0ab66bb0ab1d8" + }, + "app/tests/tx_methods.rs|crate::reorder_methods_changes_positions#free:ac503fa2a2bf": { + "kind": "Free", + "file": "app/tests/tx_methods.rs", + "item_path": "crate::reorder_methods_changes_positions#free:ac503fa2a2bf", + "comment_kind": "Line", + "comment_text": "// set_new_tx_method_positions assigns 0-based positions", + "comment_hash": "ac503fa2a2bf66b626744fca5e75221355f9414d2bf7e1eb0fe841e09165aac9", + "body_text": "assert_eq!(methods[0].position, 0);\nassert_eq!(methods[1].position, 1);\nassert_eq!(methods[2].position, 2);\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "c9082d6e2142faa34c1eda11916d6a0710518c6e71b4d1cad6fec16b0ef9e8f1" + }, + "app/tests/tx_types.rs|crate::all_tx_types_impact_balance_correctly#free:0ae699a8cd0f": { + "kind": "Free", + "file": "app/tests/tx_types.rs", + "item_path": "crate::all_tx_types_impact_balance_correctly#free:0ae699a8cd0f", + "comment_kind": "Line", + "comment_text": "// Bank: 0 + 200 = 200", + "comment_hash": "0ae699a8cd0f93c32a4f8c07c7cfb3bbb22cebdf2c3fc175eacc929086438cf0", + "body_text": "assert_eq!(balance[&bank_id].value(), 20000);\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "71cac8750a26b748041a277ebaddbf9321c112efefca17bf5acde44288b2acef" + }, + "app/tests/tx_types.rs|crate::all_tx_types_impact_balance_correctly#free:1e4c2fe98c08": { + "kind": "Free", + "file": "app/tests/tx_types.rs", + "item_path": "crate::all_tx_types_impact_balance_correctly#free:1e4c2fe98c08", + "comment_kind": "Line", + "comment_text": "// Transfer: -200 from Cash, +200 to Bank", + "comment_hash": "1e4c2fe98c08fcced2c80b3c62d377ffe9f8608e44d3faa07a5421b3f7a77fd5", + "body_text": "add_tx(\n &mut db_conn,\n \"2025-02-10\",\n \"Transfer\",\n \"Cash\",\n \"Bank\",\n \"200.00\",\n \"Transfer\",\n \"Xfer\",\n );", + "body_hash": "00f4f2f4fc6c2d071e0e9fe09b424713c759f43a7d712389adbe0e41180b0536" + }, + "app/tests/tx_types.rs|crate::all_tx_types_impact_balance_correctly#free:2ad98f7a8c04": { + "kind": "Free", + "file": "app/tests/tx_types.rs", + "item_path": "crate::all_tx_types_impact_balance_correctly#free:2ad98f7a8c04", + "comment_kind": "Line", + "comment_text": "// Lend: -50 Cash", + "comment_hash": "2ad98f7a8c04b58217c8db70a3c545a81b723b5d4b7964689593189ed87b6f2e", + "body_text": "add_tx(\n &mut db_conn,\n \"2025-02-25\",\n \"Lend\",\n \"Cash\",\n \"\",\n \"50.00\",\n \"Lend\",\n \"Friend\",\n );", + "body_hash": "2f17467a9daa931563dea8c499cd02ab33730e22993cff52cf46426476942711" + }, + "app/tests/tx_types.rs|crate::all_tx_types_impact_balance_correctly#free:41c5bfe06bc7": { + "kind": "Free", + "file": "app/tests/tx_types.rs", + "item_path": "crate::all_tx_types_impact_balance_correctly#free:41c5bfe06bc7", + "comment_kind": "Line", + "comment_text": "// Borrow: +300 Cash", + "comment_hash": "41c5bfe06bc7593628ff35887080383827cc1fa725c0225d639943616d2920ec", + "body_text": "add_tx(\n &mut db_conn,\n \"2025-02-15\",\n \"Borrow\",\n \"Cash\",\n \"\",\n \"300.00\",\n \"Borrow\",\n \"Loan\",\n );", + "body_hash": "849f9472fa9eb12245cc0e558c42b4cdb04bd2ffc6cb75b97c41c3c7d3db836b" + }, + "app/tests/tx_types.rs|crate::all_tx_types_impact_balance_correctly#free:44caf16e99db": { + "kind": "Free", + "file": "app/tests/tx_types.rs", + "item_path": "crate::all_tx_types_impact_balance_correctly#free:44caf16e99db", + "comment_kind": "Line", + "comment_text": "// Cash: 500 - 100 - 200 + 300 - 150 - 50 + 25 = 325", + "comment_hash": "44caf16e99db8cbd91b074c0d491199908a1bb3b55dbff39f5fd550649f2e620", + "body_text": "assert_eq!(balance[&cash_id].value(), 32500);", + "body_hash": "88195246fcab80994a5c84563271d1c865396849fafbb33bae8ed298deaadf66" + }, + "app/tests/tx_types.rs|crate::all_tx_types_impact_balance_correctly#free:521052bdb35f": { + "kind": "Free", + "file": "app/tests/tx_types.rs", + "item_path": "crate::all_tx_types_impact_balance_correctly#free:521052bdb35f", + "comment_kind": "Line", + "comment_text": "// LendRepay: +25 Cash", + "comment_hash": "521052bdb35fef549f0c54ac4be613de322de09eef1a1708384ba88ce896a017", + "body_text": "add_tx(\n &mut db_conn,\n \"2025-02-28\",\n \"Lend Repay\",\n \"Cash\",\n \"\",\n \"25.00\",\n \"Lend Repay\",\n \"Friend\",\n );\nlet date = NaiveDate::from_ymd_opt(2025, 2, 1).unwrap();\nlet tx_view = db_conn\n .fetch_txs_with_date(date, FetchNature::Monthly)\n .unwrap();\nlet balance = tx_view.get_tx_balance(6);", + "body_hash": "ed0b0a76a4135688d7d0ea4694056847513fe95c3c2c635771cb34522a2ede0a" + }, + "app/tests/tx_types.rs|crate::all_tx_types_impact_balance_correctly#free:6407f984a6f0": { + "kind": "Free", + "file": "app/tests/tx_types.rs", + "item_path": "crate::all_tx_types_impact_balance_correctly#free:6407f984a6f0", + "comment_kind": "Line", + "comment_text": "// Expense: -100 Cash", + "comment_hash": "6407f984a6f02c536bda6bb398f8d67391def685b9aa03157697edb892d80b61", + "body_text": "add_tx(\n &mut db_conn,\n \"2025-02-05\",\n \"Expense\",\n \"Cash\",\n \"\",\n \"100.00\",\n \"Expense\",\n \"Food\",\n );", + "body_hash": "111c2e1830d21bd1db862ac2b3898f0c9b36f951c57397ff83d1d0ffb41a9094" + }, + "app/tests/tx_types.rs|crate::all_tx_types_impact_balance_correctly#free:8d8726a44c1e": { + "kind": "Free", + "file": "app/tests/tx_types.rs", + "item_path": "crate::all_tx_types_impact_balance_correctly#free:8d8726a44c1e", + "comment_kind": "Line", + "comment_text": "// BorrowRepay: -150 Cash", + "comment_hash": "8d8726a44c1e4540189d35d9d115423aeaaf4339e25daf79400de037406e18c0", + "body_text": "add_tx(\n &mut db_conn,\n \"2025-02-20\",\n \"Borrow Repay\",\n \"Cash\",\n \"\",\n \"150.00\",\n \"Borrow Repay\",\n \"Loan\",\n );", + "body_hash": "eeb88f356766c931b2e95a89c2a84b84025ec855a5e37327070054ab867c7252" + }, + "app/tests/tx_types.rs|crate::all_tx_types_impact_balance_correctly#free:bf55dd67f096": { + "kind": "Free", + "file": "app/tests/tx_types.rs", + "item_path": "crate::all_tx_types_impact_balance_correctly#free:bf55dd67f096", + "comment_kind": "Line", + "comment_text": "// Income: +500 Cash", + "comment_hash": "bf55dd67f09699fb0f206286c40b72148f7c982b03b7e99bf60197bba82c8cdf", + "body_text": "add_tx(\n &mut db_conn,\n \"2025-02-01\",\n \"Income\",\n \"Cash\",\n \"\",\n \"500.00\",\n \"Income\",\n \"Salary\",\n );", + "body_hash": "8ab7c350eedd88ae203750fe4c35ff50dfa7ab4cd16c5cd591b4152750867107" + }, + "app/tests/tx_types.rs|crate::borrow_repay_decreases_balance#free:48ebc590bf61": { + "kind": "Free", + "file": "app/tests/tx_types.rs", + "item_path": "crate::borrow_repay_decreases_balance#free:48ebc590bf61", + "comment_kind": "Line", + "comment_text": "// Borrow +500, Repay -500 = 0", + "comment_hash": "48ebc590bf612dc75b6d1dff8959a0bc6cb61ff92d81396ef637ad5868cd2738", + "body_text": "assert_eq!(tx_view.get_tx_balance(1)[&cash_id].value(), 0);\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "3b05dabc697654e22bb199694c499a1e17397611acf0ebc1b82339e8d3a96b31" + }, + "app/tests/tx_types.rs|crate::borrow_repay_decreases_balance#free:c416a84bd4f5": { + "kind": "Free", + "file": "app/tests/tx_types.rs", + "item_path": "crate::borrow_repay_decreases_balance#free:c416a84bd4f5", + "comment_kind": "Line", + "comment_text": "// Need money first", + "comment_hash": "c416a84bd4f557a009f973b5138d905d9f1f0393aa67960c3750c10e3f0517ed", + "body_text": "add_tx(\n &mut db_conn,\n \"2024-11-01\",\n \"Borrow\",\n \"Cash\",\n \"\",\n \"500.00\",\n \"Borrow\",\n \"Loan\",\n );\nadd_tx(\n &mut db_conn,\n \"2024-11-15\",\n \"Repay loan\",\n \"Cash\",\n \"\",\n \"500.00\",\n \"Borrow Repay\",\n \"Loan\",\n );\nlet date = NaiveDate::from_ymd_opt(2024, 11, 1).unwrap();\nlet tx_view = db_conn\n .fetch_txs_with_date(date, FetchNature::Monthly)\n .unwrap();", + "body_hash": "c4e3c65f8a05859b5b53a735038460e8b275168350cc0c66033085aa4c5abbba" + }, + "app/tests/tx_types.rs|crate::edit_transfer_amount_updates_both_methods#trailing:bba89b82450a": { + "kind": "Free", + "file": "app/tests/tx_types.rs", + "item_path": "crate::edit_transfer_amount_updates_both_methods#trailing:bba89b82450a", + "comment_kind": "Line", + "comment_text": "// 0 + 500", + "comment_hash": "bba89b82450a1085213dbeacbcab6f0f66ad3146b93b614518b9de1e2f067a4d", + "body_text": "assert_eq!(tx_view.get_tx_balance(1)[&bank_id].value(), 50000);", + "body_hash": "1baeacc5499afa02ef1cc124197c774496ad7d775c13aaba845176a5a15df467" + }, + "app/tests/tx_types.rs|crate::edit_transfer_amount_updates_both_methods#trailing:d8f050b96646": { + "kind": "Free", + "file": "app/tests/tx_types.rs", + "item_path": "crate::edit_transfer_amount_updates_both_methods#trailing:d8f050b96646", + "comment_kind": "Line", + "comment_text": "// 1000 - 500", + "comment_hash": "d8f050b966465c044809603680de52a9140869cd49b86bb66cf6a5fd8481b806", + "body_text": "assert_eq!(tx_view.get_tx_balance(1)[&cash_id].value(), 50000);", + "body_hash": "198969ca908112828399256828abcaa5689614785f2451a2e8b5ecc3940f6769" + }, + "app/tests/tx_types.rs|crate::lend_repay_increases_balance#free:d8a5d44f6ff9": { + "kind": "Free", + "file": "app/tests/tx_types.rs", + "item_path": "crate::lend_repay_increases_balance#free:d8a5d44f6ff9", + "comment_kind": "Line", + "comment_text": "// Lend -200, Repay +200 = 0", + "comment_hash": "d8a5d44f6ff9adde799a8a4965949afe23bd13414d40aec5e270423aeafc1189", + "body_text": "assert_eq!(tx_view.get_tx_balance(1)[&cash_id].value(), 0);\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "3b05dabc697654e22bb199694c499a1e17397611acf0ebc1b82339e8d3a96b31" + }, + "app/tests/tx_types.rs|crate::transfer_moves_money_between_methods#free:678c883f2089": { + "kind": "Free", + "file": "app/tests/tx_types.rs", + "item_path": "crate::transfer_moves_money_between_methods#free:678c883f2089", + "comment_kind": "Line", + "comment_text": "// Seed Cash with income", + "comment_hash": "678c883f208967b343d0baf13f6fc987c405d0bbff9090238436c1d75340256b", + "body_text": "add_tx(\n &mut db_conn,\n \"2024-06-01\",\n \"Salary\",\n \"Cash\",\n \"\",\n \"1000.00\",\n \"Income\",\n \"Salary\",\n );", + "body_hash": "26ef237ef931aa8cbffb8c4776f6414e40ff2882f1e39252540f9ad7cd9645c4" + }, + "app/tests/tx_types.rs|crate::transfer_moves_money_between_methods#free:844966ea8064": { + "kind": "Free", + "file": "app/tests/tx_types.rs", + "item_path": "crate::transfer_moves_money_between_methods#free:844966ea8064", + "comment_kind": "Line", + "comment_text": "// Transfer 300 from Cash to Bank", + "comment_hash": "844966ea8064c62b4aaddc58053730ea46057ca204f273d378a91390ecf320d9", + "body_text": "add_tx(\n &mut db_conn,\n \"2024-06-10\",\n \"Move funds\",\n \"Cash\",\n \"Bank\",\n \"300.00\",\n \"Transfer\",\n \"Transfer\",\n );\nlet date = NaiveDate::from_ymd_opt(2024, 6, 1).unwrap();\nlet tx_view = db_conn\n .fetch_txs_with_date(date, FetchNature::Monthly)\n .unwrap();\nassert_eq!(tx_view.len(), 2);\nlet balance = tx_view.get_tx_balance(1);\nassert_eq!(balance[&cash_id].value(), 70000);", + "body_hash": "598f4360fdaf08446a79b19f2d5d743f34eac71ecb37b8695447eb0457c12666" + }, + "app/tests/tx_types.rs|crate::transfer_moves_money_between_methods#trailing:93c6285fb038": { + "kind": "Free", + "file": "app/tests/tx_types.rs", + "item_path": "crate::transfer_moves_money_between_methods#trailing:93c6285fb038", + "comment_kind": "Line", + "comment_text": "// unchanged\n// Total across all methods unchanged", + "comment_hash": "93c6285fb038f84f283492a9cd243b60da4cb823505106e138aabe37c62fa1ff", + "body_text": "assert_eq!(balance[&other_id].value(), 0);", + "body_hash": "1aa7546176de0a36dd4cc348e8f7c71dfbfae8611ecb2bf8479434e24149d23a" + }, + "app/tests/tx_types.rs|crate::transfer_moves_money_between_methods#trailing:acb0437057c0": { + "kind": "Free", + "file": "app/tests/tx_types.rs", + "item_path": "crate::transfer_moves_money_between_methods#trailing:acb0437057c0", + "comment_kind": "Line", + "comment_text": "// 0 + 300", + "comment_hash": "acb0437057c0dff433c62ced91e1293b94dba8319189a5bdb239abfbd2913b90", + "body_text": "assert_eq!(balance[&bank_id].value(), 30000);", + "body_hash": "33da4fd806672d4b983c81f60e0d9f65822bd694371fd38e4874c108635b87dc" + }, + "app/tests/tx_types.rs|crate::transfer_moves_money_between_methods#trailing:c2db7c843a18": { + "kind": "Free", + "file": "app/tests/tx_types.rs", + "item_path": "crate::transfer_moves_money_between_methods#trailing:c2db7c843a18", + "comment_kind": "Line", + "comment_text": "// 1000 - 300", + "comment_hash": "c2db7c843a1883d820f66a6c0bf346e900287dc03405a6e3411a643e5ef87d44", + "body_text": "assert_eq!(balance[&cash_id].value(), 70000);", + "body_hash": "9f26bcfa83acd67ecca66bcd1004fd3ecc0b99b1e0c4b12524d9716d8afae4aa" + }, + "app/tests/tx_types.rs|crate::transfer_then_reverse_transfer_returns_to_original#trailing:2b530a7e3d1f": { + "kind": "Free", + "file": "app/tests/tx_types.rs", + "item_path": "crate::transfer_then_reverse_transfer_returns_to_original#trailing:2b530a7e3d1f", + "comment_kind": "Line", + "comment_text": "// 0 + 300 - 300", + "comment_hash": "2b530a7e3d1f9f22576870855333b6ae42dca5110fb8fcf4c38ebb79f2b31317", + "body_text": "assert_eq!(balance[&bank_id].value(), 0);", + "body_hash": "cd3ac20387e659d44a4622b8011c80794a5a9706afa7ecbbbbc74a07fb722060" + }, + "app/tests/tx_types.rs|crate::transfer_then_reverse_transfer_returns_to_original#trailing:3a9798f87e76": { + "kind": "Free", + "file": "app/tests/tx_types.rs", + "item_path": "crate::transfer_then_reverse_transfer_returns_to_original#trailing:3a9798f87e76", + "comment_kind": "Line", + "comment_text": "// 1000 - 300 + 300", + "comment_hash": "3a9798f87e76f6064b49d894486929fb0f9cb316f1194ff1f2931ec11296bb74", + "body_text": "assert_eq!(balance[&cash_id].value(), 100000);", + "body_hash": "87377acc83af999f102751e1faf025bbc29dd2dca2dd76cd1a38b6743c34c480" + }, + "app/tests/tx_view.rs|crate::add_tx_balance_array_at_index_zero#free:27e9bd612409": { + "kind": "Free", + "file": "app/tests/tx_view.rs", + "item_path": "crate::add_tx_balance_array_at_index_zero#free:27e9bd612409", + "comment_kind": "Line", + "comment_text": "// Before first tx: balance=0. Project expense -100 → -100", + "comment_hash": "27e9bd612409870dd8bd017e8aafc773ec389543ad9d42ed86694d0ed59c37bb", + "body_text": "assert_eq!(get_col(&arr, 1, 0), \"Balance\");\nassert_eq!(get_col(&arr, 1, 1), \"-100.00\");\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "d9e7d10bcfa5a6beb5d17df96f1e562d6daa81df8dedd82653e1fe9696f47bd2" + }, + "app/tests/tx_view.rs|crate::add_tx_balance_array_at_index_zero#free:b0187e5db99f": { + "kind": "Free", + "file": "app/tests/tx_view.rs", + "item_path": "crate::add_tx_balance_array_at_index_zero#free:b0187e5db99f", + "comment_kind": "Line", + "comment_text": "// At index 0, the balance shows what was BEFORE the first tx (reverses it)", + "comment_hash": "b0187e5db99f2eda90fad59e9aa94559f88ec267d575b46aef7622096a7e9168", + "body_text": "let partial = Some(PartialTx {\n from_method: \"Cash\",\n to_method: \"\",\n tx_type: \"Expense\",\n amount: \"100.00\",\n });\nlet arr = tx_view\n .add_tx_balance_array(Some(0), partial, &mut db_conn)\n .unwrap();", + "body_hash": "05d83d7943444f89601178a633967876d686bf47ee88816434a63fd9ea3410f9" + }, + "app/tests/tx_view.rs|crate::add_tx_balance_array_projecting_new_tx#free:9f5971fffe5b": { + "kind": "Free", + "file": "app/tests/tx_view.rs", + "item_path": "crate::add_tx_balance_array_projecting_new_tx#free:9f5971fffe5b", + "comment_kind": "Line", + "comment_text": "// Project what adding a new expense of $100 would look like at index 1", + "comment_hash": "9f5971fffe5b67354b80a90eb4c68f895f1724175267d685ca0522aabf307cdf", + "body_text": "let partial = Some(PartialTx {\n from_method: \"Cash\",\n to_method: \"\",\n tx_type: \"Expense\",\n amount: \"100.00\",\n });\nlet arr = tx_view\n .add_tx_balance_array(Some(1), partial, &mut db_conn)\n .unwrap();", + "body_hash": "633570208e4e04ebffdf8f3ff024b44a7a4de5a18dfe97a9235c4e5724dcc01a" + }, + "app/tests/tx_view.rs|crate::add_tx_balance_array_projecting_new_tx#free:bbd969f307c6": { + "kind": "Free", + "file": "app/tests/tx_view.rs", + "item_path": "crate::add_tx_balance_array_projecting_new_tx#free:bbd969f307c6", + "comment_kind": "Line", + "comment_text": "// Balance row: existing 500 - projected 100 = 400", + "comment_hash": "bbd969f307c6a51f27781a3fa5597a6cb873f857410f3577d414b71faabbd9d5", + "body_text": "assert_eq!(get_col(&arr, 1, 0), \"Balance\");\nassert_eq!(get_col(&arr, 1, 1), \"400.00\");", + "body_hash": "2801984bf7e7cc33690fbf8cd78b4f78c943cab29cd7371cb3eba3ca55d89ea4" + }, + "app/tests/tx_view.rs|crate::add_tx_balance_array_projecting_new_tx#trailing:e03169e6b4c9": { + "kind": "Free", + "file": "app/tests/tx_view.rs", + "item_path": "crate::add_tx_balance_array_projecting_new_tx#trailing:e03169e6b4c9", + "comment_kind": "Line", + "comment_text": "// Cash\n// Changes row: should show projected change", + "comment_hash": "e03169e6b4c95a606c2a493c65cac5016435b316e86cf888ce772eb7c1dca246", + "body_text": "assert_eq!(get_col(&arr, 1, 1), \"400.00\");", + "body_hash": "392add2899de38bd0a19b63011f44f98ecac6aeef95c66b372f417d08d67bc06" + }, + "app/tests/tx_view.rs|crate::add_tx_balance_array_projection_transfer#free:9e3cbc261885": { + "kind": "Free", + "file": "app/tests/tx_view.rs", + "item_path": "crate::add_tx_balance_array_projection_transfer#free:9e3cbc261885", + "comment_kind": "Line", + "comment_text": "// Cash: 1000 - 300 = 700, Bank: 0 + 300 = 300", + "comment_hash": "9e3cbc2618850c30def04ade9c524844a21d25840389b35a8357b3c50ac2c949", + "body_text": "assert_eq!(get_col(&arr, 1, 1), \"700.00\");", + "body_hash": "90837d2ae59c0a6cf6418117da9ab253034c4d517fbdf2a9e7b37ea2de2bf428" + }, + "app/tests/tx_view.rs|crate::add_tx_balance_array_projection_transfer#free:a087bc7e0ca4": { + "kind": "Free", + "file": "app/tests/tx_view.rs", + "item_path": "crate::add_tx_balance_array_projection_transfer#free:a087bc7e0ca4", + "comment_kind": "Line", + "comment_text": "// Project adding a transfer of $300 from Cash to Bank", + "comment_hash": "a087bc7e0ca4a0017f752af09e8f072dc229a53678a27c156507d7dc4f0685ad", + "body_text": "let partial = Some(PartialTx {\n from_method: \"Cash\",\n to_method: \"Bank\",\n tx_type: \"Transfer\",\n amount: \"300.00\",\n });\nlet arr = tx_view\n .add_tx_balance_array(Some(1), partial, &mut db_conn)\n .unwrap();", + "body_hash": "ef81c7eea8784c8e431381a07ce264d59618fc921cbf8da8ee137cd4973f4ac5" + }, + "app/tests/tx_view.rs|crate::add_tx_balance_array_projection_transfer#trailing:61746a61eb54": { + "kind": "Free", + "file": "app/tests/tx_view.rs", + "item_path": "crate::add_tx_balance_array_projection_transfer#trailing:61746a61eb54", + "comment_kind": "Line", + "comment_text": "// Bank", + "comment_hash": "61746a61eb54d2060ea03dece1a37f1c79d181e0f07552527d59de906d2c613a", + "body_text": "assert_eq!(get_col(&arr, 1, 2), \"300.00\");", + "body_hash": "33736a3691223b5df9f4d898f2747fc317675087daba6dd7ad28d09b1b2ba4da" + }, + "app/tests/tx_view.rs|crate::add_tx_balance_array_projection_transfer#trailing:a40831e20cd7": { + "kind": "Free", + "file": "app/tests/tx_view.rs", + "item_path": "crate::add_tx_balance_array_projection_transfer#trailing:a40831e20cd7", + "comment_kind": "Line", + "comment_text": "// Cash", + "comment_hash": "a40831e20cd7c3957739f62e2a7c768017051d048b18d11ed47a9efa0a098990", + "body_text": "assert_eq!(get_col(&arr, 1, 1), \"700.00\");", + "body_hash": "90837d2ae59c0a6cf6418117da9ab253034c4d517fbdf2a9e7b37ea2de2bf428" + }, + "app/tests/tx_view.rs|crate::balance_array_daily_income_expense#free:0bb853021c8b": { + "kind": "Free", + "file": "app/tests/tx_view.rs", + "item_path": "crate::balance_array_daily_income_expense#free:0bb853021c8b", + "comment_kind": "Line", + "comment_text": "// At index 1 (second tx of Aug 1): daily income = 100+50 = 150", + "comment_hash": "0bb853021c8b91926c1c2a0868db812674da2dd7e58d9a65cc20909420109abd", + "body_text": "let arr = tx_view.balance_array(Some(1), &mut db_conn).unwrap();\nassert_eq!(get_col(&arr, 5, 0), \"Daily Income\");\nassert_eq!(get_col(&arr, 5, 1), \"150.00\");", + "body_hash": "962a8dafa6247250e27adaa7bf610f213e345dabe34d7528dd56c84c41c72969" + }, + "app/tests/tx_view.rs|crate::balance_array_daily_income_expense#free:9c03fb047604": { + "kind": "Free", + "file": "app/tests/tx_view.rs", + "item_path": "crate::balance_array_daily_income_expense#free:9c03fb047604", + "comment_kind": "Line", + "comment_text": "// Same date: two incomes", + "comment_hash": "9c03fb047604d714bcaad5754914d81d532966e22bd2008db19797116eaccabe", + "body_text": "add_tx(\n &mut db_conn,\n \"2024-08-01\",\n \"Morning\",\n \"Cash\",\n \"\",\n \"100.00\",\n \"Income\",\n \"A\",\n );\nadd_tx(\n &mut db_conn,\n \"2024-08-01\",\n \"Afternoon\",\n \"Cash\",\n \"\",\n \"50.00\",\n \"Income\",\n \"B\",\n );\nadd_tx(\n &mut db_conn,\n \"2024-08-02\",\n \"Next day\",\n \"Cash\",\n \"\",\n \"200.00\",\n \"Income\",\n \"C\",\n );\nlet date = NaiveDate::from_ymd_opt(2024, 8, 1).unwrap();\nlet tx_view = db_conn\n .fetch_txs_with_date(date, FetchNature::Monthly)\n .unwrap();", + "body_hash": "c7e740ad4bf79a3a59a24bc5570f117c32a5174b176ce1c4512b9c9ba4386366" + }, + "app/tests/tx_view.rs|crate::balance_array_daily_income_expense#trailing:43c55510adc0": { + "kind": "Free", + "file": "app/tests/tx_view.rs", + "item_path": "crate::balance_array_daily_income_expense#trailing:43c55510adc0", + "comment_kind": "Line", + "comment_text": "// 100+50 from same date\n// At index 2 (Aug 2): daily income = only the Aug 2 tx = 200", + "comment_hash": "43c55510adc034f42c2fe28df69de65c67b0dc20959d48f43ffcd22fe079ec7b", + "body_text": "assert_eq!(get_col(&arr, 5, 1), \"150.00\");", + "body_hash": "056ac9860a5831591f5ae223298bcb235bc97794c752662c0b1ba62abf4a914f" + }, + "app/tests/tx_view.rs|crate::balance_array_daily_income_expense#trailing:69337dce9dca": { + "kind": "Free", + "file": "app/tests/tx_view.rs", + "item_path": "crate::balance_array_daily_income_expense#trailing:69337dce9dca", + "comment_kind": "Line", + "comment_text": "// only the Aug 2 tx", + "comment_hash": "69337dce9dcad5b30d0c5de3035897935d065b2c3df813777223ff308c2288f3", + "body_text": "assert_eq!(get_col(&arr, 5, 1), \"200.00\");", + "body_hash": "8feaac2f4627e74e4884cf17815be015f74695cc73d88bc5c2867236e0e20bfd" + }, + "app/tests/tx_view.rs|crate::balance_array_with_index_shows_running_balance#free:17452458262a": { + "kind": "Free", + "file": "app/tests/tx_view.rs", + "item_path": "crate::balance_array_with_index_shows_running_balance#free:17452458262a", + "comment_kind": "Line", + "comment_text": "// At index 1: after Expense -200, running balance = 800", + "comment_hash": "17452458262a5d874c094301b4fef31cd42c5b0e8fce4ff96d5e2801cf5b43bd", + "body_text": "let arr = tx_view.balance_array(Some(1), &mut db_conn).unwrap();\nassert_eq!(get_col(&arr, 1, 1), \"800.00\");", + "body_hash": "0ea208ef1f6ec69a79213181e4df41313ef0d23b2c4b4f0880aa710c330e008f" + }, + "app/tests/tx_view.rs|crate::balance_array_with_index_shows_running_balance#free:5ea6f9a14327": { + "kind": "Free", + "file": "app/tests/tx_view.rs", + "item_path": "crate::balance_array_with_index_shows_running_balance#free:5ea6f9a14327", + "comment_kind": "Line", + "comment_text": "// Balance row: [\"Balance\", \"1000.00\", \"0.00\", \"0.00\", \"1000.00\"]", + "comment_hash": "5ea6f9a14327a03c551fa20d8576bad0cd9cfd636bcc898ff6d883daf0c955a9", + "body_text": "assert_eq!(get_col(&arr, 1, 0), \"Balance\");\nassert_eq!(get_col(&arr, 1, 1), \"1000.00\");", + "body_hash": "4f8f97c4934df32ccddb82e4265cacb8156b820febe57a18f678887fc6eaf1c9" + }, + "app/tests/tx_view.rs|crate::balance_array_with_index_shows_running_balance#free:67e843d0043e": { + "kind": "Free", + "file": "app/tests/tx_view.rs", + "item_path": "crate::balance_array_with_index_shows_running_balance#free:67e843d0043e", + "comment_kind": "Line", + "comment_text": "// Header row: [\"\", \"Cash\", \"Bank\", \"Other\", \"Total\"]", + "comment_hash": "67e843d0043ec9d710eeac297c6e861ace5f33d9af9fa5123bfdc5c0d5ca97c5", + "body_text": "assert_eq!(get_col(&arr, 0, 1), \"Cash\");\nassert_eq!(get_col(&arr, 0, 4), \"Total\");", + "body_hash": "f4b1b08dfc2164fe3f62f2e3245c5fde80b16b299ac9ea58b484414ea044644e" + }, + "app/tests/tx_view.rs|crate::balance_array_with_index_shows_running_balance#free:a45f806e06e0": { + "kind": "Free", + "file": "app/tests/tx_view.rs", + "item_path": "crate::balance_array_with_index_shows_running_balance#free:a45f806e06e0", + "comment_kind": "Line", + "comment_text": "// At index 0: running balance after first tx (Income +1000)", + "comment_hash": "a45f806e06e014a4cc31f25ee302dbad9ad04471387693690d8fca88d9e3b7c5", + "body_text": "let arr = tx_view.balance_array(Some(0), &mut db_conn).unwrap();", + "body_hash": "4e89b1ad2ad5672bfd8e29b9fd5e592803c68030bd4074cc37d562df947b5dda" + }, + "app/tests/tx_view.rs|crate::balance_array_with_index_shows_running_balance#free:b1f5919fda53": { + "kind": "Free", + "file": "app/tests/tx_view.rs", + "item_path": "crate::balance_array_with_index_shows_running_balance#free:b1f5919fda53", + "comment_kind": "Line", + "comment_text": "// Expense row: cumulative expense up to this tx", + "comment_hash": "b1f5919fda531045faf973c46e873343c25a4fb46d0f201e305e22c756bd2f88", + "body_text": "assert_eq!(get_col(&arr, 4, 0), \"Expense\");\nassert_eq!(get_col(&arr, 4, 1), \"0.00\");", + "body_hash": "856e3c6c91a000f3612ccc801b7e019ed622eed4164918d7ff3f02413dbb5214" + }, + "app/tests/tx_view.rs|crate::balance_array_with_index_shows_running_balance#free:d017b70faabb": { + "kind": "Free", + "file": "app/tests/tx_view.rs", + "item_path": "crate::balance_array_with_index_shows_running_balance#free:d017b70faabb", + "comment_kind": "Line", + "comment_text": "// Income row: cumulative income up to this tx", + "comment_hash": "d017b70faabb7e9b84787ea2d5b6589c0a5a1e5c43bda859d2d80448eb796d7e", + "body_text": "assert_eq!(get_col(&arr, 3, 0), \"Income\");\nassert_eq!(get_col(&arr, 3, 1), \"1000.00\");", + "body_hash": "5c632053e91be98fc52c14d5f2d3f3b15e6da30169ccedd6773056f9ead78450" + }, + "app/tests/tx_view.rs|crate::balance_array_with_index_shows_running_balance#trailing:05194459c358": { + "kind": "Free", + "file": "app/tests/tx_view.rs", + "item_path": "crate::balance_array_with_index_shows_running_balance#trailing:05194459c358", + "comment_kind": "Line", + "comment_text": "// Total\n// Changes row for this tx: \"↑1000.00\"", + "comment_hash": "05194459c35892877aab25db15ae74c31433cba9f10a6e762c32490de3a3b13d", + "body_text": "assert_eq!(get_col(&arr, 1, 4), \"1000.00\");", + "body_hash": "d00eeb49b315877625da6330fec7f0227d909189702f3a010826ebb79e4d623b" + }, + "app/tests/tx_view.rs|crate::balance_array_with_index_shows_running_balance#trailing:a40831e20cd7": { + "kind": "Free", + "file": "app/tests/tx_view.rs", + "item_path": "crate::balance_array_with_index_shows_running_balance#trailing:a40831e20cd7", + "comment_kind": "Line", + "comment_text": "// Cash", + "comment_hash": "a40831e20cd7c3957739f62e2a7c768017051d048b18d11ed47a9efa0a098990", + "body_text": "assert_eq!(get_col(&arr, 1, 1), \"1000.00\");", + "body_hash": "e060f5822ce57974acabcbdc026211486d1f6d00b98fd08d477aa929b2926892" + }, + "app/tests/tx_view.rs|crate::balance_array_with_index_shows_running_balance#trailing:c9326a5398c2": { + "kind": "Free", + "file": "app/tests/tx_view.rs", + "item_path": "crate::balance_array_with_index_shows_running_balance#trailing:c9326a5398c2", + "comment_kind": "Line", + "comment_text": "// Cash balance", + "comment_hash": "c9326a5398c2eddb7da8943cbd4cd10384c128ba6a69dd02e1759da28fa86ab1", + "body_text": "assert_eq!(get_col(&arr, 1, 1), \"800.00\");", + "body_hash": "ff8f488d0b99609772fecf737e1cb9adc417b585fdcd3c6ca293f8b514293a76" + }, + "app/tests/tx_view.rs|crate::balance_array_with_index_shows_running_balance#trailing:cffafaab3ae8": { + "kind": "Free", + "file": "app/tests/tx_view.rs", + "item_path": "crate::balance_array_with_index_shows_running_balance#trailing:cffafaab3ae8", + "comment_kind": "Line", + "comment_text": "// Total", + "comment_hash": "cffafaab3ae8ad8790e55d0d2cbc3af317ab02493ab6817f313ffc8c96ba5115", + "body_text": "assert_eq!(get_col(&arr, 1, 4), \"800.00\");", + "body_hash": "34b6832d74c466dd355225c724034ac638e603c2474249b78af4fb00a90c0532" + }, + "app/tests/tx_view.rs|crate::balance_array_with_none_shows_final_balance#free:a3d169a4001d": { + "kind": "Free", + "file": "app/tests/tx_view.rs", + "item_path": "crate::balance_array_with_none_shows_final_balance#free:a3d169a4001d", + "comment_kind": "Line", + "comment_text": "// None → final balance (from Balance::get_final_balance)", + "comment_hash": "a3d169a4001db10a2bfbe572f407b69d2a517449428fc16ff2e44b3800755f88", + "body_text": "let arr = tx_view.balance_array(None, &mut db_conn).unwrap();", + "body_hash": "fa25875a8ca69002256310c452a33e7b6f383dd9b52e5949733d48921b3fc89a" + }, + "app/tests/tx_view.rs|crate::balance_array_with_none_shows_final_balance#free:d07ebfc1a118": { + "kind": "Free", + "file": "app/tests/tx_view.rs", + "item_path": "crate::balance_array_with_none_shows_final_balance#free:d07ebfc1a118", + "comment_kind": "Line", + "comment_text": "// Cash: 500 - 200 = 300, Bank: 200", + "comment_hash": "d07ebfc1a118fee4027d82715b2936d356f065157ea860a694939041ab4c13df", + "body_text": "assert_eq!(get_col(&arr, 1, 1), \"300.00\");", + "body_hash": "508aa02ef97dc0ce8d4c2df1766f2fcd2940f4bb05ba5eb018bf257104ad5e75" + }, + "app/tests/tx_view.rs|crate::balance_array_with_none_shows_final_balance#trailing:61746a61eb54": { + "kind": "Free", + "file": "app/tests/tx_view.rs", + "item_path": "crate::balance_array_with_none_shows_final_balance#trailing:61746a61eb54", + "comment_kind": "Line", + "comment_text": "// Bank", + "comment_hash": "61746a61eb54d2060ea03dece1a37f1c79d181e0f07552527d59de906d2c613a", + "body_text": "assert_eq!(get_col(&arr, 1, 2), \"200.00\");", + "body_hash": "184302feca6403b330c8e5ce96da221a077c2cb98009e8eee106d9f72e82b0c5" + }, + "app/tests/tx_view.rs|crate::balance_array_with_none_shows_final_balance#trailing:648c89aa580c": { + "kind": "Free", + "file": "app/tests/tx_view.rs", + "item_path": "crate::balance_array_with_none_shows_final_balance#trailing:648c89aa580c", + "comment_kind": "Line", + "comment_text": "// Total\n// Changes should all be 0.00 (no specific tx)", + "comment_hash": "648c89aa580cc61dbe84521794bb4b4ff76a92fc61324ba60aefc836249f7d5f", + "body_text": "assert_eq!(get_col(&arr, 1, 4), \"500.00\");", + "body_hash": "11b3b08eaf6c580c827c59c6afe514c33b6f15f3819b29fc1ddd8b26d08f6787" + }, + "app/tests/tx_view.rs|crate::balance_array_with_none_shows_final_balance#trailing:a40831e20cd7": { + "kind": "Free", + "file": "app/tests/tx_view.rs", + "item_path": "crate::balance_array_with_none_shows_final_balance#trailing:a40831e20cd7", + "comment_kind": "Line", + "comment_text": "// Cash", + "comment_hash": "a40831e20cd7c3957739f62e2a7c768017051d048b18d11ed47a9efa0a098990", + "body_text": "assert_eq!(get_col(&arr, 1, 1), \"300.00\");", + "body_hash": "508aa02ef97dc0ce8d4c2df1766f2fcd2940f4bb05ba5eb018bf257104ad5e75" + }, + "app/tests/verifier.rs|crate#free:11739f096a49": { + "kind": "Free", + "file": "app/tests/verifier.rs", + "item_path": "crate#free:11739f096a49", + "comment_kind": "Line", + "comment_text": "// ---- Recurring: Recur Value verification ----", + "comment_hash": "11739f096a49e4cadf2a180dedace32ee07054b120fc3622241563e14837816e", + "body_text": "#[test]\nfn verify_recur_value_empty() {\n let file_name = \"test_verify_recur_value_empty.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = String::new();\n let v = db_conn.verify();\n let result = v.recur_value(&mut s, RecurrenceFrequency::Monthly).unwrap();\n assert!(matches!(result, Output::Nothing(_)));\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn verify_recur_value_daily_accepts_anything_non_empty() {\n let file_name = \"test_verify_recur_value_daily.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"irrelevant\".to_string();\n let v = db_conn.verify();\n let result = v.recur_value(&mut s, RecurrenceFrequency::Daily).unwrap();\n assert!(matches!(result, Output::Accepted(_)));\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn verify_recur_value_weekly_exact_match() {\n let file_name = \"test_verify_recur_value_weekly_exact.sqlite\";\n let mut db_conn = create_test_db(file_name);\n\n for day in [\n \"Sunday\",\n \"Monday\",\n \"Tuesday\",\n \"Wednesday\",\n \"Thursday\",\n \"Friday\",\n \"Saturday\",\n ] {\n let mut s = day.to_string();\n let v = db_conn.verify();\n let result = v.recur_value(&mut s, RecurrenceFrequency::Weekly).unwrap();\n assert!(matches!(result, Output::Accepted(_)));\n assert_eq!(s, day);\n }\n\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn verify_recur_value_weekly_fuzzy_correction() {\n let file_name = \"test_verify_recur_value_weekly_fuzzy.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"Mondey\".to_string();\n let v = db_conn.verify();\n let result = v.recur_value(&mut s, RecurrenceFrequency::Weekly);\n assert!(matches!(\n result,\n Err(VerifierError::InvalidRecurValueWeekly)\n ));\n assert_eq!(s, \"Monday\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn verify_recur_value_monthly_valid_range() {\n let file_name = \"test_verify_recur_value_monthly_valid.sqlite\";\n let mut db_conn = create_test_db(file_name);\n\n for day in [\"1\", \"15\", \"31\"] {\n let mut s = day.to_string();\n let v = db_conn.verify();\n let result = v.recur_value(&mut s, RecurrenceFrequency::Monthly).unwrap();\n assert!(matches!(result, Output::Accepted(_)));\n assert_eq!(s, day);\n }\n\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn verify_recur_value_monthly_zero_is_rejected() {\n let file_name = \"test_verify_recur_value_monthly_zero.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"0\".to_string();\n let v = db_conn.verify();\n let result = v.recur_value(&mut s, RecurrenceFrequency::Monthly);\n assert!(matches!(\n result,\n Err(VerifierError::InvalidRecurValueMonthly)\n ));\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn verify_recur_value_monthly_above_31_is_rejected() {\n let file_name = \"test_verify_recur_value_monthly_32.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"32\".to_string();\n let v = db_conn.verify();\n let result = v.recur_value(&mut s, RecurrenceFrequency::Monthly);\n assert!(matches!(\n result,\n Err(VerifierError::InvalidRecurValueMonthly)\n ));\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn verify_recur_value_yearly_uses_same_monthly_range() {\n let file_name = \"test_verify_recur_value_yearly.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"29\".to_string();\n let v = db_conn.verify();\n let result = v.recur_value(&mut s, RecurrenceFrequency::Yearly).unwrap();\n assert!(matches!(result, Output::Accepted(_)));\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn verify_recur_value_monthly_non_numeric_errors() {\n let file_name = \"test_verify_recur_value_monthly_non_numeric.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"abc\".to_string();\n let v = db_conn.verify();\n let result = v.recur_value(&mut s, RecurrenceFrequency::Monthly);\n assert!(result.is_err());\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}", + "body_hash": "a856b658b72b485ac40618ab267e3e1daaa3b06150ae7e8b71d9693c955b2fac" + }, + "app/tests/verifier.rs|crate#free:17e578d92344": { + "kind": "Free", + "file": "app/tests/verifier.rs", + "item_path": "crate#free:17e578d92344", + "comment_kind": "Line", + "comment_text": "// ---- Missing branches from coverage ----", + "comment_hash": "17e578d92344d269ac501d9b16e7b5d5152da77607380b2b33b54ac45a218c96", + "body_text": "#[test]\nfn verify_date_monthly_wrong_parts() {\n let file_name = \"test_verify_date_monthly_parts.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"2024\".to_string();\n let result = db_conn.verify().date(&mut s, DateType::Monthly);\n assert!(matches!(result, Err(VerifierError::InvalidDate)));\n assert_eq!(s, \"2022-01\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn verify_date_monthly_month_too_short() {\n let file_name = \"test_verify_date_monthly_short.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"2024-6\".to_string();\n let result = db_conn.verify().date(&mut s, DateType::Monthly);\n assert!(matches!(result, Err(VerifierError::InvalidMonth)));\n assert_eq!(s, \"2024-06\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn verify_date_monthly_month_too_big() {\n let file_name = \"test_verify_date_monthly_big.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"2024-13\".to_string();\n let result = db_conn.verify().date(&mut s, DateType::Monthly);\n assert!(matches!(result, Err(VerifierError::MonthTooBig)));\n assert_eq!(s, \"2024-12\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn verify_date_day_too_short() {\n let file_name = \"test_verify_date_day_short.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"2024-06-5\".to_string();\n let result = db_conn.verify().date(&mut s, DateType::Exact);\n assert!(matches!(result, Err(VerifierError::InvalidDay)));\n assert_eq!(s, \"2024-06-05\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn verify_amount_decimal_truncated() {\n let file_name = \"test_verify_amount_trunc.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"10.123\".to_string();\n let v = db_conn.verify();\n v.amount(&mut s).unwrap();\n assert_eq!(s, \"10.12\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn verify_amount_integer_limits_to_10_chars() {\n let file_name = \"test_verify_amount_limit.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"12345678901.50\".to_string();\n let v = db_conn.verify();\n v.amount(&mut s).unwrap();\n assert_eq!(s, \"1234567890.50\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}", + "body_hash": "ddb7512c68286286f2b918eb46b729238e8111657435e03df0b321965fd44782" + }, + "app/tests/verifier.rs|crate#free:1ce48a7ee6ec": { + "kind": "Free", + "file": "app/tests/verifier.rs", + "item_path": "crate#free:1ce48a7ee6ec", + "comment_kind": "Line", + "comment_text": "// ---- Date verification ----", + "comment_hash": "1ce48a7ee6ece8ba76b3e1708cd27877aa913ec46de8f27c2e720d6ab4240b1e", + "body_text": "#[test]\nfn verify_date_exact_valid() {\n let file_name = \"test_verify_date.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"2024-06-15\".to_string();\n let result = db_conn.verify().date(&mut s, DateType::Exact).unwrap();\n assert!(matches!(result, Output::Accepted(_)));\n assert_eq!(s, \"2024-06-15\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn verify_date_empty_returns_nothing() {\n let file_name = \"test_verify_date_empty.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = String::new();\n let result = db_conn.verify().date(&mut s, DateType::Exact).unwrap();\n assert!(matches!(result, Output::Nothing(_)));\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn verify_date_year_too_short_is_corrected() {\n let file_name = \"test_verify_date_year_short.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"24-06-15\".to_string();\n let result = db_conn.verify().date(&mut s, DateType::Exact);\n assert!(matches!(result, Err(VerifierError::InvalidYear)));\n assert_eq!(s, \"2022-06-15\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn verify_date_year_too_long_is_truncated() {\n let file_name = \"test_verify_date_year_long.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"20245-06-15\".to_string();\n let result = db_conn.verify().date(&mut s, DateType::Exact);\n assert!(matches!(result, Err(VerifierError::InvalidYear)));\n assert_eq!(s, \"2024-06-15\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn verify_date_month_out_of_range_is_capped() {\n let file_name = \"test_verify_date_month_big.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"2024-13-15\".to_string();\n let result = db_conn.verify().date(&mut s, DateType::Exact);\n assert!(matches!(result, Err(VerifierError::MonthTooBig)));\n assert_eq!(s, \"2024-12-15\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn verify_date_day_out_of_range_is_capped() {\n let file_name = \"test_verify_date_day_big.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"2024-06-32\".to_string();\n let result = db_conn.verify().date(&mut s, DateType::Exact);\n assert!(matches!(result, Err(VerifierError::DayTooBig)));\n assert_eq!(s, \"2024-06-31\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn verify_date_nonexistent_fails() {\n let file_name = \"test_verify_date_nonexistent.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"2024-02-30\".to_string();\n let result = db_conn.verify().date(&mut s, DateType::Exact);\n assert!(matches!(result, Err(VerifierError::NonExistingDate)));\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn verify_date_monthly_valid() {\n let file_name = \"test_verify_date_monthly.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"2024-06\".to_string();\n let result = db_conn.verify().date(&mut s, DateType::Monthly).unwrap();\n assert!(matches!(result, Output::Accepted(_)));\n assert_eq!(s, \"2024-06\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn verify_date_yearly_valid() {\n let file_name = \"test_verify_date_yearly.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"2024\".to_string();\n let result = db_conn.verify().date(&mut s, DateType::Yearly).unwrap();\n assert!(matches!(result, Output::Accepted(_)));\n assert_eq!(s, \"2024\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn verify_date_wrong_part_count_is_corrected() {\n let file_name = \"test_verify_date_wrong_parts.sqlite\";\n let mut db_conn = create_test_db(file_name);\n\n \n let mut s = \"2024-06\".to_string();\n let result = db_conn.verify().date(&mut s, DateType::Exact);\n assert!(matches!(result, Err(VerifierError::InvalidDate)));\n assert_eq!(s, \"2022-01-01\");\n\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}", + "body_hash": "3e1c74cfdc84ed32b098ef00e2747b64e80b8e06c31724515deb485de2ac05c3" + }, + "app/tests/verifier.rs|crate#free:27e2eeb75577": { + "kind": "Free", + "file": "app/tests/verifier.rs", + "item_path": "crate#free:27e2eeb75577", + "comment_kind": "Line", + "comment_text": "// ---- Tx Type verification ----", + "comment_hash": "27e2eeb75577d9b9da4b9c19a03a1c0ba0ce91584e230ba70126c3c2917064a5", + "body_text": "#[test]\nfn verify_tx_type_empty() {\n let file_name = \"test_verify_type_empty.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = String::new();\n let v = db_conn.verify();\n let result = v.tx_type(&mut s).unwrap();\n assert!(matches!(result, Output::Nothing(_)));\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn verify_tx_type_shortcuts() {\n let file_name = \"test_verify_type_short.sqlite\";\n let mut db_conn = create_test_db(file_name);\n\n let cases = [\n (\"e\", \"Expense\"),\n (\"E\", \"Expense\"),\n (\"i\", \"Income\"),\n (\"I\", \"Income\"),\n (\"t\", \"Transfer\"),\n (\"T\", \"Transfer\"),\n (\"b\", \"Borrow\"),\n (\"B\", \"Borrow\"),\n (\"l\", \"Lend\"),\n (\"L\", \"Lend\"),\n (\"br\", \"Borrow Repay\"),\n (\"BR\", \"Borrow Repay\"),\n (\"lr\", \"Lend Repay\"),\n (\"LR\", \"Lend Repay\"),\n ];\n\n for (input, expected) in cases {\n let mut s = input.to_string();\n let v = db_conn.verify();\n let result = v.tx_type(&mut s).unwrap();\n assert!(\n matches!(result, Output::Accepted(_)),\n \"Expected Accepted for input '{input}'\"\n );\n assert_eq!(s, expected, \"Mismatch for input '{input}'\");\n }\n\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn verify_tx_type_exact_match() {\n let file_name = \"test_verify_type_exact.sqlite\";\n let mut db_conn = create_test_db(file_name);\n\n for expected in [\n \"Income\",\n \"Expense\",\n \"Transfer\",\n \"Borrow\",\n \"Lend\",\n \"Borrow Repay\",\n \"Lend Repay\",\n ] {\n let mut s = expected.to_string();\n let v = db_conn.verify();\n let result = v.tx_type(&mut s).unwrap();\n assert!(matches!(result, Output::Accepted(_)));\n assert_eq!(s, expected);\n }\n\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn verify_tx_type_fuzzy_correction() {\n let file_name = \"test_verify_type_fuzzy.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"Incom\".to_string();\n let v = db_conn.verify();\n let result = v.tx_type(&mut s);\n assert!(matches!(result, Err(VerifierError::InvalidTxType)));\n assert_eq!(s, \"Income\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}", + "body_hash": "9fc15caeb16416da81584191ca05f1d392a12e4e3db1b0ef196b9eed94105566" + }, + "app/tests/verifier.rs|crate#free:282c3d2cdef3": { + "kind": "Free", + "file": "app/tests/verifier.rs", + "item_path": "crate#free:282c3d2cdef3", + "comment_kind": "Line", + "comment_text": "// ---- Tx Method verification ----", + "comment_hash": "282c3d2cdef38f0193cf05176182cc94eebd5c6f2d92c4bd2a5748d974a2c571", + "body_text": "#[test]\nfn verify_tx_method_exact_match() {\n let file_name = \"test_verify_method.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"Cash\".to_string();\n let v = db_conn.verify();\n let result = v.tx_method(&mut s).unwrap();\n assert!(matches!(result, Output::Accepted(_)));\n assert_eq!(s, \"Cash\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn verify_tx_method_case_insensitive() {\n let file_name = \"test_verify_method_case.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"cash\".to_string();\n let v = db_conn.verify();\n let result = v.tx_method(&mut s).unwrap();\n assert!(matches!(result, Output::Accepted(_)));\n assert_eq!(s, \"Cash\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn verify_tx_method_empty() {\n let file_name = \"test_verify_method_empty.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = String::new();\n let v = db_conn.verify();\n let result = v.tx_method(&mut s).unwrap();\n assert!(matches!(result, Output::Nothing(_)));\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn verify_tx_method_fuzzy_correction() {\n let file_name = \"test_verify_method_fuzzy.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"Csh\".to_string();\n let v = db_conn.verify();\n let result = v.tx_method(&mut s);\n assert!(matches!(result, Err(VerifierError::InvalidTxMethod)));\n assert_eq!(s, \"Cash\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}", + "body_hash": "f2e5e2d286409529ef4d4f47cb9c97575d34b68ab831d7b3c4db1bc4159cba30" + }, + "app/tests/verifier.rs|crate#free:331517c1a8f0": { + "kind": "Free", + "file": "app/tests/verifier.rs", + "item_path": "crate#free:331517c1a8f0", + "comment_kind": "Line", + "comment_text": "// ---- Tags verification ----", + "comment_hash": "331517c1a8f04e3d62ccbb3044606c095a57a41aec75be24313cec28b9769874", + "body_text": "#[test]\nfn verify_tags_dedup_removes_duplicates() {\n let file_name = \"test_verify_tags_dedup.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"A, A, B\".to_string();\n let v = db_conn.verify();\n v.tags(&mut s);\n assert_eq!(s, \"A, B\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn verify_tags_empty_is_unchanged() {\n let file_name = \"test_verify_tags_empty.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = String::new();\n let v = db_conn.verify();\n v.tags(&mut s);\n assert_eq!(s, \"\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn verify_tags_trims_whitespace() {\n let file_name = \"test_verify_tags_trim.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \" Tag1 , Tag2 , Tag3 \".to_string();\n let v = db_conn.verify();\n v.tags(&mut s);\n assert_eq!(s, \"Tag1, Tag2, Tag3\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}", + "body_hash": "0fc34b217d41d24bc0070e8713a3d89d3478449fe3768dfb986636cb0ad4b47e" + }, + "app/tests/verifier.rs|crate#free:504d461137c1": { + "kind": "Free", + "file": "app/tests/verifier.rs", + "item_path": "crate#free:504d461137c1", + "comment_kind": "Line", + "comment_text": "// ---- Tags forced verification ----", + "comment_hash": "504d461137c11a9f3d4c9f9ff009c90ace2e4bdbcf92a55d74db877db8e6b567", + "body_text": "#[test]\nfn verify_tags_forced_all_existing() {\n let file_name = \"test_verify_tags_forced.sqlite\";\n let mut db_conn = create_test_db(file_name);\n \n use crate::common::add_tx;\n add_tx(\n &mut db_conn,\n \"2024-01-01\",\n \"Test\",\n \"Cash\",\n \"\",\n \"10.00\",\n \"Expense\",\n \"ExistingTag, AnotherTag\",\n );\n\n let mut s = \"ExistingTag, AnotherTag\".to_string();\n let v = db_conn.verify();\n let result = v.tags_forced(&mut s).unwrap();\n assert!(matches!(result, Output::Accepted(_)));\n assert_eq!(s, \"ExistingTag, AnotherTag\");\n\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn verify_tags_forced_nonexistent_filtered() {\n let file_name = \"test_verify_tags_forced_filter.sqlite\";\n let mut db_conn = create_test_db(file_name);\n use crate::common::add_tx;\n add_tx(\n &mut db_conn,\n \"2024-02-01\",\n \"Test\",\n \"Cash\",\n \"\",\n \"10.00\",\n \"Expense\",\n \"RealTag\",\n );\n\n let mut s = \"RealTag, FakeTag\".to_string();\n let v = db_conn.verify();\n let result = v.tags_forced(&mut s);\n assert!(matches!(result, Err(VerifierError::NonExistingTag)));\n assert_eq!(s, \"RealTag\");\n\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn verify_tags_forced_empty() {\n let file_name = \"test_verify_tags_forced_empty.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = String::new();\n let v = db_conn.verify();\n let result = v.tags_forced(&mut s).unwrap();\n assert!(matches!(result, Output::Nothing(_)));\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}", + "body_hash": "853aeae2c91457d1867a0afe2c97b29424fb4a3f6a71877f90741ff9efa614ff" + }, + "app/tests/verifier.rs|crate#free:50ea385c2326": { + "kind": "Free", + "file": "app/tests/verifier.rs", + "item_path": "crate#free:50ea385c2326", + "comment_kind": "Line", + "comment_text": "// ---- Verifier error paths ----", + "comment_hash": "50ea385c2326c40b17c1564ba5deaadfc10fda1e0730688e54b32788cd12fd11", + "body_text": "#[test]\nfn verify_date_parsing_error() {\n let file_name = \"test_verify_date_parse_err.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"abc-def-ghi\".to_string();\n let result = db_conn.verify().date(&mut s, DateType::Exact);\n assert!(matches!(result, Err(VerifierError::ParsingError(_))));\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn verify_amount_only_symbols_becomes_zero() {\n let file_name = \"test_verify_amount_parse_err.sqlite\";\n let mut db_conn = create_test_db(file_name);\n \n let mut s = \"+\".to_string();\n let v = db_conn.verify();\n let result = v.amount(&mut s);\n assert!(matches!(result, Err(VerifierError::AmountBelowZero)));\n assert_eq!(s, \"0.00\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn verify_tx_method_not_found_fuzzy_corrects() {\n let file_name = \"test_verify_method_err.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"NonExistent\".to_string();\n let v = db_conn.verify();\n let result = v.tx_method(&mut s);\n assert!(matches!(result, Err(VerifierError::InvalidTxMethod)));\n \n assert!(!s.is_empty());\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn verify_tx_type_long_invalid_fuzzy_corrects() {\n let file_name = \"test_verify_type_long_err.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"SomethingWeird\".to_string();\n let v = db_conn.verify();\n let result = v.tx_type(&mut s);\n assert!(matches!(result, Err(VerifierError::InvalidTxType)));\n \n assert!(!s.is_empty());\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn verify_tx_type_short_invalid_fuzzy_corrects() {\n let file_name = \"test_verify_type_short_err.sqlite\";\n let mut db_conn = create_test_db(file_name);\n \n let mut s = \"x\".to_string();\n let v = db_conn.verify();\n let result = v.tx_type(&mut s);\n assert!(matches!(result, Err(VerifierError::InvalidTxType)));\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}", + "body_hash": "d2837057bf864f47e73d45a965372feed6c9d392b063f68f247fe8f771d07d89" + }, + "app/tests/verifier.rs|crate#free:8f573e6a83a0": { + "kind": "Free", + "file": "app/tests/verifier.rs", + "item_path": "crate#free:8f573e6a83a0", + "comment_kind": "Line", + "comment_text": "// ---- Recurring: Recur Month verification ----", + "comment_hash": "8f573e6a83a0288e09a003b7b195f31b15d59f3dd24911a2805586f23934615a", + "body_text": "#[test]\nfn verify_recur_month_empty() {\n let file_name = \"test_verify_recur_month_empty.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = String::new();\n let v = db_conn.verify();\n let result = v.recur_month(&mut s).unwrap();\n assert!(matches!(result, Output::Nothing(_)));\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn verify_recur_month_exact_match() {\n let file_name = \"test_verify_recur_month_exact.sqlite\";\n let mut db_conn = create_test_db(file_name);\n\n for month in [\"January\", \"June\", \"December\"] {\n let mut s = month.to_string();\n let v = db_conn.verify();\n let result = v.recur_month(&mut s).unwrap();\n assert!(matches!(result, Output::Accepted(_)));\n assert_eq!(s, month);\n }\n\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn verify_recur_month_fuzzy_correction() {\n let file_name = \"test_verify_recur_month_fuzzy.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"Januarz\".to_string();\n let v = db_conn.verify();\n let result = v.recur_month(&mut s);\n assert!(matches!(result, Err(VerifierError::InvalidRecurMonth)));\n assert_eq!(s, \"January\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}", + "body_hash": "79a6b99deec8d16421a1a8071f1e03213c8023088de548fe074891ac3d935083" + }, + "app/tests/verifier.rs|crate#free:e4eddf5b18e7": { + "kind": "Free", + "file": "app/tests/verifier.rs", + "item_path": "crate#free:e4eddf5b18e7", + "comment_kind": "Line", + "comment_text": "// ---- Amount verification ----", + "comment_hash": "e4eddf5b18e79d9d7f8b7071c8773ffba13c49a8a88f4d7555d95e6aadbaa685", + "body_text": "#[test]\nfn verify_amount_valid() {\n let file_name = \"test_verify_amount.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"42.50\".to_string();\n let v = db_conn.verify();\n let result = v.amount(&mut s).unwrap();\n assert!(matches!(result, Output::Accepted(_)));\n assert_eq!(s, \"42.50\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn verify_amount_empty_returns_nothing() {\n let file_name = \"test_verify_amount_empty.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = String::new();\n let v = db_conn.verify();\n let result = v.amount(&mut s).unwrap();\n assert!(matches!(result, Output::Nothing(_)));\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn verify_amount_integer_gets_decimals() {\n let file_name = \"test_verify_amount_integer.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"10\".to_string();\n let v = db_conn.verify();\n v.amount(&mut s).unwrap();\n assert_eq!(s, \"10.00\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn verify_amount_one_decimal_gets_padded() {\n let file_name = \"test_verify_amount_one_dec.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"10.5\".to_string();\n let v = db_conn.verify();\n v.amount(&mut s).unwrap();\n assert_eq!(s, \"10.50\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn verify_amount_negative_becomes_positive_via_calc() {\n let file_name = \"test_verify_amount_negative.sqlite\";\n let mut db_conn = create_test_db(file_name);\n \n \n let mut s = \"-5.00\".to_string();\n let v = db_conn.verify();\n v.amount(&mut s).unwrap();\n assert_eq!(s, \"5.00\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn verify_amount_zero_is_rejected() {\n let file_name = \"test_verify_amount_zero.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"0\".to_string();\n let v = db_conn.verify();\n let result = v.amount(&mut s);\n assert!(matches!(result, Err(VerifierError::AmountBelowZero)));\n assert_eq!(s, \"0.00\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn verify_amount_calculation_multiplication() {\n let file_name = \"test_verify_amount_calc.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"1+2*3\".to_string();\n let v = db_conn.verify();\n v.amount(&mut s).unwrap();\n assert_eq!(s, \"7.00\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn verify_amount_calculation_division() {\n let file_name = \"test_verify_amount_calc_div.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"10/2\".to_string();\n let v = db_conn.verify();\n v.amount(&mut s).unwrap();\n assert_eq!(s, \"5.00\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn verify_amount_calculation_complex() {\n let file_name = \"test_verify_amount_calc_complex.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"10+5*2-3\".to_string();\n let v = db_conn.verify();\n v.amount(&mut s).unwrap();\n assert_eq!(s, \"17.00\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn verify_amount_non_numeric_chars_stripped() {\n let file_name = \"test_verify_amount_strip.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \" $ 1,234.56 \".to_string();\n let v = db_conn.verify();\n v.amount(&mut s).unwrap();\n assert_eq!(s, \"1234.56\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}", + "body_hash": "c79a5ce6dfbdb3e880e9a74b5e77a355db1fd5e6329506dd44697b3fd9593241" + }, + "app/tests/verifier.rs|crate#free:f986352af924": { + "kind": "Free", + "file": "app/tests/verifier.rs", + "item_path": "crate#free:f986352af924", + "comment_kind": "Line", + "comment_text": "// ---- Recurring: Frequency verification ----", + "comment_hash": "f986352af924717123c9a6c158d4ecdd4073e2e5fab4de322dfd77492ae556d4", + "body_text": "#[test]\nfn verify_frequency_empty() {\n let file_name = \"test_verify_frequency_empty.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = String::new();\n let v = db_conn.verify();\n let result = v.frequency(&mut s).unwrap();\n assert!(matches!(result, Output::Nothing(_)));\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn verify_frequency_shortcuts() {\n let file_name = \"test_verify_frequency_short.sqlite\";\n let mut db_conn = create_test_db(file_name);\n\n let cases = [\n (\"d\", \"Daily\"),\n (\"D\", \"Daily\"),\n (\"w\", \"Weekly\"),\n (\"W\", \"Weekly\"),\n (\"m\", \"Monthly\"),\n (\"M\", \"Monthly\"),\n (\"y\", \"Yearly\"),\n (\"Y\", \"Yearly\"),\n ];\n\n for (input, expected) in cases {\n let mut s = input.to_string();\n let v = db_conn.verify();\n v.frequency(&mut s).unwrap();\n assert_eq!(s, expected);\n }\n\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn verify_frequency_exact_match() {\n let file_name = \"test_verify_frequency_exact.sqlite\";\n let mut db_conn = create_test_db(file_name);\n\n for expected in [\"Daily\", \"Weekly\", \"Monthly\", \"Yearly\"] {\n let mut s = expected.to_string();\n let v = db_conn.verify();\n let result = v.frequency(&mut s).unwrap();\n assert!(matches!(result, Output::Accepted(_)));\n assert_eq!(s, expected);\n }\n\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn verify_frequency_fuzzy_correction() {\n let file_name = \"test_verify_frequency_fuzzy.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"Dailz\".to_string();\n let v = db_conn.verify();\n let result = v.frequency(&mut s);\n assert!(matches!(result, Err(VerifierError::InvalidFrequency)));\n assert_eq!(s, \"Daily\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn verify_frequency_short_invalid_fuzzy_corrects() {\n let file_name = \"test_verify_frequency_short_err.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"x\".to_string();\n let v = db_conn.verify();\n let result = v.frequency(&mut s);\n assert!(matches!(result, Err(VerifierError::InvalidFrequency)));\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}", + "body_hash": "4f7d6161af41455dc04e678173e9774a240c6188ff3de84edf634f0375fec230" + }, + "app/tests/verifier.rs|crate#free:fe9dd884b85d": { + "kind": "Free", + "file": "app/tests/verifier.rs", + "item_path": "crate#free:fe9dd884b85d", + "comment_kind": "Line", + "comment_text": "// ---- Recurring: Recur Interval verification ----", + "comment_hash": "fe9dd884b85d5e9f2b6f5542e3bd07145da8028385fe3935bc864519eab6f6ea", + "body_text": "#[test]\nfn verify_recur_interval_empty() {\n let file_name = \"test_verify_recur_interval_empty.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = String::new();\n let v = db_conn.verify();\n let result = v.recur_interval(&mut s).unwrap();\n assert!(matches!(result, Output::Nothing(_)));\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn verify_recur_interval_valid() {\n let file_name = \"test_verify_recur_interval_valid.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"5\".to_string();\n let v = db_conn.verify();\n let result = v.recur_interval(&mut s).unwrap();\n assert!(matches!(result, Output::Accepted(_)));\n assert_eq!(s, \"5\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn verify_recur_interval_strips_non_digits() {\n let file_name = \"test_verify_recur_interval_strip.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"5a\".to_string();\n let v = db_conn.verify();\n v.recur_interval(&mut s).unwrap();\n assert_eq!(s, \"5\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn verify_recur_interval_zero_is_rejected() {\n let file_name = \"test_verify_recur_interval_zero.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"0\".to_string();\n let v = db_conn.verify();\n let result = v.recur_interval(&mut s);\n assert!(matches!(result, Err(VerifierError::InvalidRecurInterval)));\n assert_eq!(s, \"1\");\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}\n#[test]\nfn verify_recur_interval_non_numeric_errors() {\n let file_name = \"test_verify_recur_interval_non_numeric.sqlite\";\n let mut db_conn = create_test_db(file_name);\n let mut s = \"abc\".to_string();\n let v = db_conn.verify();\n let result = v.recur_interval(&mut s);\n assert!(result.is_err());\n drop(db_conn);\n fs::remove_file(file_name).unwrap();\n}", + "body_hash": "4ed818b8d9fb0f0684d0b951da5a4d762f6fdada614f91f947060a7923bf5a8f" + }, + "app/tests/verifier.rs|crate::verify_amount_negative_becomes_positive_via_calc#free:828a6e45b7e7": { + "kind": "Free", + "file": "app/tests/verifier.rs", + "item_path": "crate::verify_amount_negative_becomes_positive_via_calc#free:828a6e45b7e7", + "comment_kind": "Line", + "comment_text": "// '-' is a calc operator, so -5.00 goes through calc path:\n// no left operand → resolves to just the right operand = 5.00", + "comment_hash": "828a6e45b7e7d01e3644646528c2bf81fd7cddef78537b516f5c143562067222", + "body_text": "let mut s = \"-5.00\".to_string();\nlet v = db_conn.verify();\nv.amount(&mut s).unwrap();\nassert_eq!(s, \"5.00\");\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "8a995d1cc7e81459304de5e44adfdd0bb2d312713c8045326490e93446968128" + }, + "app/tests/verifier.rs|crate::verify_amount_only_symbols_becomes_zero#free:d1770389e61c": { + "kind": "Free", + "file": "app/tests/verifier.rs", + "item_path": "crate::verify_amount_only_symbols_becomes_zero#free:d1770389e61c", + "comment_kind": "Line", + "comment_text": "// \"+\" is a calc symbol; empty operands → result is \"\" → \".00\" → 0.00 → AmountBelowZero", + "comment_hash": "d1770389e61c9faff64d8cc513536638975055faac59b0a231ac62ec5be2ae2f", + "body_text": "let mut s = \"+\".to_string();\nlet v = db_conn.verify();\nlet result = v.amount(&mut s);\nassert!(matches!(result, Err(VerifierError::AmountBelowZero)));\nassert_eq!(s, \"0.00\");\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "cf02b0371cecea1657927a5e39666f6afce961c41094991308e106bc689e8bee" + }, + "app/tests/verifier.rs|crate::verify_date_wrong_part_count_is_corrected#free:a5e5676a9fe2": { + "kind": "Free", + "file": "app/tests/verifier.rs", + "item_path": "crate::verify_date_wrong_part_count_is_corrected#free:a5e5676a9fe2", + "comment_kind": "Line", + "comment_text": "// Exact expects 3 parts, monthly expects 2", + "comment_hash": "a5e5676a9fe23dcf1d31b004f9751349c8fcb0c4a8b2b76eff17a3a16878be2e", + "body_text": "let mut s = \"2024-06\".to_string();\nlet result = db_conn.verify().date(&mut s, DateType::Exact);\nassert!(matches!(result, Err(VerifierError::InvalidDate)));\nassert_eq!(s, \"2022-01-01\");\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "49aa6a38bd115d7275ca584fbd2220bac5ca55b409a1d9656c29a0d23a5c2f2e" + }, + "app/tests/verifier.rs|crate::verify_tags_forced_all_existing#free:58e37e0e2f39": { + "kind": "Free", + "file": "app/tests/verifier.rs", + "item_path": "crate::verify_tags_forced_all_existing#free:58e37e0e2f39", + "comment_kind": "Line", + "comment_text": "// Tags must exist in DB. Add one via a transaction.", + "comment_hash": "58e37e0e2f39d1f87e2574779291ab12303ea8afca4f76f0fe4c5c7b82cf9d0e", + "body_text": "use crate::common::add_tx;\nadd_tx(\n &mut db_conn,\n \"2024-01-01\",\n \"Test\",\n \"Cash\",\n \"\",\n \"10.00\",\n \"Expense\",\n \"ExistingTag, AnotherTag\",\n );\nlet mut s = \"ExistingTag, AnotherTag\".to_string();\nlet v = db_conn.verify();\nlet result = v.tags_forced(&mut s).unwrap();\nassert!(matches!(result, Output::Accepted(_)));\nassert_eq!(s, \"ExistingTag, AnotherTag\");\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "4a0883fb9ab81e10529365f3f4851a896353bb785b3da10eee34e61609ee7c93" + }, + "app/tests/verifier.rs|crate::verify_tx_method_not_found_fuzzy_corrects#free:6d34886a5328": { + "kind": "Free", + "file": "app/tests/verifier.rs", + "item_path": "crate::verify_tx_method_not_found_fuzzy_corrects#free:6d34886a5328", + "comment_kind": "Line", + "comment_text": "// Fuzzy corrects to closest match among Cash/Bank/Other", + "comment_hash": "6d34886a532888eec3a99610e3b78f022361780768b48d3d8639e5cbb862c94f", + "body_text": "assert!(!s.is_empty());\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "c6be4c1cb2a07eae117d77e4d65b4ac9632eec0bc2b327896ba1b18ec21db163" + }, + "app/tests/verifier.rs|crate::verify_tx_type_long_invalid_fuzzy_corrects#free:d6c5727e5a6a": { + "kind": "Free", + "file": "app/tests/verifier.rs", + "item_path": "crate::verify_tx_type_long_invalid_fuzzy_corrects#free:d6c5727e5a6a", + "comment_kind": "Line", + "comment_text": "// Gets fuzzy-corrected to closest match", + "comment_hash": "d6c5727e5a6a81f73bd9fc3be8cd05f3c8c2c4f6033c6f750f8149ad9e8f7c91", + "body_text": "assert!(!s.is_empty());\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "c6be4c1cb2a07eae117d77e4d65b4ac9632eec0bc2b327896ba1b18ec21db163" + }, + "app/tests/verifier.rs|crate::verify_tx_type_short_invalid_fuzzy_corrects#free:b68147835a87": { + "kind": "Free", + "file": "app/tests/verifier.rs", + "item_path": "crate::verify_tx_type_short_invalid_fuzzy_corrects#free:b68147835a87", + "comment_kind": "Line", + "comment_text": "// Short (<2 chars) but not E/I/T/B/L/BR/LR", + "comment_hash": "b68147835a879cb9b245c89919a6a8535c56b2bb2e8d4272426732a981d63ec8", + "body_text": "let mut s = \"x\".to_string();\nlet v = db_conn.verify();\nlet result = v.tx_type(&mut s);\nassert!(matches!(result, Err(VerifierError::InvalidTxType)));\ndrop(db_conn);\nfs::remove_file(file_name).unwrap();\n}", + "body_hash": "25de05662893e827788dc54a95438a3183aad3944ba6e1c172619050f3e21220" + }, + "db/src/models/balances.rs|crate::impl Balance::get_last_balance": { + "kind": "Function", + "file": "db/src/models/balances.rs", + "item_path": "crate::impl Balance::get_last_balance", + "comment_kind": "OuterDoc", + "comment_text": " The last non-final balance of every method before the given date\n", + "comment_hash": "80d28553c23fafd20011f18fcc28f8646b4026ea5499488ad19a2a270a5f5d50", + "body_text": "pub fn get_last_balance(\n date: NaiveDate,\n nature: FetchNature,\n db_conn: &mut impl ConnCache,\n ) -> Result, Error> {\n use crate::schema::balances::dsl::{balances, is_final_balance, method_id, month, year};\n\n let mut ongoing_date = date;\n\n if let FetchNature::Yearly = nature {\n ongoing_date = NaiveDate::from_ymd_opt(date.year(), 1, 1).unwrap();\n }\n\n let mut found_method_balances = HashMap::new();\n\n let mut pending_balance_tx_methods = HashSet::new();\n\n \n \n if let FetchNature::All = nature {\n let mut to_return = HashMap::new();\n\n for key in db_conn.cache().tx_methods.keys().copied() {\n to_return.insert(key, Cent::new(0));\n }\n\n return Ok(to_return);\n }\n\n for key in db_conn.cache().tx_methods.keys().copied() {\n pending_balance_tx_methods.insert(key);\n }\n\n for _ in 0..3 {\n ongoing_date = ongoing_date - Months::new(1);\n\n let date_year = ongoing_date.year();\n let date_month = ongoing_date.month() as i32;\n\n let Some(last_balances) = balances\n .filter(year.eq(date_year))\n .filter(month.eq(date_month))\n .filter(method_id.eq_any(&pending_balance_tx_methods))\n .filter(is_final_balance.eq(false))\n .select(Self::as_select())\n .load(db_conn.conn())\n .optional()?\n else {\n continue;\n };\n\n for bal in last_balances {\n if found_method_balances.contains_key(&bal.method_id) {\n continue;\n }\n\n found_method_balances.insert(bal.method_id, Cent::new(bal.balance));\n pending_balance_tx_methods.remove(&bal.method_id);\n }\n }\n\n \n \n let date = date - Months::new(1);\n\n if !pending_balance_tx_methods.is_empty() {\n for mid in pending_balance_tx_methods {\n if let Some(last_balance) = balances\n .filter(method_id.eq(mid))\n .filter(is_final_balance.eq(false))\n .filter(\n year.lt(date.year())\n .or(year.eq(date.year()).and(month.lt(date.month() as i32))),\n )\n .order((year.desc(), month.desc()))\n .select(Self::as_select())\n .first::(db_conn.conn())\n .optional()?\n {\n found_method_balances.insert(mid, Cent::new(last_balance.balance));\n }\n }\n }\n\n if found_method_balances.len() != db_conn.cache().tx_methods.len() {\n for method in db_conn.cache().tx_methods.keys() {\n if !found_method_balances.contains_key(method) {\n found_method_balances.insert(*method, Cent::new(0));\n }\n }\n }\n\n Ok(found_method_balances)\n }", + "body_hash": "8b04cfe3509c373fbf551033813e1a100120c1c6e575e3a2599f5918c3bc54d1" + }, + "db/src/models/balances.rs|crate::impl Balance::get_last_balance#free:914a0dda9731": { + "kind": "Free", + "file": "db/src/models/balances.rs", + "item_path": "crate::impl Balance::get_last_balance#free:914a0dda9731", + "comment_kind": "Line", + "comment_text": "// Fallback: any method the loop above could not resolve gets its most\n// recent balance recorded before the previous month", + "comment_hash": "914a0dda973189878301f7f176b02bdc0bdf881f2d2462572f459082c202b7f1", + "body_text": "let date = date - Months::new(1);\nif !pending_balance_tx_methods.is_empty() {}\nif found_method_balances.len() != db_conn.cache().tx_methods.len() {}\nOk(found_method_balances)\n}", + "body_hash": "6303027d0a54a5df5af9a8be14a2e8dbc71673ff4b06ef73f9c09742bc503adb" + }, + "db/src/models/balances.rs|crate::impl Balance::get_last_balance#free:e2870b9e13a4": { + "kind": "Free", + "file": "db/src/models/balances.rs", + "item_path": "crate::impl Balance::get_last_balance#free:e2870b9e13a4", + "comment_kind": "Line", + "comment_text": "// \"All\" means every tx was fetched, so the balance before the very first\n// one is 0", + "comment_hash": "e2870b9e13a4635ab7dba551cc9c44c845902b38045f4d27a42b222cb0abc7f6", + "body_text": "if let FetchNature::All = nature {}\nfor key in db_conn.cache().tx_methods.keys().copied() {}\nfor _ in 0..3 {}", + "body_hash": "fc9b3fe267c2b3f4e97d7f0d681e4e9460bddffbbd193a7f1fb56fa3c5785ab2" + }, + "db/src/models/recurring_txs.rs|crate::impl RecurringTx::get_due": { + "kind": "Function", + "file": "db/src/models/recurring_txs.rs", + "item_path": "crate::impl RecurringTx::get_due", + "comment_kind": "OuterDoc", + "comment_text": " All non-paused rules whose next occurrence is already due\n", + "comment_hash": "15d6e98d8f566620422d7e42a07efa0bab11014f82347bac3049362c062ad046", + "body_text": "pub fn get_due(today: NaiveDate, db_conn: &mut impl ConnCache) -> Result, Error> {\n use crate::schema::recurring_txs::dsl::{is_paused, next_recurring_date, recurring_txs};\n\n recurring_txs\n .filter(is_paused.eq(false))\n .filter(next_recurring_date.le(today))\n .select(Self::as_select())\n .load(db_conn.conn())\n }", + "body_hash": "787c2ec8f831e3de78844ad9c2504bebb3b04439cf18b781f713e93b4423a779" + }, + "db/src/schema.rs|crate#free:f73cbadf7d16": { + "kind": "Free", + "file": "db/src/schema.rs", + "item_path": "crate#free:f73cbadf7d16", + "comment_kind": "Line", + "comment_text": "// @generated automatically by Diesel CLI.", + "comment_hash": "f73cbadf7d16ef3e0449359111ad35a1730b73c3253c31e4f882c92ded18ce81", + "body_text": "diesel::table! {\n activities (id) {\n id -> Integer,\n date -> Timestamp,\n activity_type -> Text,\n }\n}\ndiesel::table! {\n activity_tx_tags (tx_id, tag_id) {\n tx_id -> Integer,\n tag_id -> Integer,\n }\n}\ndiesel::table! {\n activity_txs (id) {\n id -> Integer,\n date -> Nullable,\n details -> Nullable,\n from_method -> Nullable,\n to_method -> Nullable,\n amount -> Nullable,\n amount_type -> Nullable,\n tx_type -> Nullable,\n display_order -> Nullable,\n activity_num -> Integer,\n }\n}\ndiesel::table! {\n balances (id) {\n id -> Integer,\n method_id -> Integer,\n year -> Integer,\n month -> Integer,\n balance -> BigInt,\n is_final_balance -> Bool,\n }\n}\ndiesel::table! {\n recurring_tx_tags (recurring_tx_id, tag_id) {\n recurring_tx_id -> Integer,\n tag_id -> Integer,\n is_primary -> Bool,\n }\n}\ndiesel::table! {\n recurring_txs (id) {\n id -> Integer,\n created_at -> Timestamp,\n details -> Nullable,\n from_method -> Integer,\n to_method -> Nullable,\n amount -> BigInt,\n tx_type -> Text,\n frequency -> Text,\n recur_interval -> Integer,\n recur_value -> Nullable,\n recur_month -> Nullable,\n last_recurred_date -> Nullable,\n next_recurring_date -> Date,\n end_date -> Nullable,\n is_paused -> Bool,\n }\n}\ndiesel::table! {\n tags (id) {\n id -> Integer,\n name -> Text,\n }\n}\ndiesel::table! {\n tx_methods (id) {\n id -> Integer,\n name -> Text,\n position -> Integer,\n }\n}\ndiesel::table! {\n tx_tags (tx_id, tag_id) {\n tx_id -> Integer,\n tag_id -> Integer,\n is_primary -> Bool,\n }\n}\ndiesel::table! {\n txs (id) {\n id -> Integer,\n date -> Timestamp,\n details -> Nullable,\n from_method -> Integer,\n to_method -> Nullable,\n amount -> BigInt,\n tx_type -> Text,\n display_order -> Integer,\n }\n}\ndiesel::joinable!(activity_tx_tags -> activity_txs (tx_id));\ndiesel::joinable!(activity_tx_tags -> tags (tag_id));\ndiesel::joinable!(activity_txs -> activities (activity_num));\ndiesel::joinable!(balances -> tx_methods (method_id));\ndiesel::joinable!(recurring_tx_tags -> recurring_txs (recurring_tx_id));\ndiesel::joinable!(recurring_tx_tags -> tags (tag_id));\ndiesel::joinable!(tx_tags -> tags (tag_id));\ndiesel::joinable!(tx_tags -> txs (tx_id));\ndiesel::allow_tables_to_appear_in_same_query!(\n activities,\n activity_tx_tags,\n activity_txs,\n balances,\n recurring_tx_tags,\n recurring_txs,\n tags,\n tx_methods,\n tx_tags,\n txs,\n);", + "body_hash": "6e5030148c353ff8baeaabe6bf8f4725e7228535a2fca3494f87b12d8ca079e2" + }, + "tui/src/key_checker/add_tx.rs|crate::add_tx_keys": { + "kind": "Function", + "file": "tui/src/key_checker/add_tx.rs", + "item_path": "crate::add_tx_keys", + "comment_kind": "OuterDoc", + "comment_text": " Tracks the keys of the Add Tx page and calls relevant function based on it\n", + "comment_hash": "db878b8656e29f8567d1a16a6dfb2b14da231194e61d500369b37db93fa96cb1", + "body_text": "pub fn add_tx_keys(handler: &mut InputKeyHandler) -> Result> {\n match handler.popup_status {\n \n PopupType::Nothing => match handler.add_tx_tab {\n TxTab::Nothing => match handler.key.code {\n KeyCode::Char('q') => return Ok(Some(HandlingOutput::QuitUi)),\n KeyCode::Char('f') => handler.go_home(),\n KeyCode::Char('r') => handler.go_chart(),\n KeyCode::Char('j') => handler.do_config_popup(),\n KeyCode::Char('z') => handler.go_summary()?,\n KeyCode::Char('h') => handler.do_help_popup(),\n KeyCode::Char('s') => handler.add_tx()?,\n KeyCode::Char('w') => handler.go_search(),\n KeyCode::Char('c') => handler.clear_input()?,\n KeyCode::Char('y') => handler.go_activity(),\n KeyCode::Char('u') => handler.go_recurring(),\n KeyCode::Char('t') => handler.next_theme()?,\n KeyCode::Enter => handler.select_date_field(),\n KeyCode::Char(c) if c.is_numeric() => {\n handler.handle_number_press();\n }\n _ => {}\n },\n _ => match handler.key.code {\n KeyCode::Right => handler.handle_right_arrow()?,\n KeyCode::Left => handler.handle_left_arrow()?,\n KeyCode::Up => handler.handle_up_arrow(),\n KeyCode::Down => handler.handle_down_arrow(),\n KeyCode::Tab => handler.do_autofill(),\n _ => match handler.add_tx_tab {\n TxTab::Date => handler.handle_date(),\n TxTab::Details => handler.handle_details(),\n TxTab::FromMethod | TxTab::ToMethod => handler.handle_tx_method()?,\n TxTab::Amount => handler.handle_amount()?,\n TxTab::TxType => handler.handle_tx_type()?,\n TxTab::Tags => handler.handle_tags(),\n TxTab::Nothing\n | TxTab::Frequency\n | TxTab::RecurInterval\n | TxTab::RecurValue\n | TxTab::RecurMonth\n | TxTab::EndDate => {}\n },\n },\n },\n _ => return popup_keys(handler),\n }\n\n Ok(None)\n}", + "body_hash": "4673eaba3fddc3abcdaa5a6db2abb719d57356a075b43bf1a08612adcb8e9994" + }, + "tui/src/key_checker/chart.rs|crate::chart_keys": { + "kind": "Function", + "file": "tui/src/key_checker/chart.rs", + "item_path": "crate::chart_keys", + "comment_kind": "OuterDoc", + "comment_text": " Tracks the keys of the Chart page and calls relevant function based on it\n", + "comment_hash": "5658c0f775955e077532f8b44bc75fbd81512256c3892bab9f509252b8cfe068", + "body_text": "pub fn chart_keys(handler: &mut InputKeyHandler) -> Result> {\n match handler.popup_status {\n PopupType::Nothing => match handler.key.code {\n KeyCode::Char('a') => handler.go_add_tx()?,\n KeyCode::Char('z') => handler.go_summary()?,\n KeyCode::Char('q') => return Ok(Some(HandlingOutput::QuitUi)),\n KeyCode::Char('f') => handler.go_home(),\n KeyCode::Char('h') => handler.do_help_popup(),\n KeyCode::Char('j') => handler.do_config_popup(),\n KeyCode::Char('r') => handler.do_chart_hidden_mode(),\n KeyCode::Char('R') => handler.do_chart_lgeneds(),\n KeyCode::Char('w') => handler.go_search(),\n KeyCode::Char('y') => handler.go_activity(),\n KeyCode::Char('u') => handler.go_recurring(),\n KeyCode::Char('t') => handler.next_theme()?,\n KeyCode::Right => handler.handle_right_arrow()?,\n KeyCode::Left => handler.handle_left_arrow()?,\n KeyCode::Up => handler.handle_up_arrow(),\n KeyCode::Down => handler.handle_down_arrow(),\n KeyCode::Char(' ') => handler.switch_chart_tx_method_activation()?,\n _ => {}\n },\n _ => return popup_keys(handler),\n }\n Ok(None)\n}", + "body_hash": "62fda1f31201f0fd5eaf1f2f4ddd3495a274282d2b017dc43a93d7ec1f2f782f" + }, + "tui/src/key_checker/home.rs|crate::home_keys": { + "kind": "Function", + "file": "tui/src/key_checker/home.rs", + "item_path": "crate::home_keys", + "comment_kind": "OuterDoc", + "comment_text": " Tracks the keys of the Homepage and calls relevant function based on it\n", + "comment_hash": "3dd962886c4f7cf87fc262fdee1d3e1c44ad6ac90dbc9e00fb3552a4ae38b6f4", + "body_text": "pub fn home_keys(handler: &mut InputKeyHandler) -> Result> {\n match handler.popup_status {\n PopupType::Nothing => match handler.key.code {\n KeyCode::Char('q') => return Ok(Some(HandlingOutput::QuitUi)),\n KeyCode::Char('a') => handler.go_add_tx()?,\n KeyCode::Char('r') => handler.go_chart(),\n KeyCode::Char('j') => handler.do_config_popup(),\n KeyCode::Char('h') => handler.do_help_popup(),\n KeyCode::Char('z') => handler.go_summary()?,\n KeyCode::Char('w') => handler.go_search(),\n KeyCode::Char('e') => handler.home_edit_tx()?,\n KeyCode::Char('d') => handler.do_deletion_popup(),\n KeyCode::Char('y') => handler.go_activity(),\n KeyCode::Char('u') => handler.go_recurring(),\n KeyCode::Char(',') => handler.switch_tx_position_up()?,\n KeyCode::Char('.') => handler.switch_tx_position_down()?,\n KeyCode::Char('v') => handler.show_home_tx_details(),\n KeyCode::Char('t') => handler.next_theme()?,\n KeyCode::Right => handler.handle_right_arrow()?,\n KeyCode::Left => handler.handle_left_arrow()?,\n KeyCode::Up => handler.handle_up_arrow(),\n KeyCode::Down => handler.handle_down_arrow(),\n _ => {}\n },\n _ => return popup_keys(handler),\n }\n Ok(None)\n}", + "body_hash": "09d3c0dfd4442cdc26a60b49fa3aaf8dd93fcf688b3bfae3435980bce2caa119" + }, + "tui/src/key_checker/initial.rs|crate::initial_keys": { + "kind": "Function", + "file": "tui/src/key_checker/initial.rs", + "item_path": "crate::initial_keys", + "comment_kind": "OuterDoc", + "comment_text": " Tracks the keys of the Initial page and calls relevant function based on it\n", + "comment_hash": "24fead6be00b24ed893609e4f4e96547f6847655559258306e71f65dbecb705a", + "body_text": "pub fn initial_keys(handler: &mut InputKeyHandler) -> Result> {\n if let PopupType::Nothing = handler.popup_status {\n match handler.key.code {\n KeyCode::Char('q') => return Ok(Some(HandlingOutput::QuitUi)),\n _ => handler.go_home(),\n }\n } else {\n if let KeyCode::Char('q') = handler.key.code {\n return Ok(Some(HandlingOutput::QuitUi));\n }\n return popup_keys(handler);\n }\n Ok(None)\n}", + "body_hash": "06ec9767d106630cee84e3a5e838f525dd044c5c59149fc91f597e0502f50c33" + }, + "tui/src/key_checker/key_handler.rs|crate::InputKeyHandler": { + "kind": "Struct", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::InputKeyHandler", + "comment_kind": "OuterDoc", + "comment_text": " Stores all the data that is required to handle\n\n every single possible keypress event from the\n\n entire app\n", + "comment_hash": "e280f27284cf338cbcf0141cc1ae119e5a3a2084f538dbd34be29601018e0a1c", + "body_text": "pub struct InputKeyHandler<'a> {\n pub key: KeyEvent,\n pub page: &'a mut CurrentUi,\n add_tx_balance: &'a mut Vec>,\n pub popup_status: &'a mut PopupType,\n pub add_tx_tab: &'a mut TxTab,\n chart_tab: &'a mut ChartTab,\n summary_tab: &'a mut SummaryTab,\n home_tab: &'a mut HomeTab,\n add_tx_data: &'a mut TxData,\n chart_view: &'a mut ChartView,\n summary_view: &'a mut SummaryView,\n full_summary: &'a mut FullSummary,\n home_table: &'a mut TableData,\n home_txs: &'a mut TxViewGroup,\n summary_table: &'a mut TableData,\n home_months: &'a mut IndexedData,\n home_years: &'a mut IndexedData,\n chart_months: &'a mut IndexedData,\n chart_years: &'a mut IndexedData,\n chart_modes: &'a mut IndexedData,\n chart_tx_methods: &'a mut IndexedData,\n summary_months: &'a mut IndexedData,\n summary_years: &'a mut IndexedData,\n summary_modes: &'a mut IndexedData,\n summary_sort: &'a mut SortingType,\n search_data: &'a mut TxData,\n search_date_type: &'a mut DateType,\n pub search_tab: &'a mut TxTab,\n search_table: &'a mut TableData,\n search_txs: &'a mut SearchView,\n activity_months: &'a mut IndexedData,\n activity_years: &'a mut IndexedData,\n activity_tab: &'a mut ActivityTab,\n activity_view: &'a mut ActivityView,\n activity_table: &'a mut TableData,\n recurring_data: &'a mut TxData,\n pub recurring_tab: &'a mut TxTab,\n recurring_table: &'a mut TableData,\n recurring_txs: &'a mut Vec,\n chart_hidden_mode: &'a mut bool,\n chart_hidden_legends: &'a mut bool,\n summary_hidden_mode: &'a mut bool,\n chart_activated_methods: &'a mut HashMap,\n lerp_state: &'a mut LerpState,\n config: &'a mut Config,\n theme: &'a mut Theme,\n conn: &'a mut DbConn,\n}", + "body_hash": "57872d8dcabd8f3f2c4186ebdfb0a97f7f65f773617f654c7f630403a89979bf" + }, + "tui/src/key_checker/key_handler.rs|crate::digit_to_tab_index": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::digit_to_tab_index", + "comment_kind": "OuterDoc", + "comment_text": " Maps a pressed digit to a 0-based field index: 1-9 -> 0-8 (the 1st-9th field), 0 -> 9\n\n (the 10th field).\n", + "comment_hash": "f4aef789286a1b60499bb722ebcc068e3974a0c73ddfea3d8d07091f9a8a2360", + "body_text": "fn digit_to_tab_index(digit: u32) -> usize {\n if digit == 0 { 9 } else { digit as usize - 1 }\n}", + "body_hash": "55afc874a87639bd38ec80afcef7eae8fcfb848bc45f01c899121e3a531967aa" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'_>::check_add_tx_amount": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'_>::check_add_tx_amount", + "comment_kind": "OuterDoc", + "comment_text": " Handle key inputs for the Amount field on the Add TX page\n", + "comment_hash": "f57d7a7fd4788a29705fa57aaeb2c5c01bc71ed3ecabedd9b5494dbcfa77abaa", + "body_text": "fn check_add_tx_amount(&mut self) -> Result<()> {\n match self.key.code {\n KeyCode::Enter => {\n let status = self.add_tx_data.check_amount(false, self.conn);\n\n match status {\n Ok(data) => {\n *self.add_tx_tab = TxTab::Tags;\n self.go_correct_index();\n self.reload_add_tx_balance_data()?;\n\n self.add_tx_data\n .add_tx_status(data.to_string(), LogType::Info);\n }\n Err(e) => {\n self.add_tx_data\n .add_tx_status(e.to_string(), LogType::Error);\n }\n }\n }\n KeyCode::Esc => {\n let status = self.add_tx_data.check_amount(false, self.conn);\n\n match status {\n Ok(data) => {\n *self.add_tx_tab = TxTab::Nothing;\n self.reload_add_tx_balance_data()?;\n\n self.add_tx_data\n .add_tx_status(data.to_string(), LogType::Info);\n }\n Err(e) => {\n self.add_tx_data\n .add_tx_status(e.to_string(), LogType::Error);\n }\n }\n }\n KeyCode::Backspace => self.add_tx_data.edit_amount(None),\n KeyCode::Char(a) => self.add_tx_data.edit_amount(Some(a)),\n _ => {}\n }\n\n Ok(())\n }", + "body_hash": "ee88f535fec5690d3239b37a453d66ebc626f87c00d67a2d07d1a93ebce6b25e" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'_>::check_add_tx_date": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'_>::check_add_tx_date", + "comment_kind": "OuterDoc", + "comment_text": " Handle key inputs for the Date field on the Add TX page\n", + "comment_hash": "5428ecb7f0b23e8dac2cfd7ac8af7b847c0d3f7f692a7fa97fd5638380a70e69", + "body_text": "fn check_add_tx_date(&mut self) {\n match self.key.code {\n KeyCode::Enter => {\n let status = self.add_tx_data.check_date(DateType::Exact, self.conn);\n\n match status {\n Ok(data) => {\n *self.add_tx_tab = TxTab::Details;\n self.go_correct_index();\n\n self.add_tx_data\n .add_tx_status(data.to_string(), LogType::Info);\n }\n Err(e) => {\n self.add_tx_data\n .add_tx_status(e.to_string(), LogType::Error);\n }\n }\n }\n KeyCode::Esc => {\n let status = self.add_tx_data.check_date(DateType::Exact, self.conn);\n\n match status {\n Ok(data) => {\n *self.add_tx_tab = TxTab::Nothing;\n\n self.add_tx_data\n .add_tx_status(data.to_string(), LogType::Info);\n }\n Err(e) => {\n self.add_tx_data\n .add_tx_status(e.to_string(), LogType::Error);\n }\n }\n }\n KeyCode::Backspace => self.add_tx_data.edit_date(None),\n KeyCode::Char(a) => self.add_tx_data.edit_date(Some(a)),\n _ => {}\n }\n }", + "body_hash": "60f917c77cd68cdc325cbb566337116e144f96c638aa1767b18731fc6a544ddb" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'_>::check_add_tx_details": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'_>::check_add_tx_details", + "comment_kind": "OuterDoc", + "comment_text": " Handle key inputs for the Details field on the Add TX page\n", + "comment_hash": "6fe3d8060c517d92aa07f9949ca62614cd0a79ca83b15d6c0cb73120a200f14e", + "body_text": "fn check_add_tx_details(&mut self) {\n match self.key.code {\n KeyCode::Enter => {\n *self.add_tx_tab = TxTab::TxType;\n self.go_correct_index();\n }\n KeyCode::Esc => *self.add_tx_tab = TxTab::Nothing,\n KeyCode::Backspace => self.add_tx_data.edit_details(None),\n KeyCode::Char(a) => self.add_tx_data.edit_details(Some(a)),\n _ => {}\n }\n }", + "body_hash": "ec7bf18d6daad5ee3d276759e52ffb0d9fec4462262de772fa3a6acb6820b7e6" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'_>::check_add_tx_from": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'_>::check_add_tx_from", + "comment_kind": "OuterDoc", + "comment_text": " Handle key inputs for the From Method field on the Add TX page\n", + "comment_hash": "5753c3264563b26298e93bdddaf6c91f04951bd73339dc2f6de9a66e2af2fece", + "body_text": "fn check_add_tx_from(&mut self) -> Result<()> {\n match self.key.code {\n KeyCode::Enter => {\n let status = self.add_tx_data.check_from_method(self.conn);\n\n match status {\n Ok(data) => {\n match self.add_tx_data.get_tx_type() {\n TxType::IncomeExpense => *self.add_tx_tab = TxTab::Amount,\n TxType::Transfer => *self.add_tx_tab = TxTab::ToMethod,\n }\n self.reload_add_tx_balance_data()?;\n self.go_correct_index();\n\n self.add_tx_data\n .add_tx_status(data.to_string(), LogType::Info);\n }\n\n Err(e) => {\n self.add_tx_data\n .add_tx_status(e.to_string(), LogType::Error);\n }\n }\n }\n KeyCode::Esc => {\n let status = self.add_tx_data.check_from_method(self.conn);\n\n match status {\n Ok(data) => {\n *self.add_tx_tab = TxTab::Nothing;\n self.reload_add_tx_balance_data()?;\n\n self.add_tx_data\n .add_tx_status(data.to_string(), LogType::Info);\n }\n\n Err(e) => {\n self.add_tx_data\n .add_tx_status(e.to_string(), LogType::Error);\n }\n }\n }\n KeyCode::Backspace => self.add_tx_data.edit_from_method(None),\n KeyCode::Char(a) => self.add_tx_data.edit_from_method(Some(a)),\n _ => {}\n }\n\n Ok(())\n }", + "body_hash": "955e784f96ed61e6733c0987b5206fb9e2ae61299f67b3dc515bae28906a7bcd" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'_>::check_add_tx_tags": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'_>::check_add_tx_tags", + "comment_kind": "OuterDoc", + "comment_text": " Handle key inputs for the Tag field on the Add TX page\n", + "comment_hash": "90d37e1cdd0d94e3da755ecb97b37635a355c3b9f05a1b3f37a31d37120249fb", + "body_text": "fn check_add_tx_tags(&mut self) {\n match self.key.code {\n KeyCode::Enter | KeyCode::Esc => {\n *self.add_tx_tab = TxTab::Nothing;\n self.add_tx_data.check_tags(self.conn);\n }\n KeyCode::Backspace => self.add_tx_data.edit_tags(None),\n KeyCode::Char(a) => self.add_tx_data.edit_tags(Some(a)),\n _ => {}\n }\n }", + "body_hash": "45a8cce931b9a331a329943ca3624515fb65e159511e81e149afcddaff61b7c0" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'_>::check_add_tx_to": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'_>::check_add_tx_to", + "comment_kind": "OuterDoc", + "comment_text": " Handle key inputs for the To Method field on the Add TX page\n", + "comment_hash": "81734a77cb8dd8d135b8f21c48b7ab44e6a7827293cafd7b86ef71947829c3cd", + "body_text": "fn check_add_tx_to(&mut self) -> Result<()> {\n match self.key.code {\n KeyCode::Enter => {\n let status = self.add_tx_data.check_to_method(self.conn);\n\n match status {\n Ok(data) => {\n *self.add_tx_tab = TxTab::Amount;\n self.go_correct_index();\n self.reload_add_tx_balance_data()?;\n\n self.add_tx_data\n .add_tx_status(data.to_string(), LogType::Info);\n }\n Err(e) => {\n self.add_tx_data\n .add_tx_status(e.to_string(), LogType::Error);\n }\n }\n }\n KeyCode::Esc => {\n let status = self.add_tx_data.check_to_method(self.conn);\n\n match status {\n Ok(data) => {\n *self.add_tx_tab = TxTab::Nothing;\n self.reload_add_tx_balance_data()?;\n\n self.add_tx_data\n .add_tx_status(data.to_string(), LogType::Info);\n }\n Err(e) => {\n self.add_tx_data\n .add_tx_status(e.to_string(), LogType::Error);\n }\n }\n }\n KeyCode::Backspace => self.add_tx_data.edit_to_method(None),\n KeyCode::Char(a) => self.add_tx_data.edit_to_method(Some(a)),\n _ => {}\n }\n\n Ok(())\n }", + "body_hash": "00919e119ce82c50948e62d12c983f6674db7f4d0e164bae6545043cb265bcdf" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'_>::check_add_tx_type": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'_>::check_add_tx_type", + "comment_kind": "OuterDoc", + "comment_text": " Handle key inputs for the TX Type field on the Add TX page\n", + "comment_hash": "615b53998a2c2840ea95f4899073e3a3498ef0da9b17ce679c79b823110d3c54", + "body_text": "fn check_add_tx_type(&mut self) -> Result<()> {\n match self.key.code {\n KeyCode::Enter => {\n let status = self.add_tx_data.check_tx_type(self.conn);\n\n match status {\n Ok(data) => {\n *self.add_tx_tab = TxTab::FromMethod;\n self.go_correct_index();\n self.reload_add_tx_balance_data()?;\n\n self.add_tx_data\n .add_tx_status(data.to_string(), LogType::Info);\n }\n Err(e) => {\n self.add_tx_data\n .add_tx_status(e.to_string(), LogType::Error);\n }\n }\n }\n KeyCode::Esc => {\n let status = self.add_tx_data.check_tx_type(self.conn);\n\n match status {\n Ok(data) => {\n *self.add_tx_tab = TxTab::Nothing;\n self.reload_add_tx_balance_data()?;\n\n self.add_tx_data\n .add_tx_status(data.to_string(), LogType::Info);\n }\n Err(e) => {\n self.add_tx_data\n .add_tx_status(e.to_string(), LogType::Error);\n }\n }\n }\n KeyCode::Backspace => self.add_tx_data.edit_tx_type(None),\n KeyCode::Char(a) => self.add_tx_data.edit_tx_type(Some(a)),\n _ => {}\n }\n\n Ok(())\n }", + "body_hash": "35232df89953a5979695b963f27669c0fede0476cf00ece89749b71b99ae12b5" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'_>::check_recurring_amount": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'_>::check_recurring_amount", + "comment_kind": "OuterDoc", + "comment_text": " Handle key inputs for the Amount field on the Recurring page\n", + "comment_hash": "34d0dd2c6d390abd5bc3576337d5d690766cfa9adf079199e5bfe51df34df70a", + "body_text": "fn check_recurring_amount(&mut self) -> Result<()> {\n match self.key.code {\n KeyCode::Enter => {\n let status = self.recurring_data.check_amount(false, self.conn);\n\n match status {\n Ok(data) => {\n *self.recurring_tab = TxTab::Frequency;\n self.go_correct_index();\n\n self.recurring_data\n .add_tx_status(data.to_string(), LogType::Info);\n }\n Err(e) => {\n self.recurring_data\n .add_tx_status(e.to_string(), LogType::Error);\n }\n }\n }\n KeyCode::Esc => {\n let status = self.recurring_data.check_amount(false, self.conn);\n\n match status {\n Ok(data) => {\n *self.recurring_tab = TxTab::Nothing;\n\n self.recurring_data\n .add_tx_status(data.to_string(), LogType::Info);\n }\n Err(e) => {\n self.recurring_data\n .add_tx_status(e.to_string(), LogType::Error);\n }\n }\n }\n KeyCode::Backspace => self.recurring_data.edit_amount(None),\n KeyCode::Char(a) => self.recurring_data.edit_amount(Some(a)),\n _ => {}\n }\n\n Ok(())\n }", + "body_hash": "4cea2a684b8ca80126409a1258c79952d3a05655f5e95ff7630ca10408b6e8e3" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'_>::check_recurring_date": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'_>::check_recurring_date", + "comment_kind": "OuterDoc", + "comment_text": " Handle key inputs for the Date field on the Recurring page\n", + "comment_hash": "1bf9c5ba9e7e7889e977d9586433ffbb3cb7bc494f5f0a46be36deef38ae0685", + "body_text": "fn check_recurring_date(&mut self) {\n match self.key.code {\n KeyCode::Enter => {\n let status = self.recurring_data.check_date(DateType::Exact, self.conn);\n\n match status {\n Ok(data) => {\n *self.recurring_tab = TxTab::Details;\n self.go_correct_index();\n\n self.recurring_data\n .add_tx_status(data.to_string(), LogType::Info);\n }\n Err(e) => {\n self.recurring_data\n .add_tx_status(e.to_string(), LogType::Error);\n }\n }\n }\n KeyCode::Esc => {\n let status = self.recurring_data.check_date(DateType::Exact, self.conn);\n\n match status {\n Ok(data) => {\n *self.recurring_tab = TxTab::Nothing;\n\n self.recurring_data\n .add_tx_status(data.to_string(), LogType::Info);\n }\n Err(e) => {\n self.recurring_data\n .add_tx_status(e.to_string(), LogType::Error);\n }\n }\n }\n KeyCode::Backspace => self.recurring_data.edit_date(None),\n KeyCode::Char(a) => self.recurring_data.edit_date(Some(a)),\n _ => {}\n }\n }", + "body_hash": "bea68b163a692f6f561067e9eaf975453f17c4fa30659d8683093472fa9ef2b8" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'_>::check_recurring_details": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'_>::check_recurring_details", + "comment_kind": "OuterDoc", + "comment_text": " Handle key inputs for the Details field on the Recurring page\n", + "comment_hash": "15c30759038c1cd6fc5d1396fd9bf347ffd627f3b2e00239911854616ecbf39e", + "body_text": "fn check_recurring_details(&mut self) {\n match self.key.code {\n KeyCode::Enter => {\n *self.recurring_tab = TxTab::TxType;\n self.go_correct_index();\n }\n KeyCode::Esc => *self.recurring_tab = TxTab::Nothing,\n KeyCode::Backspace => self.recurring_data.edit_details(None),\n KeyCode::Char(a) => self.recurring_data.edit_details(Some(a)),\n _ => {}\n }\n }", + "body_hash": "3bbcef6dde6456b715ddb35801843559ca501d5bfed1ad0b245d7eda49525bef" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'_>::check_recurring_end_date": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'_>::check_recurring_end_date", + "comment_kind": "OuterDoc", + "comment_text": " Handle key inputs for the End Date field on the Recurring page\n", + "comment_hash": "02181f304f7a89dc4f4f7fbedc880a8d94b2805dc7575df94d87a63d5d91fc7d", + "body_text": "fn check_recurring_end_date(&mut self) {\n match self.key.code {\n KeyCode::Enter => {\n let status = self.recurring_data.check_end_date(self.conn);\n\n match status {\n Ok(data) => {\n *self.recurring_tab = TxTab::Tags;\n self.go_correct_index();\n\n self.recurring_data\n .add_tx_status(data.to_string(), LogType::Info);\n }\n Err(e) => {\n self.recurring_data\n .add_tx_status(e.to_string(), LogType::Error);\n }\n }\n }\n KeyCode::Esc => {\n let status = self.recurring_data.check_end_date(self.conn);\n\n match status {\n Ok(data) => {\n *self.recurring_tab = TxTab::Nothing;\n\n self.recurring_data\n .add_tx_status(data.to_string(), LogType::Info);\n }\n Err(e) => {\n self.recurring_data\n .add_tx_status(e.to_string(), LogType::Error);\n }\n }\n }\n KeyCode::Backspace => self.recurring_data.edit_end_date(None),\n KeyCode::Char(a) => self.recurring_data.edit_end_date(Some(a)),\n _ => {}\n }\n }", + "body_hash": "ea20fe68703f2de19c52c91b8df9fc0fcfb6f6da44844ae8e19f84bf8f788e9c" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'_>::check_recurring_frequency": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'_>::check_recurring_frequency", + "comment_kind": "OuterDoc", + "comment_text": " Handle key inputs for the Frequency field on the Recurring page\n", + "comment_hash": "2c7fec11d1f9c7606cfdc8ac22b0e63ddca187b94cadf353d05983e1f02175a7", + "body_text": "fn check_recurring_frequency(&mut self) {\n match self.key.code {\n KeyCode::Enter => {\n let status = self.recurring_data.check_frequency(self.conn);\n\n match status {\n Ok(data) => {\n *self.recurring_tab = TxTab::RecurInterval;\n self.go_correct_index();\n\n self.recurring_data\n .add_tx_status(data.to_string(), LogType::Info);\n }\n Err(e) => {\n self.recurring_data\n .add_tx_status(e.to_string(), LogType::Error);\n }\n }\n }\n KeyCode::Esc => {\n let status = self.recurring_data.check_frequency(self.conn);\n\n match status {\n Ok(data) => {\n *self.recurring_tab = TxTab::Nothing;\n\n self.recurring_data\n .add_tx_status(data.to_string(), LogType::Info);\n }\n Err(e) => {\n self.recurring_data\n .add_tx_status(e.to_string(), LogType::Error);\n }\n }\n }\n KeyCode::Backspace => self.recurring_data.edit_frequency(None),\n KeyCode::Char(a) => self.recurring_data.edit_frequency(Some(a)),\n _ => {}\n }\n }", + "body_hash": "47c845405bff433225bfc671e2ef1e45e47ce42c49641702eb00709ce4b502ca" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'_>::check_recurring_from": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'_>::check_recurring_from", + "comment_kind": "OuterDoc", + "comment_text": " Handle key inputs for the From Method field on the Recurring page\n", + "comment_hash": "a548a11b4542d7519790e4de25e357f71abf0d5cf661f5e426ebd7d0b6a6180d", + "body_text": "fn check_recurring_from(&mut self) -> Result<()> {\n match self.key.code {\n KeyCode::Enter => {\n let status = self.recurring_data.check_from_method(self.conn);\n\n match status {\n Ok(data) => {\n match self.recurring_data.get_tx_type() {\n TxType::IncomeExpense => *self.recurring_tab = TxTab::Amount,\n TxType::Transfer => *self.recurring_tab = TxTab::ToMethod,\n }\n self.go_correct_index();\n\n self.recurring_data\n .add_tx_status(data.to_string(), LogType::Info);\n }\n Err(e) => {\n self.recurring_data\n .add_tx_status(e.to_string(), LogType::Error);\n }\n }\n }\n KeyCode::Esc => {\n let status = self.recurring_data.check_from_method(self.conn);\n\n match status {\n Ok(data) => {\n *self.recurring_tab = TxTab::Nothing;\n\n self.recurring_data\n .add_tx_status(data.to_string(), LogType::Info);\n }\n Err(e) => {\n self.recurring_data\n .add_tx_status(e.to_string(), LogType::Error);\n }\n }\n }\n KeyCode::Backspace => self.recurring_data.edit_from_method(None),\n KeyCode::Char(a) => self.recurring_data.edit_from_method(Some(a)),\n _ => {}\n }\n\n Ok(())\n }", + "body_hash": "479e929fedcf931ae7b9e9cd2662b5cd894cf763c72c0b632567bccf7872e643" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'_>::check_recurring_recur_interval": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'_>::check_recurring_recur_interval", + "comment_kind": "OuterDoc", + "comment_text": " Handle key inputs for the Recur Interval field on the Recurring page\n", + "comment_hash": "6f7f1b71c338ab7c526d60e159933ba39525146439d6960b61865e211c427c69", + "body_text": "fn check_recurring_recur_interval(&mut self) {\n match self.key.code {\n KeyCode::Enter => {\n let status = self.recurring_data.check_recur_interval(self.conn);\n\n match status {\n Ok(data) => {\n *self.recurring_tab = match self.recurring_data.get_frequency() {\n RecurrenceFrequency::Daily => TxTab::EndDate,\n RecurrenceFrequency::Weekly\n | RecurrenceFrequency::Monthly\n | RecurrenceFrequency::Yearly => TxTab::RecurValue,\n };\n self.go_correct_index();\n\n self.recurring_data\n .add_tx_status(data.to_string(), LogType::Info);\n }\n Err(e) => {\n self.recurring_data\n .add_tx_status(e.to_string(), LogType::Error);\n }\n }\n }\n KeyCode::Esc => {\n let status = self.recurring_data.check_recur_interval(self.conn);\n\n match status {\n Ok(data) => {\n *self.recurring_tab = TxTab::Nothing;\n\n self.recurring_data\n .add_tx_status(data.to_string(), LogType::Info);\n }\n Err(e) => {\n self.recurring_data\n .add_tx_status(e.to_string(), LogType::Error);\n }\n }\n }\n KeyCode::Backspace => self.recurring_data.edit_recur_interval(None),\n KeyCode::Char(a) => self.recurring_data.edit_recur_interval(Some(a)),\n _ => {}\n }\n }", + "body_hash": "145f8c0302b2971dc94220263ebcb1d6192d52b0345755035741fdf9744f9640" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'_>::check_recurring_recur_month": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'_>::check_recurring_recur_month", + "comment_kind": "OuterDoc", + "comment_text": " Handle key inputs for the Recur Month field on the Recurring page\n", + "comment_hash": "866731551d86155a196867d49c1108b2e99f4fcdcc5feea29a5bba5bf5a24cda", + "body_text": "fn check_recurring_recur_month(&mut self) {\n match self.key.code {\n KeyCode::Enter => {\n let status = self.recurring_data.check_recur_month(self.conn);\n\n match status {\n Ok(data) => {\n *self.recurring_tab = TxTab::EndDate;\n self.go_correct_index();\n\n self.recurring_data\n .add_tx_status(data.to_string(), LogType::Info);\n }\n Err(e) => {\n self.recurring_data\n .add_tx_status(e.to_string(), LogType::Error);\n }\n }\n }\n KeyCode::Esc => {\n let status = self.recurring_data.check_recur_month(self.conn);\n\n match status {\n Ok(data) => {\n *self.recurring_tab = TxTab::Nothing;\n\n self.recurring_data\n .add_tx_status(data.to_string(), LogType::Info);\n }\n Err(e) => {\n self.recurring_data\n .add_tx_status(e.to_string(), LogType::Error);\n }\n }\n }\n KeyCode::Backspace => self.recurring_data.edit_recur_month(None),\n KeyCode::Char(a) => self.recurring_data.edit_recur_month(Some(a)),\n _ => {}\n }\n }", + "body_hash": "424b7c519177fccb414364b744144d49c4ec41327af38138ec6a07cf80e1d853" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'_>::check_recurring_recur_value": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'_>::check_recurring_recur_value", + "comment_kind": "OuterDoc", + "comment_text": " Handle key inputs for the Recur Value field on the Recurring page\n", + "comment_hash": "ce5ec2e83f2acece0ee4ef46edbfbd423039ab036a2855e9b3720cc97d50e45f", + "body_text": "fn check_recurring_recur_value(&mut self) {\n match self.key.code {\n KeyCode::Enter => {\n let status = self.recurring_data.check_recur_value(self.conn);\n\n match status {\n Ok(data) => {\n *self.recurring_tab = match self.recurring_data.get_frequency() {\n RecurrenceFrequency::Yearly => TxTab::RecurMonth,\n RecurrenceFrequency::Daily\n | RecurrenceFrequency::Weekly\n | RecurrenceFrequency::Monthly => TxTab::EndDate,\n };\n self.go_correct_index();\n\n self.recurring_data\n .add_tx_status(data.to_string(), LogType::Info);\n }\n Err(e) => {\n self.recurring_data\n .add_tx_status(e.to_string(), LogType::Error);\n }\n }\n }\n KeyCode::Esc => {\n let status = self.recurring_data.check_recur_value(self.conn);\n\n match status {\n Ok(data) => {\n *self.recurring_tab = TxTab::Nothing;\n\n self.recurring_data\n .add_tx_status(data.to_string(), LogType::Info);\n }\n Err(e) => {\n self.recurring_data\n .add_tx_status(e.to_string(), LogType::Error);\n }\n }\n }\n KeyCode::Backspace => self.recurring_data.edit_recur_value(None),\n KeyCode::Char(a) => self.recurring_data.edit_recur_value(Some(a)),\n _ => {}\n }\n }", + "body_hash": "eb5b72d2f4781aef311a9b94ebf1d22183cc5ac632d5e969c6f84eba357146ab" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'_>::check_recurring_tags": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'_>::check_recurring_tags", + "comment_kind": "OuterDoc", + "comment_text": " Handle key inputs for the Tag field on the Recurring page\n", + "comment_hash": "6f1747e2eb2349d4578c411375f48b137761ca947f58218f4bf4b4bf9c453fc8", + "body_text": "fn check_recurring_tags(&mut self) {\n match self.key.code {\n KeyCode::Enter | KeyCode::Esc => {\n *self.recurring_tab = TxTab::Nothing;\n self.recurring_data.check_tags(self.conn);\n }\n KeyCode::Backspace => self.recurring_data.edit_tags(None),\n KeyCode::Char(a) => self.recurring_data.edit_tags(Some(a)),\n _ => {}\n }\n }", + "body_hash": "56be5ab3d4aab4f8843a4f095e559dae0eac338d3e7e4459a23db0c2f459070f" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'_>::check_recurring_to": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'_>::check_recurring_to", + "comment_kind": "OuterDoc", + "comment_text": " Handle key inputs for the To Method field on the Recurring page\n", + "comment_hash": "15772cd0a09d1af193463ae36c1cff6d298e534f09ef4d9ff443e088a2327d0a", + "body_text": "fn check_recurring_to(&mut self) -> Result<()> {\n match self.key.code {\n KeyCode::Enter => {\n let status = self.recurring_data.check_to_method(self.conn);\n\n match status {\n Ok(data) => {\n *self.recurring_tab = TxTab::Amount;\n self.go_correct_index();\n\n self.recurring_data\n .add_tx_status(data.to_string(), LogType::Info);\n }\n Err(e) => {\n self.recurring_data\n .add_tx_status(e.to_string(), LogType::Error);\n }\n }\n }\n KeyCode::Esc => {\n let status = self.recurring_data.check_to_method(self.conn);\n\n match status {\n Ok(data) => {\n *self.recurring_tab = TxTab::Nothing;\n\n self.recurring_data\n .add_tx_status(data.to_string(), LogType::Info);\n }\n Err(e) => {\n self.recurring_data\n .add_tx_status(e.to_string(), LogType::Error);\n }\n }\n }\n KeyCode::Backspace => self.recurring_data.edit_to_method(None),\n KeyCode::Char(a) => self.recurring_data.edit_to_method(Some(a)),\n _ => {}\n }\n\n Ok(())\n }", + "body_hash": "e66121bf63ccea2a9392183762f8fa51487bca08981a66e71d136c0e78f206f5" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'_>::check_recurring_type": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'_>::check_recurring_type", + "comment_kind": "OuterDoc", + "comment_text": " Handle key inputs for the TX Type field on the Recurring page\n", + "comment_hash": "04215f99d93732800e6b91296b9f967c0a304b55fde375d155d211547b3eae20", + "body_text": "fn check_recurring_type(&mut self) -> Result<()> {\n match self.key.code {\n KeyCode::Enter => {\n let status = self.recurring_data.check_tx_type(self.conn);\n\n match status {\n Ok(data) => {\n *self.recurring_tab = TxTab::FromMethod;\n self.go_correct_index();\n\n self.recurring_data\n .add_tx_status(data.to_string(), LogType::Info);\n }\n Err(e) => {\n self.recurring_data\n .add_tx_status(e.to_string(), LogType::Error);\n }\n }\n }\n KeyCode::Esc => {\n let status = self.recurring_data.check_tx_type(self.conn);\n\n match status {\n Ok(data) => {\n *self.recurring_tab = TxTab::Nothing;\n\n self.recurring_data\n .add_tx_status(data.to_string(), LogType::Info);\n }\n Err(e) => {\n self.recurring_data\n .add_tx_status(e.to_string(), LogType::Error);\n }\n }\n }\n KeyCode::Backspace => self.recurring_data.edit_tx_type(None),\n KeyCode::Char(a) => self.recurring_data.edit_tx_type(Some(a)),\n _ => {}\n }\n\n Ok(())\n }", + "body_hash": "9f78799caba853cbd70e3f339cfdd7e578ca4c0f7790964cfba91e617d609cdb" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'_>::check_search_amount": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'_>::check_search_amount", + "comment_kind": "OuterDoc", + "comment_text": " Handle key inputs for the amount field on the Search page\n", + "comment_hash": "a3547b0d10196b3e3da896ecf08ad019dadbf953ad0625304b5478d75d1a65e9", + "body_text": "fn check_search_amount(&mut self) -> Result<()> {\n match self.key.code {\n KeyCode::Enter => {\n let status = self.search_data.check_amount(true, self.conn);\n\n match status {\n Ok(data) => {\n *self.search_tab = TxTab::Tags;\n self.go_correct_index();\n\n self.search_data\n .add_tx_status(data.to_string(), LogType::Info);\n }\n Err(e) => {\n self.search_data\n .add_tx_status(e.to_string(), LogType::Error);\n }\n }\n }\n KeyCode::Esc => {\n let status = self.search_data.check_amount(true, self.conn);\n\n match status {\n Ok(data) => {\n *self.search_tab = TxTab::Nothing;\n\n self.search_data\n .add_tx_status(data.to_string(), LogType::Info);\n }\n Err(e) => {\n self.search_data\n .add_tx_status(e.to_string(), LogType::Error);\n }\n }\n }\n KeyCode::Backspace => self.search_data.edit_amount(None),\n KeyCode::Char(a) => self.search_data.edit_amount(Some(a)),\n _ => {}\n }\n\n Ok(())\n }", + "body_hash": "05f8eede6890758cdae0606b44ea08510d2012632165bfdbd57106813d496e4a" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'_>::check_search_date": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'_>::check_search_date", + "comment_kind": "OuterDoc", + "comment_text": " Handle key inputs for the Date field on the Search page\n", + "comment_hash": "9fbf6ac94edf777b3b0693dfc65c929b4ff7fd323e56e58aba43c667da2a5110", + "body_text": "fn check_search_date(&mut self) {\n match self.key.code {\n KeyCode::Enter => {\n let status = self\n .search_data\n .check_date(*self.search_date_type, self.conn);\n\n match status {\n Ok(data) => {\n *self.search_tab = TxTab::Details;\n self.go_correct_index();\n\n self.add_tx_data\n .add_tx_status(data.to_string(), LogType::Info);\n }\n Err(e) => {\n self.add_tx_data\n .add_tx_status(e.to_string(), LogType::Error);\n }\n }\n }\n KeyCode::Esc => {\n let status = self\n .search_data\n .check_date(*self.search_date_type, self.conn);\n\n match status {\n Ok(data) => {\n *self.search_tab = TxTab::Nothing;\n\n self.add_tx_data\n .add_tx_status(data.to_string(), LogType::Info);\n }\n Err(e) => {\n self.add_tx_data\n .add_tx_status(e.to_string(), LogType::Error);\n }\n }\n }\n KeyCode::Backspace => self.search_data.edit_date(None),\n KeyCode::Char(a) => self.search_data.edit_date(Some(a)),\n _ => {}\n }\n }", + "body_hash": "6a6e4b0ec13f469f7733f5527053b3ccedf81095ed8f99ebf5a5222abb3dfacb" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'_>::check_search_details": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'_>::check_search_details", + "comment_kind": "OuterDoc", + "comment_text": " Handle key inputs for the Details field on the Search page\n", + "comment_hash": "dc75453a90161ca648bbf6621ab646cc30e2a7c9b94e1cd3e7cdd2932579ba42", + "body_text": "fn check_search_details(&mut self) {\n match self.key.code {\n KeyCode::Enter => {\n *self.search_tab = TxTab::TxType;\n self.go_correct_index();\n }\n KeyCode::Esc => *self.search_tab = TxTab::Nothing,\n KeyCode::Backspace => self.search_data.edit_details(None),\n KeyCode::Char(a) => self.search_data.edit_details(Some(a)),\n _ => {}\n }\n }", + "body_hash": "08e33d3ba6415e885c3761d9cb6dd9e074f1271a89e24eb2ccacad23949cc123" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'_>::check_search_from": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'_>::check_search_from", + "comment_kind": "OuterDoc", + "comment_text": " Handle key inputs for the From Method field on the Search page\n", + "comment_hash": "80e1afb8492d4da93160183e639c8335c239e553ec793f9efa2b243ed16f8276", + "body_text": "fn check_search_from(&mut self) {\n match self.key.code {\n KeyCode::Enter => {\n let status = self.search_data.check_from_method(self.conn);\n\n match status {\n Ok(data) => {\n match self.search_data.get_tx_type() {\n TxType::IncomeExpense => *self.search_tab = TxTab::Amount,\n TxType::Transfer => *self.search_tab = TxTab::ToMethod,\n }\n self.go_correct_index();\n\n self.search_data\n .add_tx_status(data.to_string(), LogType::Info);\n }\n\n Err(e) => {\n self.search_data\n .add_tx_status(e.to_string(), LogType::Error);\n }\n }\n }\n KeyCode::Esc => {\n let status = self.search_data.check_from_method(self.conn);\n\n match status {\n Ok(data) => {\n *self.search_tab = TxTab::Nothing;\n\n self.search_data\n .add_tx_status(data.to_string(), LogType::Info);\n }\n\n Err(e) => {\n self.search_data\n .add_tx_status(e.to_string(), LogType::Error);\n }\n }\n }\n KeyCode::Backspace => self.search_data.edit_from_method(None),\n KeyCode::Char(a) => self.search_data.edit_from_method(Some(a)),\n _ => {}\n }\n }", + "body_hash": "5ff05a7aa3571bd5287aa33e3f78b518dc8232a32c5efa2c21e307e5147bafe7" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'_>::check_search_tags": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'_>::check_search_tags", + "comment_kind": "OuterDoc", + "comment_text": " Handle key inputs for the Tag field on the Search page\n", + "comment_hash": "3eadc8b6fae9f91368b3dced4ee8d2d9de8d1ebf09d5d6376d0c2117b076d055", + "body_text": "fn check_search_tags(&mut self) {\n match self.key.code {\n KeyCode::Enter | KeyCode::Esc => {\n let status = self.search_data.check_tags_forced(self.conn);\n\n match status {\n Ok(data) => {\n *self.search_tab = TxTab::Nothing;\n\n self.search_data\n .add_tx_status(data.to_string(), LogType::Info);\n }\n Err(e) => {\n self.search_data\n .add_tx_status(e.to_string(), LogType::Error);\n }\n }\n }\n KeyCode::Backspace => self.search_data.edit_tags(None),\n KeyCode::Char(a) => self.search_data.edit_tags(Some(a)),\n _ => {}\n }\n }", + "body_hash": "8e8c449d04d1fae42ebc7f582a94edc78cc27288ab949b4b3433ed75c7441219" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'_>::check_search_to": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'_>::check_search_to", + "comment_kind": "OuterDoc", + "comment_text": " Handle key inputs for the To Method field on the Search page\n", + "comment_hash": "948b0f82e1090a16373a746c94f360e1d6e64d8f5bbb891b480270454e39fafa", + "body_text": "fn check_search_to(&mut self) {\n match self.key.code {\n KeyCode::Enter => {\n let status = self.search_data.check_to_method(self.conn);\n\n match status {\n Ok(data) => {\n *self.search_tab = TxTab::Amount;\n self.go_correct_index();\n\n self.search_data\n .add_tx_status(data.to_string(), LogType::Info);\n }\n\n Err(e) => {\n self.search_data\n .add_tx_status(e.to_string(), LogType::Error);\n }\n }\n }\n KeyCode::Esc => {\n let status = self.search_data.check_to_method(self.conn);\n\n match status {\n Ok(data) => {\n *self.search_tab = TxTab::Nothing;\n\n self.search_data\n .add_tx_status(data.to_string(), LogType::Info);\n }\n\n Err(e) => {\n self.search_data\n .add_tx_status(e.to_string(), LogType::Error);\n }\n }\n }\n KeyCode::Backspace => self.search_data.edit_to_method(None),\n KeyCode::Char(a) => self.search_data.edit_to_method(Some(a)),\n _ => {}\n }\n }", + "body_hash": "8ac913f302de788db0954e335d438edd957316557cb9bf1fae732e16e127cee3" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'_>::check_search_type": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'_>::check_search_type", + "comment_kind": "OuterDoc", + "comment_text": " Handle key inputs for the TX Type field on the Search page\n", + "comment_hash": "9dfcf2139bdb6d51bc6465f8d3754dcac8504b38835b2ac96ad07c0df87ec9e1", + "body_text": "fn check_search_type(&mut self) {\n match self.key.code {\n KeyCode::Enter => {\n let status = self.search_data.check_tx_type(self.conn);\n\n match status {\n Ok(data) => {\n *self.search_tab = TxTab::FromMethod;\n self.go_correct_index();\n\n self.add_tx_data\n .add_tx_status(data.to_string(), LogType::Info);\n }\n Err(e) => {\n self.add_tx_data\n .add_tx_status(e.to_string(), LogType::Error);\n }\n }\n }\n KeyCode::Esc => {\n let status = self.search_data.check_tx_type(self.conn);\n\n match status {\n Ok(data) => {\n *self.search_tab = TxTab::Nothing;\n\n self.add_tx_data\n .add_tx_status(data.to_string(), LogType::Info);\n }\n Err(e) => {\n self.add_tx_data\n .add_tx_status(e.to_string(), LogType::Error);\n }\n }\n }\n KeyCode::Backspace => self.search_data.edit_tx_type(None),\n KeyCode::Char(a) => self.search_data.edit_tx_type(Some(a)),\n _ => {}\n }\n }", + "body_hash": "a00f2af501aab0be726029b8ad46d6206298bef11fcb5002e13b3485e5142154" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'_>::do_chart_down": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'_>::do_chart_down", + "comment_kind": "OuterDoc", + "comment_text": " Handle Arrow Down keypress on the chart page\n", + "comment_hash": "dd4dcc26c127bc4ee7bdaf84c09a90f6c75aeeacc6b7c1d49f246d2c43ea5322", + "body_text": "fn do_chart_down(&mut self) {\n if !*self.chart_hidden_mode {\n match self.chart_modes.index {\n 0 => *self.chart_tab = self.chart_tab.change_tab_down_monthly(),\n 1 => *self.chart_tab = self.chart_tab.change_tab_down_yearly(),\n 2 => *self.chart_tab = self.chart_tab.change_tab_down_all_time(),\n _ => {}\n }\n }\n }", + "body_hash": "92b19df2ff38a491832e2de7600deaa81a8b04c18c96b264138a9d13312c750d" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'_>::do_chart_up": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'_>::do_chart_up", + "comment_kind": "OuterDoc", + "comment_text": " Handle Arrow Up keypress on the chart page\n", + "comment_hash": "111cc1a529cf9c31f881d75f5b5b5e7c03df3034af5ce0cd9a9bba3c10bee49d", + "body_text": "fn do_chart_up(&mut self) {\n if !*self.chart_hidden_mode {\n match self.chart_modes.index {\n 0 => *self.chart_tab = self.chart_tab.change_tab_up_monthly(),\n 1 => *self.chart_tab = self.chart_tab.change_tab_up_yearly(),\n 2 => *self.chart_tab = self.chart_tab.change_tab_up_all_time(),\n _ => {}\n }\n }\n }", + "body_hash": "b80d48f6c14ed16c49bc432da38fc760b8716ea6639c900d546e3a3b46d8f91a" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'_>::do_home_down": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'_>::do_home_down", + "comment_kind": "OuterDoc", + "comment_text": " Handle Arrow Down keypress on the Homepage\n", + "comment_hash": "25ad5fcf9fe3c09734b8b3f79a801cec02eb9c09fcba6fd7616a6ed3e8214ff9", + "body_text": "fn do_home_down(&mut self) {\n match &self.home_tab {\n HomeTab::Table => {\n \n \n \n if self.home_txs.is_empty() {\n *self.home_tab = self.home_tab.change_tab_down();\n } else if self.home_table.state.selected() == Some(self.home_table.items.len() - 1)\n {\n *self.home_tab = HomeTab::Years;\n self.home_table.state.select(None);\n } else if !self.home_txs.is_empty() {\n self.home_table.next();\n }\n }\n HomeTab::Months => {\n \n \n if self.home_txs.is_empty() {\n *self.home_tab = self.home_tab.change_tab_up();\n } else {\n *self.home_tab = self.home_tab.change_tab_down();\n self.home_table.state.select(Some(0));\n }\n }\n HomeTab::Years => *self.home_tab = self.home_tab.change_tab_down(),\n }\n }", + "body_hash": "f435118d78a0c5d55ce2918c3a310a2577903c98587f5be1f9a44020722e8711" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'_>::do_home_down#free:6101b076d78e": { + "kind": "Free", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'_>::do_home_down#free:6101b076d78e", + "comment_kind": "Line", + "comment_text": "// With no tx to select, step straight out of the table. Otherwise\n// the last row moves the focus down to the year widget and any\n// other row just moves the selection down", + "comment_hash": "6101b076d78ee862794c24c7eb16c8d0c947cb374b1f1a97e1f75e997725409b", + "body_text": "if self.home_txs.is_empty() {} else if self.home_table.state.selected() == Some(self.home_table.items.len() - 1)\n {} else if !self.home_txs.is_empty() {}\n}", + "body_hash": "c1ad027610096fb1c2ad90d93389408408b3a2ccd8036037e67c14fc4d745c95" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'_>::do_home_down#free:d1335303d5f0": { + "kind": "Free", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'_>::do_home_down#free:d1335303d5f0", + "comment_kind": "Line", + "comment_text": "// With no tx to select, stay on the month widget. Otherwise enter\n// the table on its first row", + "comment_hash": "d1335303d5f02fd84b4698ced747095509ef74586d9dea1e603c148e249d62eb", + "body_text": "if self.home_txs.is_empty() {} else {}\n}", + "body_hash": "5e0563170aa16a524e795a89850e21e29a8b84ffbb90f2324bf346cac9477a2e" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'_>::do_home_up": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'_>::do_home_up", + "comment_kind": "OuterDoc", + "comment_text": " Handle Arrow Up keypress on the Homepage\n", + "comment_hash": "ad0cf1269787dcdeb8cf6407e7c4adb70f5afd124f2053088426720645f0b7c2", + "body_text": "fn do_home_up(&mut self) {\n match &self.home_tab {\n HomeTab::Table => {\n \n \n \n if self.home_txs.is_empty() {\n *self.home_tab = self.home_tab.change_tab_up();\n } else if self.home_table.state.selected() == Some(0) {\n *self.home_tab = HomeTab::Months;\n self.home_table.state.select(None);\n } else if !self.home_txs.is_empty() {\n self.home_table.previous();\n }\n }\n HomeTab::Years => {\n \n \n if self.home_txs.is_empty() {\n *self.home_tab = self.home_tab.change_tab_down();\n } else {\n self.home_table\n .state\n .select(Some(self.home_table.items.len() - 1));\n *self.home_tab = self.home_tab.change_tab_up();\n }\n }\n HomeTab::Months => *self.home_tab = self.home_tab.change_tab_up(),\n }\n }", + "body_hash": "02827122e9d7bf2bf8bf36ae476c97c312d7c2a076ceb005346d6cd296d6e104" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'_>::do_home_up#free:28d9a4a668e0": { + "kind": "Free", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'_>::do_home_up#free:28d9a4a668e0", + "comment_kind": "Line", + "comment_text": "// With no tx to select, stay on the year widget. Otherwise enter\n// the table on its last row", + "comment_hash": "28d9a4a668e08fdbb38bd026c5898daa8c16119f3f5729c1a608036fec5e4540", + "body_text": "if self.home_txs.is_empty() {} else {}\n}", + "body_hash": "5e0563170aa16a524e795a89850e21e29a8b84ffbb90f2324bf346cac9477a2e" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'_>::do_home_up#free:d3b429b92fec": { + "kind": "Free", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'_>::do_home_up#free:d3b429b92fec", + "comment_kind": "Line", + "comment_text": "// With no tx to select, step straight out of the table. Otherwise\n// the first row moves the focus up to the month widget and any\n// other row just moves the selection up", + "comment_hash": "d3b429b92fec608e26243bb9d7dd9f4c1a7afb909eaa2e88947813afe682b4a4", + "body_text": "if self.home_txs.is_empty() {} else if self.home_table.state.selected() == Some(0) {} else if !self.home_txs.is_empty() {}\n}", + "body_hash": "289c87ca3c28235132ea932f858586965743a190dd087d3dd83d14b196e2c050" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'_>::do_summary_down": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'_>::do_summary_down", + "comment_kind": "OuterDoc", + "comment_text": " Handle Arrow Down keypress on the Summary page\n", + "comment_hash": "0f3fa441f54bea049a5ab336bc4ea4d0b14f66f721baea42db184838f192d661", + "body_text": "fn do_summary_down(&mut self) {\n let total_tags = self.summary_table.items.len();\n\n if !*self.summary_hidden_mode {\n match self.summary_modes.index {\n 0 => match self.summary_tab {\n SummaryTab::Table => {\n if self.summary_table.state.selected() == Some(total_tags - 1) {\n *self.summary_tab = self.summary_tab.change_tab_down_monthly();\n } else {\n self.summary_table.next();\n }\n }\n SummaryTab::Months => {\n if total_tags > 0 {\n self.summary_table.state.select(Some(0));\n *self.summary_tab = self.summary_tab.change_tab_down_monthly();\n } else {\n *self.summary_tab = self.summary_tab.change_tab_down_monthly();\n *self.summary_tab = self.summary_tab.change_tab_down_monthly();\n self.summary_table.state.select(None);\n }\n }\n _ => *self.summary_tab = self.summary_tab.change_tab_down_monthly(),\n },\n 1 => match self.summary_tab {\n SummaryTab::Table => {\n if self.summary_table.state.selected() == Some(total_tags - 1) {\n *self.summary_tab = self.summary_tab.change_tab_down_yearly();\n } else {\n self.summary_table.next();\n }\n }\n SummaryTab::Years => {\n if total_tags > 0 {\n self.summary_table.state.select(Some(0));\n *self.summary_tab = self.summary_tab.change_tab_down_yearly();\n } else {\n *self.summary_tab = self.summary_tab.change_tab_down_yearly();\n *self.summary_tab = self.summary_tab.change_tab_down_yearly();\n self.summary_table.state.select(None);\n }\n }\n _ => *self.summary_tab = self.summary_tab.change_tab_down_yearly(),\n },\n 2 => match self.summary_tab {\n SummaryTab::Table => {\n if self.summary_table.state.selected() == Some(total_tags - 1) {\n *self.summary_tab = self.summary_tab.change_tab_down_all_time();\n } else {\n self.summary_table.next();\n }\n }\n SummaryTab::ModeSelection => {\n if total_tags > 0 {\n self.summary_table.state.select(Some(0));\n *self.summary_tab = self.summary_tab.change_tab_down_all_time();\n } else {\n *self.summary_tab = self.summary_tab.change_tab_down_all_time();\n *self.summary_tab = self.summary_tab.change_tab_down_all_time();\n self.summary_table.state.select(None);\n }\n }\n _ => {}\n },\n _ => {}\n }\n } else if total_tags > 0 {\n if self.summary_table.state.selected() == Some(total_tags - 1) {\n self.summary_table.state.select(Some(0));\n } else {\n self.summary_table.next();\n }\n }\n }", + "body_hash": "2478f8446490186a122410d7ea2493d326256907246be450020d10ef7b80f51e" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'_>::do_summary_up": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'_>::do_summary_up", + "comment_kind": "OuterDoc", + "comment_text": " Handle Arrow Up keypress on the Summary page\n", + "comment_hash": "63591a01acf57f0ff5c55a360e63b170129d307dc614c9f8f2534e443a5080e5", + "body_text": "fn do_summary_up(&mut self) {\n let total_tags = self.summary_table.items.len();\n\n if !*self.summary_hidden_mode {\n match self.summary_modes.index {\n 0 => match self.summary_tab {\n SummaryTab::Table => {\n if self.summary_table.state.selected() == Some(0) {\n *self.summary_tab = self.summary_tab.change_tab_up_monthly();\n } else {\n self.summary_table.previous();\n }\n }\n SummaryTab::ModeSelection => {\n if total_tags > 0 {\n self.summary_table.state.select(Some(total_tags - 1));\n *self.summary_tab = self.summary_tab.change_tab_up_monthly();\n } else {\n *self.summary_tab = self.summary_tab.change_tab_up_monthly();\n *self.summary_tab = self.summary_tab.change_tab_up_monthly();\n self.summary_table.state.select(None);\n }\n }\n _ => *self.summary_tab = self.summary_tab.change_tab_up_monthly(),\n },\n 1 => match self.summary_tab {\n SummaryTab::Table => {\n if self.summary_table.state.selected() == Some(0) {\n *self.summary_tab = self.summary_tab.change_tab_up_yearly();\n } else {\n self.summary_table.previous();\n }\n }\n SummaryTab::ModeSelection => {\n if total_tags > 0 {\n self.summary_table.state.select(Some(total_tags - 1));\n *self.summary_tab = self.summary_tab.change_tab_up_yearly();\n } else {\n *self.summary_tab = self.summary_tab.change_tab_up_yearly();\n *self.summary_tab = self.summary_tab.change_tab_up_yearly();\n self.summary_table.state.select(None);\n }\n }\n _ => *self.summary_tab = self.summary_tab.change_tab_up_yearly(),\n },\n 2 => match self.summary_tab {\n SummaryTab::Table => {\n if self.summary_table.state.selected() == Some(0) {\n *self.summary_tab = self.summary_tab.change_tab_up_all_time();\n } else {\n self.summary_table.previous();\n }\n }\n SummaryTab::ModeSelection => {\n if total_tags > 0 {\n self.summary_table.state.select(Some(total_tags - 1));\n *self.summary_tab = self.summary_tab.change_tab_up_all_time();\n } else {\n *self.summary_tab = self.summary_tab.change_tab_up_all_time();\n *self.summary_tab = self.summary_tab.change_tab_up_all_time();\n self.summary_table.state.select(None);\n }\n }\n _ => {}\n },\n _ => {}\n }\n } else if total_tags > 0 {\n if self.summary_table.state.selected() == Some(0) {\n self.summary_table.state.select(Some(total_tags - 1));\n } else {\n self.summary_table.previous();\n }\n }\n }", + "body_hash": "5fbb9e14203e6d18de7ce4828d2f9f083dc0e47f2d3530ceaaf9832e0575c913" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'_>::go_correct_index": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'_>::go_correct_index", + "comment_kind": "OuterDoc", + "comment_text": " Move the cursor for text fields to the correct position, if it's misplaced\n", + "comment_hash": "86cc026665121502ce0abdae77454f0e287490c6f22fa8b3d9d49ec7393a19c6", + "body_text": "fn go_correct_index(&mut self) {\n match self.page {\n CurrentUi::AddTx => self.add_tx_data.go_current_index(self.add_tx_tab),\n CurrentUi::Search => self.search_data.go_current_index(self.search_tab),\n CurrentUi::Recurring => self.recurring_data.go_current_index(self.recurring_tab),\n _ => {}\n }\n }", + "body_hash": "f453c8d178db8a1532107895bcf48871c868c18f7f4ab1fb943c0db6155ce31f" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'_>::reload_activity_table": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'_>::reload_activity_table", + "comment_kind": "OuterDoc", + "comment_text": " Reload activity data by fetching from the DB\n", + "comment_hash": "d4bddec368084f9bb722c73c58c765c5ecbd8a04ad08afb8189a48a6594ce42e", + "body_text": "fn reload_activity_table(&mut self) -> Result<()> {\n *self.activity_view = self.conn.get_activity_view_with_str(\n self.activity_months.get_selected_value(),\n self.activity_years.get_selected_value(),\n )?;\n\n let old_table_position = self.activity_table.state;\n\n *self.activity_table = TableData::new(self.activity_view.get_activity_table());\n self.activity_table.state = old_table_position;\n\n Ok(())\n }", + "body_hash": "fe49119d50c4073b0eac13cd0d9620db0f852ad38a2ff56be4b6d6485d0a8bf8" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'_>::reload_add_tx_balance_data": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'_>::reload_add_tx_balance_data", + "comment_kind": "OuterDoc", + "comment_text": " Update add TX page balance section data that is being shown on the UI\n", + "comment_hash": "ae106a4ed09203ab6a08a4006d409c407157ebf8ae095be30d5a639e5558e2be", + "body_text": "fn reload_add_tx_balance_data(&mut self) -> Result<()> {\n let is_editing_tx = self.add_tx_data.editing_tx;\n\n let mut current_table_index = if is_editing_tx {\n self.home_table.state.selected()\n } else {\n None\n };\n\n if self.add_tx_data.from_search {\n current_table_index = None;\n }\n\n *self.add_tx_balance = self.add_tx_data.generate_balance_section(\n self.home_txs,\n current_table_index,\n self.conn,\n )?;\n\n Ok(())\n }", + "body_hash": "02135be6d82757a65d5c29f1fa0629cdadaffdcfcecd76441573f9308e5344fa" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'_>::reload_chart_data": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'_>::reload_chart_data", + "comment_kind": "OuterDoc", + "comment_text": " Reload chart data by fetching from the DB\n", + "comment_hash": "3455b15998e4840ff7d071ae4a583573afce709af6bfba7df22e204522ad0f1d", + "body_text": "fn reload_chart_data(&mut self) -> Result<()> {\n let fetch_nature = match self.chart_modes.index {\n 0 => FetchNature::Monthly,\n 1 => FetchNature::Yearly,\n 2 => FetchNature::All,\n _ => panic!(\"Invalid chart mode\"),\n };\n\n *self.chart_view = self.conn.get_chart_view_with_str(\n self.chart_months.get_selected_value(),\n self.chart_years.get_selected_value(),\n fetch_nature,\n )?;\n\n *self.chart_tx_methods = IndexedData::new_tx_methods_cumulative(self.conn);\n *self.chart_activated_methods = self\n .conn\n .get_tx_methods_cumulative()\n .into_iter()\n .map(|s| (s, true))\n .collect();\n\n Ok(())\n }", + "body_hash": "33026f436528b4fc50c4cdcc52b7eda8460e4ba16af0f4c66193bf3f839551c3" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'_>::reload_home_table": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'_>::reload_home_table", + "comment_kind": "OuterDoc", + "comment_text": " Reload Home page's table data by fetching from the DB\n", + "comment_hash": "956c2d7be20a66a3c923fc4ef7c176d513a028e95f03193c53e4672db87e2394", + "body_text": "fn reload_home_table(&mut self, clear_lerp: bool) -> Result<()> {\n *self.home_txs = self.conn.fetch_txs_with_str(\n self.home_months.get_selected_value(),\n self.home_years.get_selected_value(),\n FetchNature::Monthly,\n )?;\n\n *self.home_table = TableData::new(self.home_txs.tx_array());\n\n if clear_lerp {\n self.lerp_state.clear_lerp(HOME_TABLE_ID);\n }\n\n Ok(())\n }", + "body_hash": "65c1da21419d1b661f70e0d72d1b75cd4e7ef9591bcaee5a1df56601343502da" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'_>::reload_recurring_table": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'_>::reload_recurring_table", + "comment_kind": "OuterDoc", + "comment_text": " Refetches the recurring transaction rules and rebuilds the Recurring page's table\n", + "comment_hash": "29657154b9c8097f005930abd9cbbe26a33fb20d43bb255b64308cd87c7bb6a9", + "body_text": "fn reload_recurring_table(&mut self) -> Result<()> {\n *self.recurring_txs = self.conn.get_recurring_txs()?;\n\n let old_table_position = self.recurring_table.state;\n\n let items = self\n .recurring_txs\n .iter()\n .map(FullRecurringTx::to_array)\n .collect();\n *self.recurring_table = TableData::new(items);\n self.recurring_table.state = old_table_position;\n\n Ok(())\n }", + "body_hash": "88e2f3d6f9cf7978c4a719fe667ff2c2a60818090d9dc325d8ad0131ca4756ab" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'_>::reload_summary": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'_>::reload_summary", + "comment_kind": "OuterDoc", + "comment_text": " Reset summary table data by recreating it from gathered Summary Data\n", + "comment_hash": "e452276c9d00f7e6968657eb87f20ebdb902f11136d23039536cf18cb9cfc8ef", + "body_text": "fn reload_summary(&mut self) -> Result<()> {\n let fetch_nature = match self.summary_modes.index {\n 0 => FetchNature::Monthly,\n 1 => FetchNature::Yearly,\n 2 => FetchNature::All,\n _ => panic!(\"Invalid summary mode\"),\n };\n\n let summary_view = self.conn.get_summary_with_str(\n self.summary_months.get_selected_value(),\n self.summary_years.get_selected_value(),\n fetch_nature,\n )?;\n\n *self.summary_sort = SortingType::Tags;\n\n let (previous_month, previous_year) = self.get_summary_previous_value();\n\n if let Some(month) = previous_month\n && let Some(year) = previous_year\n {\n let last_summary_view = self\n .conn\n .get_summary_with_str(&month, &year, fetch_nature)?;\n\n let last_full_summary = last_summary_view.generate_summary(None, self.conn);\n\n let mut summary_tags_table =\n summary_view.tags_array(Some(&last_summary_view), self.conn);\n\n summary_tags_table = sort_table_data(summary_tags_table, self.summary_sort);\n\n *self.summary_table = TableData::new(summary_tags_table);\n\n *self.full_summary = summary_view.generate_summary(Some(&last_full_summary), self.conn);\n\n *self.summary_view = summary_view;\n } else {\n let mut summary_tags_table = summary_view.tags_array(None, self.conn);\n summary_tags_table = sort_table_data(summary_tags_table, self.summary_sort);\n\n *self.summary_table = TableData::new(summary_tags_table);\n\n *self.full_summary = summary_view.generate_summary(None, self.conn);\n\n *self.summary_view = summary_view;\n }\n\n Ok(())\n }", + "body_hash": "7b102d70a8d60c64a522bcfc16a34797eb342f2da82b39b13e73bcc30fd3ac41" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'_>::reset_search_data": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'_>::reset_search_data", + "comment_kind": "OuterDoc", + "comment_text": " Reset all currently shown search related data\n", + "comment_hash": "e3a78eee0c223b5ccab4dcee5ccb59de247923e5477ec6f5c1e62a42e8a257ed", + "body_text": "fn reset_search_data(&mut self) {\n *self.search_table = TableData::new(Vec::new());\n *self.search_txs = SearchView::new_empty();\n }", + "body_hash": "9cab903ec3c270955163bac0484592d45f9006072c0427e2383488c67a3556de" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'a>::add_tx": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'a>::add_tx", + "comment_kind": "OuterDoc", + "comment_text": " Adds new TX and reloads home and chart data\n", + "comment_hash": "86017af021c4909fe61263004c108c2e85cee631b8554f17036fa5bde1f8b5d1", + "body_text": "pub fn add_tx(&mut self) -> Result<()> {\n let status = self.add_tx_data.add_tx(self.home_txs, self.conn);\n\n match status {\n Ok(()) => {\n self.go_home_reset();\n \n \n *self.home_tab = HomeTab::Months;\n self.reload_home_table(true)?;\n self.reload_chart_data()?;\n self.reload_summary()?;\n self.reset_search_data();\n self.reload_activity_table()?;\n }\n Err(e) => self\n .add_tx_data\n .add_tx_status(e.to_string(), LogType::Error),\n }\n\n Ok(())\n }", + "body_hash": "1fa322c07a1e6f7d8667ef5cc469d3dbdc44163445c126fba55ed09332ccdd6f" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'a>::add_tx#free:3f4cb557632d": { + "kind": "Free", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'a>::add_tx#free:3f4cb557632d", + "comment_kind": "Line", + "comment_text": "// A new tx was added, so go back to the month tab and refetch the\n// balances and the table to show it", + "comment_hash": "3f4cb557632d570e8258f10a56af79ada8930912614a9311cd286eeae66d5c91", + "body_text": "*self.home_tab = HomeTab::Months;\nself.reload_home_table(true)?;\nself.reload_chart_data()?;\nself.reload_summary()?;\nself.reset_search_data();\nself.reload_activity_table()?;\n}", + "body_hash": "5a6e3db4021dbff528563c38c8686ff0795332b0c2f98a43482b943d5b45b8b0" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'a>::change_search_date_type": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'a>::change_search_date_type", + "comment_kind": "OuterDoc", + "comment_text": " Cycles through available date types\n", + "comment_hash": "a2c4287a082088bd48aa5c7794122a4a964d0456a0cd0b7aae62ba9a6df082d0", + "body_text": "pub fn change_search_date_type(&mut self) {\n *self.search_date_type = self.search_date_type.get_next();\n self.search_data.clear_date();\n }", + "body_hash": "58304e35746cf280f6b2906677e1c9825a161be48b6f6fc55fdd16914f673714" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'a>::change_summary_sort": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'a>::change_summary_sort", + "comment_kind": "OuterDoc", + "comment_text": " Cycles through tag, income, expense table sorting on summary page\n", + "comment_hash": "0c2ba1621dacb4469026a01a6ed79a80d2bf8511dc549137416e7d6dc9b131ce", + "body_text": "pub fn change_summary_sort(&mut self) {\n *self.summary_sort = self.summary_sort.next_type();\n let summary_data = self.summary_table.items.clone();\n let sorted_data = sort_table_data(summary_data, self.summary_sort);\n *self.summary_table = TableData::new(sorted_data);\n }", + "body_hash": "6bae173ce0ec8f60ea3606985a86fb408a43c0093abae291a1d666af7c03151d" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'a>::clear_input": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'a>::clear_input", + "comment_kind": "OuterDoc", + "comment_text": " Resets all input boxes on Add TX and Transfer page\n", + "comment_hash": "477df4152c70e66db7c7bc53cf189f179328e7c0dc765d0a7a152ddcdd0f949e", + "body_text": "pub fn clear_input(&mut self) -> Result<()> {\n match self.page {\n CurrentUi::AddTx => {\n *self.add_tx_data = TxData::new();\n self.reload_add_tx_balance_data()?;\n }\n CurrentUi::Search => {\n *self.search_data = TxData::new_empty();\n self.reset_search_data();\n }\n CurrentUi::Recurring => {\n *self.recurring_data = TxData::new_empty();\n }\n _ => {}\n }\n\n Ok(())\n }", + "body_hash": "a1e27ec5e6e9a5f28e327214dd832255277bfa736b715cc647f34059956ed140" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'a>::do_autofill": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'a>::do_autofill", + "comment_kind": "OuterDoc", + "comment_text": " Takes the auto fill value and adds it to the relevant field\n", + "comment_hash": "3d1d85dbed2b7d87fe9c656fd104d3a9016992521e32809f269415ee9612ad7f", + "body_text": "pub fn do_autofill(&mut self) {\n match self.page {\n CurrentUi::AddTx => self.add_tx_data.accept_autofill(self.add_tx_tab),\n CurrentUi::Search => self.search_data.accept_autofill(self.search_tab),\n CurrentUi::Recurring => self.recurring_data.accept_autofill(self.recurring_tab),\n _ => {}\n }\n }", + "body_hash": "8039441764e90dba6e7932b180a314ee41d9314798e814e335759b098a031025" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'a>::do_chart_hidden_mode": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'a>::do_chart_hidden_mode", + "comment_kind": "OuterDoc", + "comment_text": " Hides/shows chart top widgets\n", + "comment_hash": "c7405cae0fdbb1d953cce01779027efdf7ce8b55b6a8fc77b420b6252d05bbe1", + "body_text": "pub fn do_chart_hidden_mode(&mut self) {\n *self.chart_hidden_mode = !*self.chart_hidden_mode;\n }", + "body_hash": "bd6e7e95b28190ab8bd35394f235dbe0e5683ab44a49f11c502b21f1b033999d" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'a>::do_chart_lgeneds": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'a>::do_chart_lgeneds", + "comment_kind": "OuterDoc", + "comment_text": " Hides/shows chart legends\n", + "comment_hash": "d4a7a4aac44bfc28a79144d1f6cf7fa60321479915289ca491fb4b4eb31b8d74", + "body_text": "pub fn do_chart_lgeneds(&mut self) {\n *self.chart_hidden_legends = !*self.chart_hidden_legends;\n }", + "body_hash": "81b974451a3a714bbd34e5c6e32d93713d4462de3be1e430e86ea16f2f1b670c" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'a>::do_deletion_popup": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'a>::do_deletion_popup", + "comment_kind": "OuterDoc", + "comment_text": " Turns on deletion confirmation popup for the selected transaction\n", + "comment_hash": "efc3a4b0b932663d9e9083180b4f0bc87a400cf9e386ab1c429af99633d79957", + "body_text": "pub fn do_deletion_popup(&mut self) {\n match self.page {\n CurrentUi::Home if self.home_table.state.selected().is_some() => {\n *self.popup_status = PopupType::new_choice_deletion(self.theme);\n }\n CurrentUi::Home => {}\n CurrentUi::Search if self.search_table.state.selected().is_some() => {\n *self.popup_status = PopupType::new_choice_deletion(self.theme);\n }\n CurrentUi::Recurring if self.recurring_table.state.selected().is_some() => {\n *self.popup_status = PopupType::new_choice_deletion(self.theme);\n }\n _ => {}\n }\n }", + "body_hash": "7d0a6ded6e75a0ae06edf7119fabe77f5b6b50227de23dd099aa91c9a8847fe3" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'a>::do_empty_popup": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'a>::do_empty_popup", + "comment_kind": "OuterDoc", + "comment_text": " Removes pop up status\n", + "comment_hash": "d4c85f55fe87e9b4dcaeaf6aa5477666060d642af89cb173c255712f6ed67098", + "body_text": "pub fn do_empty_popup(&mut self) {\n *self.popup_status = PopupType::Nothing;\n }", + "body_hash": "066dcd5760d2d224734704dd924c08f0e83232f5f9eb03f94c0e1484abfdbe4e" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'a>::do_help_popup": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'a>::do_help_popup", + "comment_kind": "OuterDoc", + "comment_text": " Turns on help popup\n", + "comment_hash": "1a53847e6f859444f72f8089af43e8451f8df741ab9be8bb4e08a132c9be973a", + "body_text": "pub fn do_help_popup(&mut self) {\n let popup_state = match self.page {\n CurrentUi::Home => InfoPopupState::HomeHelp,\n CurrentUi::AddTx => InfoPopupState::AddTxHelp,\n CurrentUi::Chart => InfoPopupState::ChartHelp,\n CurrentUi::Summary => InfoPopupState::SummaryHelp,\n CurrentUi::Search => InfoPopupState::SearchHelp,\n CurrentUi::Activity => InfoPopupState::ActivityHelp,\n CurrentUi::Recurring => InfoPopupState::RecurringHelp,\n CurrentUi::Initial => unreachable!(),\n };\n\n *self.popup_status = PopupType::new_info(popup_state);\n }", + "body_hash": "7dd3eafe87a483e3c9b87adcc565e1a487114f11c7ff04064ddfd7283c7ec8d0" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'a>::do_summary_hidden_mode": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'a>::do_summary_hidden_mode", + "comment_kind": "OuterDoc", + "comment_text": " Hides summary top widgets\n", + "comment_hash": "7627c507903a0c0ee6f26cfefb79c8ffa5617058b9b5c6d877dccdf5ddb39231", + "body_text": "pub fn do_summary_hidden_mode(&mut self) {\n *self.summary_hidden_mode = !*self.summary_hidden_mode;\n\n if *self.summary_hidden_mode\n && self.summary_table.state.selected().is_none()\n && !self.summary_table.items.is_empty()\n {\n *self.summary_tab = SummaryTab::Table;\n self.summary_table.state.select(Some(0));\n }\n }", + "body_hash": "a7a1a4246639d4e1e1486fb5ddea23c6bb8eb53957c36934bcfa1388abf45886" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'a>::go_add_tx": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'a>::go_add_tx", + "comment_kind": "OuterDoc", + "comment_text": " Moves the interface to Add TX page\n", + "comment_hash": "771c217243f279035707086ad94ec9c060272779b802d13296cd7af3d091b1cc", + "body_text": "pub fn go_add_tx(&mut self) -> Result<()> {\n *self.page = CurrentUi::AddTx;\n self.add_tx_data.add_tx_status(\n \"Info: Entering Normal Transaction mode.\".to_string(),\n LogType::Info,\n );\n self.reload_add_tx_balance_data()?;\n self.lerp_state.clear();\n\n Ok(())\n }", + "body_hash": "f29215db5b098f52655301089aee8141dd69d487e2d63bffed3b42fa5ae41f62" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'a>::go_chart": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'a>::go_chart", + "comment_kind": "OuterDoc", + "comment_text": " Moves the interface to Chart page\n", + "comment_hash": "bfeb1b4df45c50031adc566319be666fd0072f50fd795b328e524c5aeeed6cee", + "body_text": "pub fn go_chart(&mut self) {\n *self.page = CurrentUi::Chart;\n *self.chart_tab = ChartTab::ModeSelection;\n *self.chart_hidden_mode = false;\n self.lerp_state.clear();\n }", + "body_hash": "1e17607373a3d4fbedbccdda9a603acfdde3053ba008f3ce8b1a14449c30541c" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'a>::go_home": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'a>::go_home", + "comment_kind": "OuterDoc", + "comment_text": " Moves the interface to Home page\n", + "comment_hash": "6d85cc5ef9c764da431c8c5f52861f0522c67316b1e2668846c7495e13e2dcd6", + "body_text": "pub fn go_home(&mut self) {\n *self.page = CurrentUi::Home;\n self.lerp_state.clear();\n }", + "body_hash": "b2c7f6d3dbe0d2e72e134d08766e7bf689625cbbc8618a595b134f9595e7797e" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'a>::go_home_reset": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'a>::go_home_reset", + "comment_kind": "OuterDoc", + "comment_text": " Moves the interface to the Home page and clears both the fields and the\n\n selected widget of the Add TX, Search and Recurring pages\n", + "comment_hash": "be100d86c9855cae2e15c82c32573ad918398a3af32aa3f27c79b2aff148ab77", + "body_text": "pub fn go_home_reset(&mut self) {\n match self.page {\n CurrentUi::AddTx => {\n *self.add_tx_data = TxData::new();\n *self.add_tx_tab = TxTab::Nothing;\n }\n CurrentUi::Search => {\n *self.search_data = TxData::new_empty();\n *self.search_tab = TxTab::Nothing;\n }\n CurrentUi::Recurring => {\n *self.recurring_data = TxData::new_empty();\n *self.recurring_tab = TxTab::Nothing;\n }\n _ => {}\n }\n self.go_home();\n }", + "body_hash": "157f7eff71ce32a15dd70e778be948e3576e848d66f0dd47e1be88aa39ea7b29" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'a>::go_recurring": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'a>::go_recurring", + "comment_kind": "OuterDoc", + "comment_text": " Moves the interface to the Recurring Transactions page\n", + "comment_hash": "be864035a28aa016f9198b49eaf133efe806295e4564fff713a91ff432067c65", + "body_text": "pub fn go_recurring(&mut self) {\n *self.page = CurrentUi::Recurring;\n *self.recurring_tab = TxTab::Nothing;\n self.lerp_state.clear();\n }", + "body_hash": "8b4f1bebe8b569ecdd49fda82e9555e9e40275993f2a37475b1dd784ba0a3594" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'a>::go_search": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'a>::go_search", + "comment_kind": "OuterDoc", + "comment_text": " Moves the interface to Search page\n", + "comment_hash": "314501e372640620d831aa3adf77711b79b559922c916cc3ab19c20d43b8db87", + "body_text": "pub fn go_search(&mut self) {\n *self.page = CurrentUi::Search;\n self.lerp_state.clear();\n }", + "body_hash": "8928b73a014ec6476ad0653fbe1ca891ffeec13c4b8ea7b34c2da50f894b0aee" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'a>::go_summary": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'a>::go_summary", + "comment_kind": "OuterDoc", + "comment_text": " Moves the interface to Summary page\n", + "comment_hash": "a5eee32a6b3d41213040e506945a0ba6db05d44b0544ad30bc094fb4bf3ba916", + "body_text": "pub fn go_summary(&mut self) -> Result<()> {\n *self.page = CurrentUi::Summary;\n *self.summary_tab = SummaryTab::ModeSelection;\n *self.summary_hidden_mode = false;\n self.reload_summary()?;\n self.lerp_state.clear();\n\n Ok(())\n }", + "body_hash": "1a7e2346b960106caf8b7fdad9ea2da383a1d46bd01d10bffac10480b24ad024" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'a>::handle_amount": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'a>::handle_amount", + "comment_kind": "OuterDoc", + "comment_text": " Checks and verifies amount field\n", + "comment_hash": "e603394fe8e8286e67cda82d74728b9a19bc23ef5ccf6957bb80061d86ea157f", + "body_text": "pub fn handle_amount(&mut self) -> Result<()> {\n match self.page {\n CurrentUi::AddTx => self.check_add_tx_amount()?,\n CurrentUi::Search => self.check_search_amount()?,\n CurrentUi::Recurring => self.check_recurring_amount()?,\n _ => {}\n }\n\n Ok(())\n }", + "body_hash": "e5ae0f1438933807b56ec2a65d3c01b2670ca14cab2abac085fbfe4b5f5f8c81" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'a>::handle_date": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'a>::handle_date", + "comment_kind": "OuterDoc", + "comment_text": " Checks and verifies date field\n", + "comment_hash": "dc21dfe134e1d931a58a675d8cf7d29de1c5282ccfe1f0c55705867ba6f9c1cc", + "body_text": "pub fn handle_date(&mut self) {\n match self.page {\n CurrentUi::AddTx => self.check_add_tx_date(),\n CurrentUi::Search => self.check_search_date(),\n CurrentUi::Recurring => self.check_recurring_date(),\n _ => {}\n }\n }", + "body_hash": "f5b079a22bb79f4c7f4e74d53cf8712256b6db5b841baf5f402cc405326ab62d" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'a>::handle_details": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'a>::handle_details", + "comment_kind": "OuterDoc", + "comment_text": " Checks and verifies details field\n", + "comment_hash": "90ed53844d7447c517acd736250aa12439d0057aa90085dcfa3091fa20977339", + "body_text": "pub fn handle_details(&mut self) {\n match self.page {\n CurrentUi::AddTx => self.check_add_tx_details(),\n CurrentUi::Search => self.check_search_details(),\n CurrentUi::Recurring => self.check_recurring_details(),\n _ => {}\n }\n self.check_autofill();\n }", + "body_hash": "061ff5bd3d9b4accaf4a943197148c4aa3ba89266cc451ec47546a1dc4e3a6a8" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'a>::handle_down_arrow": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'a>::handle_down_arrow", + "comment_kind": "OuterDoc", + "comment_text": " Handles down arrow key press for multiple pages\n", + "comment_hash": "644e5f1d55bffe935422f99f35ad7ef302ae744f148edebfc1a01dd8ccd5ddf0", + "body_text": "pub fn handle_down_arrow(&mut self) {\n let step_type = StepType::StepDown;\n match self.page {\n CurrentUi::Home => self.do_home_down(),\n CurrentUi::AddTx => self.do_add_tx_step(step_type),\n CurrentUi::Summary => self.do_summary_down(),\n CurrentUi::Chart => self.do_chart_down(),\n CurrentUi::Search => self.do_search_step(step_type),\n CurrentUi::Activity => self.do_activity_down(),\n CurrentUi::Recurring => self.do_recurring_step(step_type),\n CurrentUi::Initial => {}\n }\n self.check_autofill();\n }", + "body_hash": "cc74a0f285e113fa0284867591a2f7bec8b50923771b542ac1562a48fc78c796" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'a>::handle_left_arrow": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'a>::handle_left_arrow", + "comment_kind": "OuterDoc", + "comment_text": " Handles left arrow key press for multiple pages\n", + "comment_hash": "e8b6d7c7b4156f5d11bdade648f8873b37d1e17b6c0b52c829c23db393cc9c48", + "body_text": "pub fn handle_left_arrow(&mut self) -> Result<()> {\n match self.page {\n CurrentUi::Home => match self.home_tab {\n HomeTab::Months => {\n self.home_months.previous();\n self.reload_home_table(true)?;\n }\n HomeTab::Years => {\n self.home_years.previous_yearly();\n self.home_months.set_index_zero();\n self.reload_home_table(true)?;\n }\n HomeTab::Table => {}\n },\n CurrentUi::AddTx => self.add_tx_data.move_index_left(self.add_tx_tab),\n CurrentUi::Search => self.search_data.move_index_left(self.search_tab),\n CurrentUi::Recurring => self.recurring_data.move_index_left(self.recurring_tab),\n CurrentUi::Chart => {\n if !*self.chart_hidden_mode {\n match self.chart_tab {\n ChartTab::ModeSelection => {\n self.chart_modes.previous();\n self.lerp_state.clear();\n self.reload_chart_data()?;\n }\n ChartTab::Years => {\n self.chart_years.previous_yearly();\n self.lerp_state.clear();\n self.chart_months.set_index_zero();\n self.reload_chart_data()?;\n }\n ChartTab::Months => {\n self.chart_months.previous();\n self.lerp_state.clear();\n self.reload_chart_data()?;\n }\n ChartTab::TxMethods => {\n self.chart_tx_methods.previous();\n }\n }\n }\n }\n CurrentUi::Summary => {\n if !*self.summary_hidden_mode {\n match self.summary_tab {\n SummaryTab::ModeSelection => {\n self.summary_modes.previous();\n self.reload_summary()?;\n }\n SummaryTab::Years => {\n self.summary_months.set_index_zero();\n self.summary_years.previous_yearly();\n self.reload_summary()?;\n }\n SummaryTab::Months => {\n self.summary_months.previous();\n self.reload_summary()?;\n }\n SummaryTab::Table => {}\n }\n }\n }\n CurrentUi::Activity => match self.activity_tab {\n ActivityTab::Years => {\n self.activity_months.set_index_zero();\n self.activity_years.previous_yearly();\n self.reload_activity_table()?;\n }\n ActivityTab::Months => {\n self.activity_months.previous();\n self.reload_activity_table()?;\n }\n ActivityTab::List => {}\n },\n CurrentUi::Initial => {}\n }\n\n Ok(())\n }", + "body_hash": "b21036c53fc0d863c23ca04a486863c1fad5a0e6c2ab444c8f9fd0686c060a27" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'a>::handle_number_press": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'a>::handle_number_press", + "comment_kind": "OuterDoc", + "comment_text": " Handles all number keypresses and selects relevant input field\n", + "comment_hash": "15550cbcd5860912d8f544a97d5503f5c06ef2c4b25381b8541d22e1fe0d6914", + "body_text": "pub fn handle_number_press(&mut self) {\n match self.page {\n CurrentUi::AddTx => match self.add_tx_data.get_tx_type() {\n TxType::IncomeExpense => match self.key.code {\n KeyCode::Char('1') => *self.add_tx_tab = TxTab::Date,\n KeyCode::Char('2') => *self.add_tx_tab = TxTab::Details,\n KeyCode::Char('3') => *self.add_tx_tab = TxTab::TxType,\n KeyCode::Char('4') => *self.add_tx_tab = TxTab::FromMethod,\n KeyCode::Char('5') => *self.add_tx_tab = TxTab::Amount,\n KeyCode::Char('6') => *self.add_tx_tab = TxTab::Tags,\n _ => {}\n },\n TxType::Transfer => match self.key.code {\n KeyCode::Char('1') => *self.add_tx_tab = TxTab::Date,\n KeyCode::Char('2') => *self.add_tx_tab = TxTab::Details,\n KeyCode::Char('3') => *self.add_tx_tab = TxTab::TxType,\n KeyCode::Char('4') => *self.add_tx_tab = TxTab::FromMethod,\n KeyCode::Char('5') => *self.add_tx_tab = TxTab::ToMethod,\n KeyCode::Char('6') => *self.add_tx_tab = TxTab::Amount,\n KeyCode::Char('7') => *self.add_tx_tab = TxTab::Tags,\n _ => {}\n },\n },\n CurrentUi::Search => match self.search_data.get_tx_type() {\n TxType::IncomeExpense => match self.key.code {\n KeyCode::Char('1') => *self.search_tab = TxTab::Date,\n KeyCode::Char('2') => *self.search_tab = TxTab::Details,\n KeyCode::Char('3') => *self.search_tab = TxTab::TxType,\n KeyCode::Char('4') => *self.search_tab = TxTab::FromMethod,\n KeyCode::Char('5') => *self.search_tab = TxTab::Amount,\n KeyCode::Char('6') => *self.search_tab = TxTab::Tags,\n _ => {}\n },\n TxType::Transfer => match self.key.code {\n KeyCode::Char('1') => *self.search_tab = TxTab::Date,\n KeyCode::Char('2') => *self.search_tab = TxTab::Details,\n KeyCode::Char('3') => *self.search_tab = TxTab::TxType,\n KeyCode::Char('4') => *self.search_tab = TxTab::FromMethod,\n KeyCode::Char('5') => *self.search_tab = TxTab::ToMethod,\n KeyCode::Char('6') => *self.search_tab = TxTab::Amount,\n KeyCode::Char('7') => *self.search_tab = TxTab::Tags,\n _ => {}\n },\n },\n CurrentUi::Recurring => {\n let tabs = recurring_number_jump_tabs(\n self.recurring_data.get_tx_type(),\n self.recurring_data.get_frequency(),\n );\n\n if let KeyCode::Char(c) = self.key.code\n && let Some(digit) = c.to_digit(10)\n && let Some(tab) = tabs.into_iter().nth(digit_to_tab_index(digit))\n {\n *self.recurring_tab = tab;\n }\n }\n _ => {}\n }\n self.go_correct_index();\n self.check_autofill();\n }", + "body_hash": "c66eaadde04458b2377544a929a207c625e13292b7220a538cd6754c4aa1c13a" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'a>::handle_recurring_field": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'a>::handle_recurring_field", + "comment_kind": "OuterDoc", + "comment_text": " Checks and verifies the recurrence-specific fields (frequency, interval, day\n\n value, month, end date)\n", + "comment_hash": "d88cac25120770172c9c20e93fe639aaa1461b4267ae3d9d10c5af1ea338ec87", + "body_text": "pub fn handle_recurring_field(&mut self) -> Result<()> {\n match self.recurring_tab {\n TxTab::Frequency => self.check_recurring_frequency(),\n TxTab::RecurInterval => self.check_recurring_recur_interval(),\n TxTab::RecurValue => self.check_recurring_recur_value(),\n TxTab::RecurMonth => self.check_recurring_recur_month(),\n TxTab::EndDate => self.check_recurring_end_date(),\n _ => {}\n }\n self.check_autofill();\n\n Ok(())\n }", + "body_hash": "27c738c82a40bd8c7abfbad11040410eceef586df31852b273618894ec9b8552" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'a>::handle_right_arrow": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'a>::handle_right_arrow", + "comment_kind": "OuterDoc", + "comment_text": " Handles right arrow key press for multiple pages\n", + "comment_hash": "02e274679ed6d519c1e319ebc19c0798b003dd1b529f44da8d95d5a288348623", + "body_text": "pub fn handle_right_arrow(&mut self) -> Result<()> {\n match self.page {\n CurrentUi::Home => match self.home_tab {\n HomeTab::Months => {\n self.home_months.next();\n self.reload_home_table(true)?;\n }\n HomeTab::Years => {\n self.home_years.next_yearly();\n self.home_months.set_index_zero();\n self.reload_home_table(true)?;\n }\n HomeTab::Table => {}\n },\n CurrentUi::AddTx => self.add_tx_data.move_index_right(self.add_tx_tab),\n CurrentUi::Search => {\n self.search_data.move_index_right(self.search_tab);\n self.lerp_state.clear_lerp(SEARCH_TABLE_ID);\n }\n CurrentUi::Recurring => {\n self.recurring_data.move_index_right(self.recurring_tab);\n self.lerp_state.clear_lerp(RECURRING_TABLE_ID);\n }\n CurrentUi::Chart => {\n if !*self.chart_hidden_mode {\n match self.chart_tab {\n ChartTab::ModeSelection => {\n self.lerp_state.clear();\n self.chart_modes.next();\n self.reload_chart_data()?;\n }\n ChartTab::Years => {\n self.lerp_state.clear();\n self.chart_years.next_yearly();\n self.chart_months.set_index_zero();\n self.reload_chart_data()?;\n }\n ChartTab::Months => {\n self.lerp_state.clear();\n self.chart_months.next();\n self.reload_chart_data()?;\n }\n ChartTab::TxMethods => {\n self.chart_tx_methods.next();\n }\n }\n }\n }\n CurrentUi::Summary => {\n if !*self.summary_hidden_mode {\n match self.summary_tab {\n SummaryTab::ModeSelection => {\n self.summary_modes.next();\n self.reload_summary()?;\n }\n SummaryTab::Years => {\n self.summary_months.set_index_zero();\n self.summary_years.next_yearly();\n self.reload_summary()?;\n }\n SummaryTab::Months => {\n self.summary_months.next();\n self.reload_summary()?;\n }\n SummaryTab::Table => {}\n }\n\n self.lerp_state.clear_lerp(SUMMARY_TABLE_ID);\n }\n }\n CurrentUi::Activity => {\n match self.activity_tab {\n ActivityTab::Years => {\n self.activity_months.set_index_zero();\n self.activity_years.next_yearly();\n self.reload_activity_table()?;\n }\n ActivityTab::Months => {\n self.activity_months.next();\n self.reload_activity_table()?;\n }\n ActivityTab::List => {}\n };\n\n self.lerp_state.clear_lerp(ACTIVITY_TABLE_ID);\n }\n CurrentUi::Initial => {}\n }\n\n Ok(())\n }", + "body_hash": "9d8bd9c466cf63af77c8d66d651048805d92235ddda78687d2ee086a4e5f48e4" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'a>::handle_tags": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'a>::handle_tags", + "comment_kind": "OuterDoc", + "comment_text": " Checks and verifies tags field\n", + "comment_hash": "18fa07d026beb2fc875e49e30503a5904bffa670d0bf27855c88fd178f82ad2b", + "body_text": "pub fn handle_tags(&mut self) {\n match self.page {\n CurrentUi::AddTx => self.check_add_tx_tags(),\n CurrentUi::Search => self.check_search_tags(),\n CurrentUi::Recurring => self.check_recurring_tags(),\n _ => {}\n }\n self.check_autofill();\n }", + "body_hash": "06f113b1e0c02e553efb03a4f544ffbb1fcf1240a1f1cf0044fbfc617dc09026" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'a>::handle_tx_method": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'a>::handle_tx_method", + "comment_kind": "OuterDoc", + "comment_text": " Checks and verifies TX method field\n", + "comment_hash": "c5813563620fad6d1f379990bc0f5cca4d3984c2ad7ee0ce394a89585fe513cd", + "body_text": "pub fn handle_tx_method(&mut self) -> Result<()> {\n match self.page {\n CurrentUi::AddTx => match self.add_tx_tab {\n TxTab::FromMethod => self.check_add_tx_from()?,\n TxTab::ToMethod => self.check_add_tx_to()?,\n _ => {}\n },\n CurrentUi::Search => match self.search_tab {\n TxTab::FromMethod => self.check_search_from(),\n TxTab::ToMethod => self.check_search_to(),\n _ => {}\n },\n CurrentUi::Recurring => match self.recurring_tab {\n TxTab::FromMethod => self.check_recurring_from()?,\n TxTab::ToMethod => self.check_recurring_to()?,\n _ => {}\n },\n _ => {}\n }\n self.check_autofill();\n\n Ok(())\n }", + "body_hash": "e165f587e206ede2f29bb615d5ce19ff0b80d5b6f09966d12a0d96e8a4fd0682" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'a>::handle_tx_type": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'a>::handle_tx_type", + "comment_kind": "OuterDoc", + "comment_text": " Checks and verifies TX type field\n", + "comment_hash": "d8f2f1127fe5081d33069137504788f5aed08dc7fa3b6af5f2c9ab1abcaa2812", + "body_text": "pub fn handle_tx_type(&mut self) -> Result<()> {\n match self.page {\n CurrentUi::AddTx => self.check_add_tx_type()?,\n CurrentUi::Search => self.check_search_type(),\n CurrentUi::Recurring => self.check_recurring_type()?,\n _ => {}\n }\n self.check_autofill();\n\n Ok(())\n }", + "body_hash": "f2ffa080272b6fa6ebfd10e958f93796b7513addb3b740a8fd76b2e55db6adf1" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'a>::handle_up_arrow": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'a>::handle_up_arrow", + "comment_kind": "OuterDoc", + "comment_text": " Handles up arrow key press for multiple pages\n", + "comment_hash": "8993c4184b0f9899c8f6a543b1a2f3c88c6eb6239f7ba21292668a56376e8a0a", + "body_text": "pub fn handle_up_arrow(&mut self) {\n let step_type = StepType::StepUp;\n match self.page {\n CurrentUi::Home => self.do_home_up(),\n CurrentUi::AddTx => self.do_add_tx_step(step_type),\n CurrentUi::Summary => self.do_summary_up(),\n CurrentUi::Chart => self.do_chart_up(),\n CurrentUi::Search => self.do_search_step(step_type),\n CurrentUi::Activity => self.do_activity_up(),\n CurrentUi::Recurring => self.do_recurring_step(step_type),\n CurrentUi::Initial => {}\n }\n self.check_autofill();\n }", + "body_hash": "c304a341e0666ff3e942465383754944c0184406c781d0f9916f3a2072d7fd1a" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'a>::handle_update_popup": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'a>::handle_update_popup", + "comment_kind": "OuterDoc", + "comment_text": " Handles Enter key press if there is a new update and the update popup is on\n", + "comment_hash": "a48ce77044ee6a1b30fff5de166d9c563f9dc41f108aab15cae88ff843340de4", + "body_text": "pub fn handle_update_popup(&mut self) -> Result<(), HandlingOutput> {\n *self.popup_status = PopupType::Nothing;\n\n if self.key.code == KeyCode::Enter {\n open::that(\"https://github.com/TheRustyPickle/Rex/releases/latest\")\n .map_err(|_| HandlingOutput::PrintNewUpdate)?;\n }\n Ok(())\n }", + "body_hash": "1f2182d14c63039801e30c8e3903142e409b8139bf749938d465379e1d459c69" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'a>::home_delete_tx": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'a>::home_delete_tx", + "comment_kind": "OuterDoc", + "comment_text": " Deletes the selected transaction and reloads pages\n", + "comment_hash": "212c62443b50901bb68940529f7351a541e7d7161eaac398263863ad18c4ca5f", + "body_text": "pub fn home_delete_tx(&mut self) -> Result<()> {\n let Some(index) = self.home_table.state.selected() else {\n return Ok(());\n };\n\n let target_tx = self.home_txs.get_tx(index);\n self.conn.delete_tx(target_tx)?;\n\n \n self.reload_home_table(false)?;\n self.reload_chart_data()?;\n self.reload_summary()?;\n self.reset_search_data();\n self.reload_activity_table()?;\n *self.add_tx_data = TxData::new();\n self.reload_add_tx_balance_data()?;\n\n if index == 0 {\n self.home_table.state.select(None);\n *self.home_tab = HomeTab::Months;\n } else {\n self.home_table.state.select(Some(index - 1));\n }\n\n Ok(())\n }", + "body_hash": "711e4e41f8fd0d41847ef1c8be693b88e53c88a6024dcf40f84138590d15e432" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'a>::home_delete_tx#free:26960bc89468": { + "kind": "Free", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'a>::home_delete_tx#free:26960bc89468", + "comment_kind": "Line", + "comment_text": "// Transaction deleted so reload the data again", + "comment_hash": "26960bc894681a1ac168a648913a30ba62e957ffd473094f9f91d18ba3978e8d", + "body_text": "self.reload_home_table(false)?;\nself.reload_chart_data()?;\nself.reload_summary()?;\nself.reset_search_data();\nself.reload_activity_table()?;\n*self.add_tx_data = TxData::new();\nself.reload_add_tx_balance_data()?;\nif index == 0 {} else {}\nOk(())\n}", + "body_hash": "e61298885cc6cc17cc383080cb16a9ec4eafdc5a7e051daa73f0be7e6f655e7e" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'a>::home_edit_tx": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'a>::home_edit_tx", + "comment_kind": "OuterDoc", + "comment_text": " Based on transaction Selected, opens Add TX page and\n\n allocates the data of the TX to the input boxes\n", + "comment_hash": "ce340221aeac6a906e22b0d896bbac74a869b9cc2017c0b67fe830657f9370ba", + "body_text": "pub fn home_edit_tx(&mut self) -> Result<()> {\n if let Some(index) = self.home_table.state.selected() {\n let target_tx = self.home_txs.get_tx(index);\n *self.add_tx_data = TxData::from_full_tx(target_tx, true, false);\n *self.page = CurrentUi::AddTx;\n self.add_tx_data.add_tx_status(\n \"Info: Entering Transaction edit mode. Press C to reset.\".to_string(),\n LogType::Info,\n );\n self.reload_add_tx_balance_data()?;\n self.lerp_state.clear();\n }\n\n Ok(())\n }", + "body_hash": "fc2fb329620790cbb1c646d2ccda25a05eae7297532d4865774ee6dc98d7ddc7" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'a>::recurring_delete_selected": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'a>::recurring_delete_selected", + "comment_kind": "OuterDoc", + "comment_text": " Deletes the selected recurring rule. Previously materialized transactions are\n\n untouched - only the schedule itself is removed.\n", + "comment_hash": "5503c49fe07d4212a92ba7edd60372d5f45977c79287b9821ad59716aea0aaff", + "body_text": "pub fn recurring_delete_selected(&mut self) -> Result<()> {\n let Some(index) = self.recurring_table.state.selected() else {\n return Ok(());\n };\n\n let target_id = self.recurring_txs[index].id;\n self.conn.delete_recurring_tx(target_id)?;\n\n self.reload_recurring_table()?;\n\n if self.recurring_table.items.is_empty() {\n self.recurring_table.state.select(None);\n } else if index >= self.recurring_table.items.len() {\n self.recurring_table\n .state\n .select(Some(self.recurring_table.items.len() - 1));\n }\n\n Ok(())\n }", + "body_hash": "5e9e9e6694fdbeee7857eb901f291264ff7897f9f0cf994e4ca9a1f5bb54035c" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'a>::recurring_edit_selected": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'a>::recurring_edit_selected", + "comment_kind": "OuterDoc", + "comment_text": " Loads the selected recurring rule from the table into the form for editing\n", + "comment_hash": "b2aa241f26030ad8b3e6014fb9c2dc7453bf4dc70ae23f46e26e782d7e541dca", + "body_text": "pub fn recurring_edit_selected(&mut self) {\n if let Some(index) = self.recurring_table.state.selected() {\n let target = &self.recurring_txs[index];\n *self.recurring_data = TxData::from_full_recurring_tx(target, true);\n *self.recurring_tab = TxTab::Nothing;\n }\n }", + "body_hash": "a93016215d7490e66d68ca7322f51b8d83e361ac4e6c4fe3c7032c6b89b135f4" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'a>::recurring_toggle_pause": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'a>::recurring_toggle_pause", + "comment_kind": "OuterDoc", + "comment_text": " Pauses/unpauses the selected recurring rule\n", + "comment_hash": "78e5fc977e8b8b4bf6e90bd94b5dfbaf90c487e08c599cf8a66b7fddcb520d7e", + "body_text": "pub fn recurring_toggle_pause(&mut self) -> Result<()> {\n if let Some(index) = self.recurring_table.state.selected() {\n let target = &self.recurring_txs[index];\n let new_paused = !target.is_paused;\n let id = target.id;\n\n self.conn.set_recurring_paused(id, new_paused)?;\n self.reload_recurring_table()?;\n }\n\n Ok(())\n }", + "body_hash": "9fe0837664f13b1e2c182265c8fe2d147c295dc3bf631785b276852395b1834d" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'a>::save_recurring_tx": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'a>::save_recurring_tx", + "comment_kind": "OuterDoc", + "comment_text": " Creates a new recurring transaction rule, or saves changes if one is being edited\n", + "comment_hash": "79c12bfdabb3f1cd93827f218d939bf094594265fd7b0935f39b3c562584d30d", + "body_text": "pub fn save_recurring_tx(&mut self) -> Result<()> {\n let status = self.recurring_data.add_recurring_tx(self.conn);\n\n match status {\n Ok(()) => {\n *self.recurring_data = TxData::new_empty();\n *self.recurring_tab = TxTab::Nothing;\n \n \n \n self.reload_recurring_table()?;\n self.reload_home_table(true)?;\n self.reload_chart_data()?;\n self.reload_summary()?;\n self.reset_search_data();\n self.reload_activity_table()?;\n }\n Err(e) => self\n .recurring_data\n .add_tx_status(e.to_string(), LogType::Error),\n }\n\n Ok(())\n }", + "body_hash": "e4b2e9a4268da0e76fd5a07c3e4414587c02a18c418ae2f665e38fae8ddca308" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'a>::save_recurring_tx#free:121831a24e8e": { + "kind": "Free", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'a>::save_recurring_tx#free:121831a24e8e", + "comment_kind": "Line", + "comment_text": "// new recurring tx can potentially add new pending txs, in case the start date\n// matches or below the current date. In such a case if new txs are added but tables\n// are not reloaded, stale data will be shown.", + "comment_hash": "121831a24e8edc97267e9861ca07800ce751b5da572a9a96ba6fb6a228d6b30e", + "body_text": "self.reload_recurring_table()?;\nself.reload_home_table(true)?;\nself.reload_chart_data()?;\nself.reload_summary()?;\nself.reset_search_data();\nself.reload_activity_table()?;\n}", + "body_hash": "4875d44fe537522a60d0b8c5d81b63fe24c5488eecae16416142671bb4af7009" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'a>::search_delete_tx": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'a>::search_delete_tx", + "comment_kind": "OuterDoc", + "comment_text": " Delete a transaction from search page\n", + "comment_hash": "324bdaab252c1b0d03f3fea84f7a89603e63e1e6029767136e5069e83e0dc061", + "body_text": "pub fn search_delete_tx(&mut self) -> Result<()> {\n let Some(index) = self.search_table.state.selected() else {\n return Ok(());\n };\n\n let target_tx = self.search_txs.get_tx(index);\n self.conn.delete_tx(target_tx)?;\n\n \n self.reload_home_table(true)?;\n self.reload_chart_data()?;\n self.reload_summary()?;\n self.reset_search_data();\n\n Ok(())\n }", + "body_hash": "6da9da17c1cd33c546dd218999d48e4eb5075dffa8e6b720773970b9f27bb546" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'a>::search_delete_tx#free:26960bc89468": { + "kind": "Free", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'a>::search_delete_tx#free:26960bc89468", + "comment_kind": "Line", + "comment_text": "// Transaction deleted so reload the data again", + "comment_hash": "26960bc894681a1ac168a648913a30ba62e957ffd473094f9f91d18ba3978e8d", + "body_text": "self.reload_home_table(true)?;\nself.reload_chart_data()?;\nself.reload_summary()?;\nself.reset_search_data();\nOk(())\n}", + "body_hash": "c063e77a76abd6ebf7a6c346bfbca9005ab588da6aff1adf24e339afc80c3345" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'a>::search_edit_tx": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'a>::search_edit_tx", + "comment_kind": "OuterDoc", + "comment_text": " Start editing TX from a search result\n", + "comment_hash": "b377308b6acaf048a937e682d2759bd8f465e6d69d0bbdabaa8de2e8160a2ad5", + "body_text": "pub fn search_edit_tx(&mut self) -> Result<()> {\n if let Some(a) = self.search_table.state.selected() {\n let target_tx = &self.search_txs.get_tx(a);\n\n *self.page = CurrentUi::AddTx;\n *self.add_tx_data = TxData::from_full_tx(target_tx, true, true);\n self.reload_add_tx_balance_data()?;\n }\n\n Ok(())\n }", + "body_hash": "fe84b3cd36c9c2e5b4049e4346306d42451a249cce75f60477169d2ce4a4ddd7" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'a>::search_tag": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'a>::search_tag", + "comment_kind": "OuterDoc", + "comment_text": " If Enter is pressed on Summary page while a tag is selected\n\n go to search page and search for it\n", + "comment_hash": "cd60a84ee3e4dd0b7707b4616858fab0b60d4b4d027e64ea44d1ec9e7eaa37d9", + "body_text": "pub fn search_tag(&mut self) -> Result<()> {\n if let SummaryTab::Table = self.summary_tab\n && let Some(index) = self.summary_table.state.selected()\n {\n let tag_name = &self.summary_table.items[index][0];\n let search_param = TxData::custom(\"\", \"\", \"\", \"\", \"\", \"\", tag_name, 0);\n *self.search_data = search_param;\n self.go_search();\n self.search_tx()?;\n }\n\n Ok(())\n }", + "body_hash": "50ee8a059bad95b2fbad6cf36d715b837bcaab427a6760983a5550cc400324e6" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'a>::select_date_field": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'a>::select_date_field", + "comment_kind": "OuterDoc", + "comment_text": " No field selected on add TX or search but enter is pressed then\n\n select the Date field\n", + "comment_hash": "0f87280bedbe10f0095f33ba413d302b34e60d39667ade0587131d757ea26510", + "body_text": "pub fn select_date_field(&mut self) {\n match self.page {\n CurrentUi::AddTx => *self.add_tx_tab = TxTab::Date,\n CurrentUi::Search => *self.search_tab = TxTab::Date,\n CurrentUi::Recurring => *self.recurring_tab = TxTab::Date,\n _ => {}\n }\n self.go_correct_index();\n }", + "body_hash": "211276491e9a4b822a906a87d2c4ca420b368c8a72192f87e5c0641287070f24" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'a>::show_activity_tx_details": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'a>::show_activity_tx_details", + "comment_kind": "OuterDoc", + "comment_text": " Opens a popup that shows the details of the selected activity TX details on the Activity page\n", + "comment_hash": "7d1287bc66c5b53fbf69a36c2f4b2e1c38e730812c949bae3c516d48071baaa1", + "body_text": "pub fn show_activity_tx_details(&mut self) -> Result<()> {\n if let Some(index) = self.activity_table.state.selected() {\n let activity_txs = self.activity_view.get_activity_txs(index);\n\n let mut popup_text = String::new();\n\n if activity_txs.len() == 2 {\n for (index, tx) in activity_txs.into_iter().enumerate() {\n let tx_details = tx.details.clone().unwrap_or_default();\n write!(&mut popup_text, \"Transaction {}: {tx_details}\", index + 1)?;\n if index == 0 {\n popup_text += \"\\n\\n\";\n }\n }\n } else {\n let tx_details = &activity_txs[0].details.clone().unwrap_or_default();\n write!(&mut popup_text, \"Transaction 1: {tx_details}\")?;\n }\n let status = InfoPopupState::ShowDetails(popup_text);\n *self.popup_status = PopupType::new_info(status);\n }\n\n Ok(())\n }", + "body_hash": "3c1c2c8bafd35e6e8a3c0c6e512ded8bd43647e225a9ec1bdbb6be4e8fe15be7" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'a>::show_home_tx_details": { + "kind": "Function", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'a>::show_home_tx_details", + "comment_kind": "OuterDoc", + "comment_text": " Opens a popup that shows the details of the selected transaction on the Homepage\n", + "comment_hash": "bed21ea0f2de93c81c2d7b96f7fb52cbffc4555eaf99c337b403f257bd7589d2", + "body_text": "pub fn show_home_tx_details(&mut self) {\n if let Some(index) = self.home_table.state.selected() {\n let selected_tx = self.home_txs.get_tx(index);\n let tx_details = selected_tx.details.clone().unwrap_or_default();\n\n let status = InfoPopupState::ShowDetails(tx_details);\n *self.popup_status = PopupType::new_info(status);\n }\n }", + "body_hash": "70a0e4b5d30e14b376f4e855e7b3cb60f784cea4d577b32551427b34887ed8f4" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'a>::switch_tx_position_down#free:21fddb0a2dce": { + "kind": "Free", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'a>::switch_tx_position_down#free:21fddb0a2dce", + "comment_kind": "Line", + "comment_text": "// Nothing to swap with 1 tx, or when the last one is\n// selected as it cannot move down", + "comment_hash": "21fddb0a2dce1f1fc740741176a3e3c29be4140a044d9b6f8ee771803e665f24", + "body_text": "if self.home_table.items.len() <= 1 || index == self.home_table.items.len() - 1 {}\nlet reload_stuff = self\n .conn\n .swap_tx_position(index, index + 1, self.home_txs)?;\nif reload_stuff {}\n}", + "body_hash": "0d5b646077fc16dd0adf9f6f505f3653020ea75ac29198ac630199c663e5cab0" + }, + "tui/src/key_checker/key_handler.rs|crate::impl InputKeyHandler<'a>::switch_tx_position_up#free:f21421411ef8": { + "kind": "Free", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::impl InputKeyHandler<'a>::switch_tx_position_up#free:f21421411ef8", + "comment_kind": "Line", + "comment_text": "// Nothing to swap with 1 tx, or when the first one is\n// selected as it cannot move up", + "comment_hash": "f21421411ef8b38a8dffc25417f58dd5cf7d3a46a3198ebef0eb65ae839ed9e8", + "body_text": "if self.home_table.items.len() <= 1 || index == 0 {}\nlet reload_stuff = self\n .conn\n .swap_tx_position(index, index - 1, self.home_txs)?;\nif reload_stuff {}\n}", + "body_hash": "7e8247d5c31a420e0f987e4b1f7382f7a55ca06d7433006e2f72a24dfae9ff8e" + }, + "tui/src/key_checker/key_handler.rs|crate::number_jump_tests::income_expense_daily_has_nine_fields_both_end_date_and_tags_reachable#free:251b69c070ca": { + "kind": "Free", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::number_jump_tests::income_expense_daily_has_nine_fields_both_end_date_and_tags_reachable#free:251b69c070ca", + "comment_kind": "Line", + "comment_text": "// No 10th field in this combination, so key 0 has nothing to jump to", + "comment_hash": "251b69c070ca554881da214c9f17bb088a9a223c16fd3700d5211161e50943f0", + "body_text": "assert!(tabs.get(digit_to_tab_index(0)).is_none());\n}", + "body_hash": "121bd454df481cb285b519788f00367fd92668647cee0d4ddcd3402bccd9fe31" + }, + "tui/src/key_checker/key_handler.rs|crate::number_jump_tests::income_expense_yearly_has_eleven_fields_tags_has_no_key_left#free:bd7a09583570": { + "kind": "Free", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::number_jump_tests::income_expense_yearly_has_eleven_fields_tags_has_no_key_left#free:bd7a09583570", + "comment_kind": "Line", + "comment_text": "// Tags is the 11th field - out of digits (1-9 then 0 only covers 10)", + "comment_hash": "bd7a09583570793270f664ef53fe3fad141e417c9b7b9033463187d98b5b3cc9", + "body_text": "assert!(tabs.get(10).is_some());\nassert_eq!(tabs[10], TxTab::Tags);\n}", + "body_hash": "71d2597d81e2b5d65a3e41ea13edb030a7191d7067ca128673b7d1280145a6f8" + }, + "tui/src/key_checker/key_handler.rs|crate::number_jump_tests::transfer_yearly_has_twelve_fields_end_date_and_tags_have_no_key_left#free:f45b37ddbf57": { + "kind": "Free", + "file": "tui/src/key_checker/key_handler.rs", + "item_path": "crate::number_jump_tests::transfer_yearly_has_twelve_fields_end_date_and_tags_have_no_key_left#free:f45b37ddbf57", + "comment_kind": "Line", + "comment_text": "// End Date and Tags are the 11th/12th fields here - genuinely out of digits,\n// only reachable via sequential Enter in this one combination", + "comment_hash": "f45b37ddbf5729d5b03dc2ebd3f0be97f6d2114b55dc49f97d6989107de387a1", + "body_text": "assert_eq!(tabs[10], TxTab::EndDate);\nassert_eq!(tabs[11], TxTab::Tags);\n}", + "body_hash": "35d304c7e6f385ff28a1e2de9f7cf50dae3703c18477654fc032ded33ecbc647" + }, + "tui/src/key_checker/recurring.rs|crate::recurring_keys": { + "kind": "Function", + "file": "tui/src/key_checker/recurring.rs", + "item_path": "crate::recurring_keys", + "comment_kind": "OuterDoc", + "comment_text": " Tracks the keys of the Recurring Transactions page and calls relevant function based on it\n", + "comment_hash": "c64636d0356bd322054412731c82c4966475fe7e08bc041c077b8541f4b4e094", + "body_text": "pub fn recurring_keys(handler: &mut InputKeyHandler) -> Result> {\n match handler.popup_status {\n \n PopupType::Nothing => match handler.recurring_tab {\n TxTab::Nothing => match handler.key.code {\n KeyCode::Char('a') => handler.go_add_tx()?,\n KeyCode::Char('r') => handler.go_chart(),\n KeyCode::Char('z') => handler.go_summary()?,\n KeyCode::Char('q') => return Ok(Some(HandlingOutput::QuitUi)),\n KeyCode::Char('j') => handler.do_config_popup(),\n KeyCode::Char('f') => handler.go_home(),\n KeyCode::Char('h') => handler.do_help_popup(),\n KeyCode::Char('s') => handler.save_recurring_tx()?,\n KeyCode::Char('c') => handler.clear_input()?,\n KeyCode::Char('e') => handler.recurring_edit_selected(),\n KeyCode::Char('d') => handler.do_deletion_popup(),\n KeyCode::Char('p') => handler.recurring_toggle_pause()?,\n KeyCode::Char('w') => handler.go_search(),\n KeyCode::Char('y') => handler.go_activity(),\n KeyCode::Char('t') => handler.next_theme()?,\n KeyCode::Up => handler.handle_up_arrow(),\n KeyCode::Down => handler.handle_down_arrow(),\n KeyCode::Enter => handler.select_date_field(),\n KeyCode::Char(c) if c.is_numeric() => {\n handler.handle_number_press();\n }\n _ => {}\n },\n _ => match handler.key.code {\n KeyCode::Right => handler.handle_right_arrow()?,\n KeyCode::Left => handler.handle_left_arrow()?,\n KeyCode::Up => handler.handle_up_arrow(),\n KeyCode::Down => handler.handle_down_arrow(),\n KeyCode::Tab => handler.do_autofill(),\n _ => match handler.recurring_tab {\n TxTab::Date => handler.handle_date(),\n TxTab::Details => handler.handle_details(),\n TxTab::FromMethod | TxTab::ToMethod => handler.handle_tx_method()?,\n TxTab::Amount => handler.handle_amount()?,\n TxTab::TxType => handler.handle_tx_type()?,\n TxTab::Tags => handler.handle_tags(),\n TxTab::Frequency\n | TxTab::RecurInterval\n | TxTab::RecurValue\n | TxTab::RecurMonth\n | TxTab::EndDate => handler.handle_recurring_field()?,\n TxTab::Nothing => {}\n },\n },\n },\n _ => return popup_keys(handler),\n }\n\n Ok(None)\n}", + "body_hash": "58dfe1f15a6cbf8cc065456347234e693f3460c500606dbd504b95d5b2f8d3b8" + }, + "tui/src/key_checker/search.rs|crate::search_keys": { + "kind": "Function", + "file": "tui/src/key_checker/search.rs", + "item_path": "crate::search_keys", + "comment_kind": "OuterDoc", + "comment_text": " Tracks the keys of the Search page and calls relevant function based on it\n", + "comment_hash": "5244faa2dc4cdf6c4551eb8a9cea60acac11b0d9a60628674ba785e727606cde", + "body_text": "pub fn search_keys(handler: &mut InputKeyHandler) -> Result> {\n match handler.popup_status {\n \n PopupType::Nothing => match handler.search_tab {\n TxTab::Nothing => match handler.key.code {\n KeyCode::Char('a') => handler.go_add_tx()?,\n KeyCode::Char('r') => handler.go_chart(),\n KeyCode::Char('z') => handler.go_summary()?,\n KeyCode::Char('q') => return Ok(Some(HandlingOutput::QuitUi)),\n KeyCode::Char('j') => handler.do_config_popup(),\n KeyCode::Char('f') => handler.go_home(),\n KeyCode::Char('h') => handler.do_help_popup(),\n KeyCode::Char('s') => handler.search_tx()?,\n KeyCode::Char('c') => handler.clear_input()?,\n KeyCode::Char('x') => handler.change_search_date_type(),\n KeyCode::Char('e') => handler.search_edit_tx()?,\n KeyCode::Char('d') => handler.do_deletion_popup(),\n KeyCode::Char('y') => handler.go_activity(),\n KeyCode::Char('u') => handler.go_recurring(),\n KeyCode::Char('t') => handler.next_theme()?,\n KeyCode::Up => handler.handle_up_arrow(),\n KeyCode::Down => handler.handle_down_arrow(),\n KeyCode::Enter => handler.select_date_field(),\n KeyCode::Char(c) if c.is_numeric() => {\n handler.handle_number_press();\n }\n _ => {}\n },\n _ => match handler.key.code {\n KeyCode::Right => handler.handle_right_arrow()?,\n KeyCode::Left => handler.handle_left_arrow()?,\n KeyCode::Up => handler.handle_up_arrow(),\n KeyCode::Down => handler.handle_down_arrow(),\n KeyCode::Tab => handler.do_autofill(),\n _ => match handler.search_tab {\n TxTab::Date => handler.handle_date(),\n TxTab::Details => handler.handle_details(),\n TxTab::FromMethod | TxTab::ToMethod => handler.handle_tx_method()?,\n TxTab::Amount => handler.handle_amount()?,\n TxTab::TxType => handler.handle_tx_type()?,\n TxTab::Tags => handler.handle_tags(),\n TxTab::Nothing\n | TxTab::Frequency\n | TxTab::RecurInterval\n | TxTab::RecurValue\n | TxTab::RecurMonth\n | TxTab::EndDate => {}\n },\n },\n },\n _ => return popup_keys(handler),\n }\n\n Ok(None)\n}", + "body_hash": "3cb2016c8633d2b961a6ffd878d61b90543ac45572507fbd4042c12c8df90063" + }, + "tui/src/key_checker/summary.rs|crate::summary_keys": { + "kind": "Function", + "file": "tui/src/key_checker/summary.rs", + "item_path": "crate::summary_keys", + "comment_kind": "OuterDoc", + "comment_text": " Tracks the keys of the Summary page and calls relevant function based on it\n", + "comment_hash": "3e6c02f8015d16674a3d5b3612ccdf543cfcb799749f905bc2a293481d0d6878", + "body_text": "pub fn summary_keys(handler: &mut InputKeyHandler) -> Result> {\n match handler.popup_status {\n PopupType::Nothing => match handler.key.code {\n KeyCode::Char('a') => handler.go_add_tx()?,\n KeyCode::Char('r') => handler.go_chart(),\n KeyCode::Char('j') => handler.do_config_popup(),\n KeyCode::Char('q') => return Ok(Some(HandlingOutput::QuitUi)),\n KeyCode::Char('f') => handler.go_home(),\n KeyCode::Char('w') => handler.go_search(),\n KeyCode::Char('h') => handler.do_help_popup(),\n KeyCode::Char('z') => handler.do_summary_hidden_mode(),\n KeyCode::Char('x') => handler.change_summary_sort(),\n KeyCode::Char('y') => handler.go_activity(),\n KeyCode::Char('u') => handler.go_recurring(),\n KeyCode::Char('t') => handler.next_theme()?,\n KeyCode::Right => handler.handle_right_arrow()?,\n KeyCode::Left => handler.handle_left_arrow()?,\n KeyCode::Up => handler.handle_up_arrow(),\n KeyCode::Down => handler.handle_down_arrow(),\n KeyCode::Enter => handler.search_tag()?,\n _ => {}\n },\n _ => return popup_keys(handler),\n }\n\n Ok(None)\n}", + "body_hash": "9cd48930010c8e13751a4833d9d4e5cf86dc85d119155a1bc1677122b28b8cd2" + }, + "tui/src/main.rs|crate::main#free:09c90db4bfe2": { + "kind": "Free", + "file": "tui/src/main.rs", + "item_path": "crate::main#free:09c90db4bfe2", + "comment_kind": "Line", + "comment_text": "// The path where the application was opened initially", + "comment_hash": "09c90db4bfe2727710cf2690e4a07c17b9f73465d9afa76f917103e4a53035d5", + "body_text": "let original_dir = current_dir().unwrap();", + "body_hash": "29e4c9954b5d6283023ee8ea8730c29babb92a07be6cd6e550d1d2ab3b85730d" + }, + "tui/src/main.rs|crate::main#free:8f3fb796ebc7": { + "kind": "Free", + "file": "tui/src/main.rs", + "item_path": "crate::main#free:8f3fb796ebc7", + "comment_kind": "Line", + "comment_text": "// Create folder if non-existing then move the current working directory\n// to the OS data directory", + "comment_hash": "8f3fb796ebc71cb655a8119bcaf7ce5edbf7a38ef70a805fefdc6bcbee7b2a74", + "body_text": "let Ok(()) = fs::create_dir_all(&working_path) else {};\nlet Ok(()) = set_current_dir(&working_path) else {};\nlet _ = WriteLogger::init(\n LevelFilter::Info,\n Config::default(),\n File::create(\"logs.log\").unwrap(),\n );\nlet mut migrated_path = working_path.clone();\nmigrated_path.push(\"rex.sqlite\");\nworking_path.push(\"data.sqlite\");\nif let Err(e) = initialize_app(&working_path, &migrated_path, &original_dir) {}\n}", + "body_hash": "7199748a49fb0b8af3fb952dadacf99af1ee81cf1351acebcad6b3c9a75bb8e6" + }, + "tui/src/main.rs|crate::main#free:fc66ea350129": { + "kind": "Free", + "file": "tui/src/main.rs", + "item_path": "crate::main#free:fc66ea350129", + "comment_kind": "Line", + "comment_text": "// The OS based path where data will be stored", + "comment_hash": "fc66ea3501296d4819b86cd73b736d76b2efabf9574120bb67809857a166ddba", + "body_text": "let mut working_path = dir;\nworking_path.push(\"Rex\");", + "body_hash": "acad82b13af2dabb11e0011f7c9dd635bcce3d2789ea3ad991d8964269c1d6bc" + }, + "tui/src/page_handler/initializer.rs|crate::initialize_app": { + "kind": "Function", + "file": "tui/src/page_handler/initializer.rs", + "item_path": "crate::initialize_app", + "comment_kind": "OuterDoc", + "comment_text": " Sets up a terminal, the migrated database and config plus a background\n\n version check, then runs the TUI loop until the user quits\n", + "comment_hash": "9d5717332a7c7e4fe9b0d2ccf958816a18ee85a7f296b805030858e51db94474", + "body_text": "pub fn initialize_app(\n old_db_path: &PathBuf,\n migrated_db_path: &Path,\n original_dir: &PathBuf,\n) -> Result<()> {\n \n \n if !atty::is(Stream::Stdout) && !start_terminal(original_dir.to_str().unwrap()) {\n let mut error_location = PathBuf::from(&original_dir);\n error_location.push(\"Error.txt\");\n\n let mut open = File::create(error_location)?;\n let to_write =\n \"Failed to start a terminal. Please open one manually by executing the binary\"\n .to_string();\n open.write_all(to_write.as_bytes())?;\n process::exit(1);\n }\n\n let result =\n migrate_to_new_schema(old_db_path, migrated_db_path.display().to_string().as_str());\n\n match result {\n Ok(result) => {\n if result {\n println!(\"Restart required after migration. Exiting\");\n process::exit(0);\n }\n }\n Err(e) => {\n println!(\"Failed to migrate to new schema. Error: {e:?}\");\n fs::remove_file(migrated_db_path)?;\n process::exit(1);\n }\n }\n\n let new_update = Arc::new(Mutex::new(None));\n\n let update_clone = new_update.clone();\n\n spawn(move || {\n let new_version = check_version().unwrap_or_default().unwrap_or_default();\n\n let mut lock = update_clone.lock().unwrap();\n *lock = Some(new_version);\n });\n\n if let Err(e) = migrate_config(old_db_path) {\n println!(\"Failed to migrate config. Error: {e:?}\");\n process::exit(1);\n }\n\n let mut config = Config::get_config(&migrated_db_path.to_path_buf())?;\n\n let new_db_path = if let Some(mut location) = config.new_location.clone() {\n let result = set_current_dir(&location);\n\n if let Err(e) = result {\n println!(\n \"Failed to set the new path. Does the path exists? Exiting program. Error: {e}\"\n );\n process::exit(1);\n } else {\n location.push(\"rex.sqlite\");\n location.clone()\n }\n } else {\n migrated_db_path.to_path_buf()\n };\n\n let mut conn = get_conn(new_db_path.display().to_string().as_str());\n\n loop {\n let mut terminal = enter_tui_interface()?;\n let result = start_app(&mut terminal, new_update.clone(), &mut config, &mut conn);\n exit_tui_interface()?;\n\n match result {\n Ok(output) => match output {\n HandlingOutput::QuitUi => {\n drop(conn);\n config.save_backup(&new_db_path.clone());\n break;\n }\n HandlingOutput::PrintNewUpdate => println!(\n \"Could not open browser.\\n\\nLatest Version Link: https://github.com/TheRustyPickle/Rex/releases/latest\"\n ),\n },\n Err(error) => {\n println!(\"{error}\");\n process::exit(1);\n }\n }\n }\n\n Ok(())\n}", + "body_hash": "5ebdbd4600e45d35bdf9c71d727d5fd7b15a4eae1adc2e7658eb666c784923b9" + }, + "tui/src/page_handler/initializer.rs|crate::initialize_app#free:70d6fd6a5e6a": { + "kind": "Free", + "file": "tui/src/page_handler/initializer.rs", + "item_path": "crate::initialize_app#free:70d6fd6a5e6a", + "comment_kind": "Line", + "comment_text": "// Without a terminal, try to start one. If that fails, write the message to\n// Error.txt and exit", + "comment_hash": "70d6fd6a5e6ae67e5cec7b9de4e0442534b2474ffc955c73360b6381c561e22f", + "body_text": "if !atty::is(Stream::Stdout) && !start_terminal(original_dir.to_str().unwrap()) {}\nlet result =\n migrate_to_new_schema(old_db_path, migrated_db_path.display().to_string().as_str());\nmatch result {\n Ok(result) => {}\n Err(e) => {}\n }\nlet new_update = Arc::new(Mutex::new(None));\nlet update_clone = new_update.clone();\nspawn(move || {});\nif let Err(e) = migrate_config(old_db_path) {}\nlet mut config = Config::get_config(&migrated_db_path.to_path_buf())?;\nlet new_db_path = if let Some(mut location) = config.new_location.clone() {} else {};\nlet mut conn = get_conn(new_db_path.display().to_string().as_str());\nloop {}\nOk(())\n}", + "body_hash": "2bd7d4c55a4d73ade4968363cdc97e92ea72b9974a4e1188bb9f24632f9ca274" + }, + "tui/src/page_handler/ui_handler.rs|crate::start_app": { + "kind": "Function", + "file": "tui/src/page_handler/ui_handler.rs", + "item_path": "crate::start_app", + "comment_kind": "OuterDoc", + "comment_text": " Starts the interface and runs the app\n", + "comment_hash": "3700e72df31d9454f3f9d6571ecf421e65469dcb570f26886c09ee4b004fae43", + "body_text": "pub fn start_app(\n terminal: &mut Terminal,\n new_version_data: Arc>>>,\n config: &mut Config,\n conn: &mut DbConn,\n) -> Result {\n \n let mut popup_status = PopupType::Nothing;\n\n let recurring_added = match conn.process_due_recurring_txs() {\n Ok(count) => count,\n Err(e) => {\n let state = InfoPopupState::Error(format!(\"Failed to process recurring TXs: {e}\"));\n popup_status = PopupType::new_info(state);\n 0\n }\n };\n\n let mut theme = Theme::new_index(config.theme_index.unwrap_or(0));\n\n \n \n \n let mut home_months = IndexedData::new_monthly();\n let mut home_years = IndexedData::new_yearly();\n let mut chart_months = IndexedData::new_monthly_no_local();\n let mut chart_years = IndexedData::new_yearly_no_local();\n let mut chart_modes = IndexedData::new_modes();\n let mut chart_tx_methods = IndexedData::new_tx_methods_cumulative(conn);\n let mut summary_months = IndexedData::new_monthly_no_local();\n let mut summary_years = IndexedData::new_yearly_no_local();\n let mut summary_modes = IndexedData::new_modes();\n let mut activity_months = IndexedData::new_monthly();\n let mut activity_years = IndexedData::new_yearly();\n\n \n let mut home_tab = HomeTab::Months;\n\n \n let mut summary_sort = SortingType::Tags;\n\n \n let mut search_txs = SearchView::new_empty();\n\n \n let mut home_txs = conn\n .fetch_txs_with_str(\n home_months.get_selected_value(),\n home_years.get_selected_value(),\n FetchNature::Monthly,\n )\n .unwrap();\n\n \n let mut home_table = TableData::new(home_txs.tx_array());\n\n \n let mut page = CurrentUi::Initial;\n\n \n let mut add_tx_tab = TxTab::Nothing;\n \n let mut chart_tab = ChartTab::ModeSelection;\n \n let mut summary_tab = SummaryTab::ModeSelection;\n \n let mut search_tab = TxTab::Nothing;\n \n let mut search_date_type = DateType::Exact;\n \n let mut activity_tab = ActivityTab::Years;\n \n let mut recurring_tab = TxTab::Nothing;\n\n \n let mut add_tx_data = TxData::new();\n \n let mut search_data = TxData::new_empty();\n \n let mut recurring_data = TxData::new_empty();\n\n \n let mut chart_view = conn\n .get_chart_view_with_str(\n chart_months.get_selected_value(),\n chart_years.get_selected_value(),\n FetchNature::Monthly,\n )\n .unwrap();\n\n \n let mut summary_view = conn\n .get_summary_with_str(\n summary_months.get_selected_value(),\n summary_years.get_selected_value(),\n FetchNature::Monthly,\n )\n .unwrap();\n\n \n let mut activity_view = conn\n .get_activity_view_with_str(\n activity_months.get_selected_value(),\n activity_years.get_selected_value(),\n )\n .unwrap();\n\n \n let mut full_summary = summary_view.generate_summary(None, conn);\n\n \n let mut summary_table = TableData::new(summary_view.tags_array(None, conn));\n\n \n let mut search_table = TableData::new(Vec::new());\n\n \n let mut activity_table = TableData::new(activity_view.get_activity_table());\n\n \n let mut recurring_txs = conn.get_recurring_txs().unwrap();\n\n \n let initial_data = InitialData::load(conn, &recurring_txs, recurring_added);\n\n \n let mut recurring_table = TableData::new(\n recurring_txs\n .iter()\n .map(FullRecurringTx::to_array)\n .collect(),\n );\n\n \n let mut starter_index = 0;\n\n \n let mut chart_hidden_mode = false;\n\n \n let mut chart_hidden_legends = false;\n\n \n let mut summary_hidden_mode = false;\n\n \n let mut chart_activated_methods = conn\n .get_tx_methods_cumulative()\n .into_iter()\n .map(|s| (s, true))\n .collect();\n\n \n let mut add_tx_balance = Vec::new();\n\n let mut lerp_state = LerpState::new(1.0);\n\n let mut version_checked = false;\n\n \n \n \n \n \n \n \n loop {\n if !version_checked {\n let update_lock = new_version_data.lock().unwrap();\n if let Some(data) = update_lock.as_ref() {\n if !data.is_empty() {\n let state = InfoPopupState::NewUpdate(data.to_vec());\n popup_status = PopupType::new_info(state);\n }\n version_checked = true;\n }\n }\n \n if conn.is_tx_method_empty()\n && let PopupType::Nothing = popup_status\n {\n popup_status = PopupType::new_choice_config_forced(&theme);\n }\n\n \n terminal\n .draw(|f| {\n match page {\n CurrentUi::Home => home_ui(\n f,\n &home_months,\n &home_years,\n &mut home_table,\n &home_tab,\n &mut lerp_state,\n &mut home_txs,\n &theme,\n conn,\n ),\n\n CurrentUi::AddTx => add_tx_ui(\n f,\n &mut add_tx_balance,\n &add_tx_data,\n &add_tx_tab,\n &mut lerp_state,\n &theme,\n conn,\n ),\n\n CurrentUi::Initial => initial_ui(f, starter_index, &initial_data, &theme),\n\n CurrentUi::Chart => chart_ui(\n f,\n &chart_months,\n &chart_years,\n &chart_modes,\n &chart_tx_methods,\n &chart_tab,\n chart_hidden_mode,\n chart_hidden_legends,\n &chart_activated_methods,\n &mut lerp_state,\n &chart_view,\n &theme,\n conn,\n ),\n\n CurrentUi::Summary => summary_ui(\n f,\n &summary_months,\n &summary_years,\n &summary_modes,\n &mut summary_table,\n &summary_tab,\n summary_hidden_mode,\n &summary_sort,\n &mut lerp_state,\n &full_summary,\n &theme,\n conn,\n ),\n CurrentUi::Search => search_ui(\n f,\n &search_data,\n &search_tab,\n &mut search_table,\n search_date_type,\n &mut lerp_state,\n &theme,\n ),\n CurrentUi::Activity => activity_ui(\n f,\n &activity_months,\n &activity_years,\n &activity_tab,\n &activity_view,\n &mut activity_table,\n &mut lerp_state,\n &theme,\n ),\n CurrentUi::Recurring => recurring_ui(\n f,\n &recurring_data,\n &recurring_tab,\n &mut recurring_table,\n &mut lerp_state,\n &theme,\n ),\n }\n\n popup_status.show_ui(f, &theme);\n })\n .map_err(|e| {\n let e = e.to_string();\n UiHandlingError::Drawing(e)\n })?;\n\n \n match page {\n CurrentUi::Initial => {\n \n if !poll(Duration::from_millis(40)).map_err(UiHandlingError::Polling)? {\n starter_index = (starter_index + 1) % 27;\n continue;\n }\n }\n CurrentUi::Home\n | CurrentUi::AddTx\n | CurrentUi::Summary\n | CurrentUi::Search\n | CurrentUi::Chart\n | CurrentUi::Activity\n | CurrentUi::Recurring => {\n \n if lerp_state.has_active_lerps()\n && !poll(Duration::from_millis(2)).map_err(UiHandlingError::Polling)?\n {\n continue;\n }\n }\n }\n\n \n if let Event::Key(key) = event::read().map_err(UiHandlingError::Polling)? {\n if key.kind != KeyEventKind::Press {\n continue;\n }\n\n let mut handler = InputKeyHandler::new(\n key,\n &mut page,\n &mut add_tx_balance,\n &mut popup_status,\n &mut add_tx_tab,\n &mut chart_tab,\n &mut summary_tab,\n &mut home_tab,\n &mut add_tx_data,\n &mut chart_view,\n &mut summary_view,\n &mut full_summary,\n &mut home_table,\n &mut home_txs,\n &mut summary_table,\n &mut home_months,\n &mut home_years,\n &mut chart_months,\n &mut chart_years,\n &mut chart_modes,\n &mut chart_tx_methods,\n &mut summary_months,\n &mut summary_years,\n &mut summary_modes,\n &mut summary_sort,\n &mut search_data,\n &mut search_date_type,\n &mut search_tab,\n &mut search_table,\n &mut search_txs,\n &mut activity_months,\n &mut activity_years,\n &mut activity_tab,\n &mut activity_view,\n &mut activity_table,\n &mut recurring_data,\n &mut recurring_tab,\n &mut recurring_table,\n &mut recurring_txs,\n &mut chart_hidden_mode,\n &mut chart_hidden_legends,\n &mut summary_hidden_mode,\n &mut chart_activated_methods,\n &mut lerp_state,\n config,\n &mut theme,\n conn,\n );\n\n let status = match handler.page {\n CurrentUi::Initial => initial_keys(&mut handler),\n CurrentUi::Home => home_keys(&mut handler),\n CurrentUi::AddTx => add_tx_keys(&mut handler),\n CurrentUi::Chart => chart_keys(&mut handler),\n CurrentUi::Summary => summary_keys(&mut handler),\n CurrentUi::Search => search_keys(&mut handler),\n CurrentUi::Activity => activity_keys(&mut handler),\n CurrentUi::Recurring => recurring_keys(&mut handler),\n };\n\n match status {\n Ok(output) => {\n if let Some(output) = output {\n return Ok(output);\n }\n }\n Err(e) => {\n let state = InfoPopupState::Error(e.to_string());\n popup_status = PopupType::new_info(state);\n }\n }\n }\n }\n}", + "body_hash": "e0bbb0d7fe3776c2b068fde6e8da4824c68fa05231bfabaf68cc0a69f785c374" + }, + "tui/src/page_handler/ui_handler.rs|crate::start_app#free:04d7c4ebf9a2": { + "kind": "Free", + "file": "tui/src/page_handler/ui_handler.rs", + "item_path": "crate::start_app#free:04d7c4ebf9a2", + "comment_kind": "Line", + "comment_text": "// The page which is currently selected. Default is the initial page", + "comment_hash": "04d7c4ebf9a287e0036fcf1f975590a9920b15d505d307f59fb480bd581da293", + "body_text": "let mut page = CurrentUi::Initial;", + "body_hash": "88d6a40ebe87131dfd66b53a2dd1b0defd16a8e2bc44d9348d21cd88531341cd" + }, + "tui/src/page_handler/ui_handler.rs|crate::start_app#free:0df66671d6ba": { + "kind": "Free", + "file": "tui/src/page_handler/ui_handler.rs", + "item_path": "crate::start_app#free:0df66671d6ba", + "comment_kind": "Line", + "comment_text": "// The generated summary", + "comment_hash": "0df66671d6bada5583b67ef2156bf5b34b365d175736d59652758a3481811e13", + "body_text": "let mut full_summary = summary_view.generate_summary(None, conn);", + "body_hash": "61a08d0cc304fe131cab761cedb19132e9d16c6b76fce8bd8acb4b5dadf14cb2" + }, + "tui/src/page_handler/ui_handler.rs|crate::start_app#free:1b36b39eff2b": { + "kind": "Free", + "file": "tui/src/page_handler/ui_handler.rs", + "item_path": "crate::start_app#free:1b36b39eff2b", + "comment_kind": "Line", + "comment_text": "// Whether the summary is in hidden mode", + "comment_hash": "1b36b39eff2b2743c21ecab21a8bc71d4a16afec1daeee5b26aa450c11573f49", + "body_text": "let mut summary_hidden_mode = false;", + "body_hash": "3d8026765125b3be59f28e520f1ce7bf5fe24fc1f67fff7eaa782e48c121bc91" + }, + "tui/src/page_handler/ui_handler.rs|crate::start_app#free:1bf26da09b0a": { + "kind": "Free", + "file": "tui/src/page_handler/ui_handler.rs", + "item_path": "crate::start_app#free:1bf26da09b0a", + "comment_kind": "Line", + "comment_text": "// Store the current selected widget on Chart page", + "comment_hash": "1bf26da09b0abc98c63d74302b1bca5c729a2e8642e25487717fd210d7baa211", + "body_text": "let mut chart_tab = ChartTab::ModeSelection;", + "body_hash": "97009f220ba3ec877ae0d4b823f8f41a4d4cf308dd2685895b3972b6c7676d10" + }, + "tui/src/page_handler/ui_handler.rs|crate::start_app#free:1c19b3f52f62": { + "kind": "Free", + "file": "tui/src/page_handler/ui_handler.rs", + "item_path": "crate::start_app#free:1c19b3f52f62", + "comment_kind": "Line", + "comment_text": "// Map of which TX methods are activated in the chart", + "comment_hash": "1c19b3f52f623c4ff2a073d718188ee0044eea3119cb0077d447e726c79f7242", + "body_text": "let mut chart_activated_methods = conn\n .get_tx_methods_cumulative()\n .into_iter()\n .map(|s| (s, true))\n .collect();", + "body_hash": "ba1c2b56563ce32ab2d003ca4523225cbfd932f487b6390a9e2b546cc9c9a6b3" + }, + "tui/src/page_handler/ui_handler.rs|crate::start_app#free:2220e3aa09bc": { + "kind": "Free", + "file": "tui/src/page_handler/ui_handler.rs", + "item_path": "crate::start_app#free:2220e3aa09bc", + "comment_kind": "Line", + "comment_text": "// Whether the chart is in hidden mode", + "comment_hash": "2220e3aa09bc852cc79ad0bdb2f54f778b11c84d7c618668c9050e5f99862016", + "body_text": "let mut chart_hidden_mode = false;", + "body_hash": "0200c439487336c9feee628dbfb1309284a6d119e327de45b2bcd4eefb1e7aa0" + }, + "tui/src/page_handler/ui_handler.rs|crate::start_app#free:27de609209e5": { + "kind": "Free", + "file": "tui/src/page_handler/ui_handler.rs", + "item_path": "crate::start_app#free:27de609209e5", + "comment_kind": "Line", + "comment_text": "// Store the current selected widget on Recurring page", + "comment_hash": "27de609209e5b4855cb30fb37aea6ff9165cf861607b47e97c2ecde21b3448d4", + "body_text": "let mut recurring_tab = TxTab::Nothing;", + "body_hash": "3206ddc4c41d2380d4c648fa67111567d2c9653e6c98af4464d390a4a150f46f" + }, + "tui/src/page_handler/ui_handler.rs|crate::start_app#free:28a7369ff461": { + "kind": "Free", + "file": "tui/src/page_handler/ui_handler.rs", + "item_path": "crate::start_app#free:28a7369ff461", + "comment_kind": "Line", + "comment_text": "// The initial page REX loading index", + "comment_hash": "28a7369ff461bb4b8ea88b601ea51f34c0175cba8a48de63fdefb570ba838ed4", + "body_text": "let mut starter_index = 0;", + "body_hash": "e8f6c47100dfc3eae16cb03493a00197fe15da53c9c424e04b690db23c5e6d55" + }, + "tui/src/page_handler/ui_handler.rs|crate::start_app#free:2f959c2ea0a7": { + "kind": "Free", + "file": "tui/src/page_handler/ui_handler.rs", + "item_path": "crate::start_app#free:2f959c2ea0a7", + "comment_kind": "Line", + "comment_text": "// Data for the Search Page's table", + "comment_hash": "2f959c2ea0a704a68b8a718c65e18abfaed3374e1b3faa88eafc0e0ecf53daf3", + "body_text": "let mut search_table = TableData::new(Vec::new());", + "body_hash": "246096e3a6be50c5b5f9fe7425776a3339efc4f2bb9973dadc56d056e29ff417" + }, + "tui/src/page_handler/ui_handler.rs|crate::start_app#free:316d2182c29a": { + "kind": "Free", + "file": "tui/src/page_handler/ui_handler.rs", + "item_path": "crate::start_app#free:316d2182c29a", + "comment_kind": "Line", + "comment_text": "// How it works:\n// Every value set up above -> rendered on the matching page -> wait for a key press.\n//\n// As long as a lerp is running the UI keeps re-rendering until all of them end\n//\n// On a key press the mutable values are handed to InputKeyHandler, which mutates\n// them -> the loop restarts -> the new values are sent to the interface -> repeat", + "comment_hash": "316d2182c29a09dc83a2a82a1b2c0044e2028581b8c6f4c3e9e4a4b46783eef6", + "body_text": "loop {}\n}", + "body_hash": "54a5925458671d950c69053fa6649d539c8e83893a89479ce09f0b44cfa7cb16" + }, + "tui/src/page_handler/ui_handler.rs|crate::start_app#free:360203c4cd21": { + "kind": "Free", + "file": "tui/src/page_handler/ui_handler.rs", + "item_path": "crate::start_app#free:360203c4cd21", + "comment_kind": "Line", + "comment_text": "// Contains the TX views for that month and year. Used for home page data + balances", + "comment_hash": "360203c4cd216d96672be0a7abc741cf3c6d6f0a79b27155c879d13f5a5832cf", + "body_text": "let mut home_txs = conn\n .fetch_txs_with_str(\n home_months.get_selected_value(),\n home_years.get_selected_value(),\n FetchNature::Monthly,\n )\n .unwrap();", + "body_hash": "f090ec79df90590d054817b7d0b5cdae1fd28ea25f11d51d00dc31d14b5abbef" + }, + "tui/src/page_handler/ui_handler.rs|crate::start_app#free:3dc9db4334c4": { + "kind": "Free", + "file": "tui/src/page_handler/ui_handler.rs", + "item_path": "crate::start_app#free:3dc9db4334c4", + "comment_kind": "Line", + "comment_text": "// Whether the chart has hidden legends", + "comment_hash": "3dc9db4334c4bc859fa06ea8c689e91f3e278e76bdebbef2915a22458a8d02e6", + "body_text": "let mut chart_hidden_legends = false;", + "body_hash": "5417c6eaaadebfd14ed9b8d2fb36921837c0c9108380d5d3d3f8dc3e5020cb33" + }, + "tui/src/page_handler/ui_handler.rs|crate::start_app#free:423dcd5cd6e8": { + "kind": "Free", + "file": "tui/src/page_handler/ui_handler.rs", + "item_path": "crate::start_app#free:423dcd5cd6e8", + "comment_kind": "Line", + "comment_text": "// If at least 1 lerp is in progress and no key press detected, continue the loop", + "comment_hash": "423dcd5cd6e8414a3df53185df450be31d545b95c33c2620a11f46a0e12e5abd", + "body_text": "if lerp_state.has_active_lerps()\n && !poll(Duration::from_millis(2)).map_err(UiHandlingError::Polling)?\n {}\n}", + "body_hash": "a644eebe747fa107e4818eedf80ee7a1e8726caff7ddf336e59525779740c806" + }, + "tui/src/page_handler/ui_handler.rs|crate::start_app#free:47ec405aff7b": { + "kind": "Free", + "file": "tui/src/page_handler/ui_handler.rs", + "item_path": "crate::start_app#free:47ec405aff7b", + "comment_kind": "Line", + "comment_text": "// The selected widget on the homepage. Default set to the month selection", + "comment_hash": "47ec405aff7bccbdde22cb0c29e18d0485d9616fe3b9c4414d7a1704f674f918", + "body_text": "let mut home_tab = HomeTab::Months;", + "body_hash": "8f1f7bc25896fbee29c78bd79021769f264a4324af5256cac7d05e96f2777850" + }, + "tui/src/page_handler/ui_handler.rs|crate::start_app#free:4b7e0484184e": { + "kind": "Free", + "file": "tui/src/page_handler/ui_handler.rs", + "item_path": "crate::start_app#free:4b7e0484184e", + "comment_kind": "Line", + "comment_text": "// Passing out relevant data to the UI function", + "comment_hash": "4b7e0484184e7ea6cd01fd884622da13cf8b2f35f45ebe881c7d946eda1f568c", + "body_text": "terminal\n .draw(|f| {})\n .map_err(|e| {})?;", + "body_hash": "adbf2f3a9a45b5b06c8904216f343b83075214e8543ddb671f70b07b3459a922" + }, + "tui/src/page_handler/ui_handler.rs|crate::start_app#free:5078af82f11b": { + "kind": "Free", + "file": "tui/src/page_handler/ui_handler.rs", + "item_path": "crate::start_app#free:5078af82f11b", + "comment_kind": "Line", + "comment_text": "// Based on the UI status, either start polling for key press or continue the loop", + "comment_hash": "5078af82f11be0a89549fa80d02696c933d98b9047cb5c5e79cb93463e066fbe", + "body_text": "match page {\n CurrentUi::Initial => {}\n CurrentUi::Home\n | CurrentUi::AddTx\n | CurrentUi::Summary\n | CurrentUi::Search\n | CurrentUi::Chart\n | CurrentUi::Activity\n | CurrentUi::Recurring => {}\n }", + "body_hash": "5dcc6eb225bdc7bf0f012520affafb61a05d24f87edd1f73846cece19fe69a1a" + }, + "tui/src/page_handler/ui_handler.rs|crate::start_app#free:65b0bb30ea35": { + "kind": "Free", + "file": "tui/src/page_handler/ui_handler.rs", + "item_path": "crate::start_app#free:65b0bb30ea35", + "comment_kind": "Line", + "comment_text": "// Home TX data but in vector form with index tracking. Used for creating the table in the UI", + "comment_hash": "65b0bb30ea35676a442102175a21180acc8782af4a325b4bdef5f8e527a2749a", + "body_text": "let mut home_table = TableData::new(home_txs.tx_array());", + "body_hash": "b273bd506bf4678e05ea72d2114e440fc5edd77e5f83857b977b622de689be28" + }, + "tui/src/page_handler/ui_handler.rs|crate::start_app#free:6d5365b1e248": { + "kind": "Free", + "file": "tui/src/page_handler/ui_handler.rs", + "item_path": "crate::start_app#free:6d5365b1e248", + "comment_kind": "Line", + "comment_text": "// Indexed month/year/mode/method lists backing each page's selectors. The\n// `_no_local` variants start at the first entry instead of the device's\n// current month or year", + "comment_hash": "6d5365b1e2480a1e7a425f5f866e19191ec11c5c22d670bd38d18ede9b8777f2", + "body_text": "let mut home_months = IndexedData::new_monthly();\nlet mut home_years = IndexedData::new_yearly();\nlet mut chart_months = IndexedData::new_monthly_no_local();\nlet mut chart_years = IndexedData::new_yearly_no_local();\nlet mut chart_modes = IndexedData::new_modes();\nlet mut chart_tx_methods = IndexedData::new_tx_methods_cumulative(conn);\nlet mut summary_months = IndexedData::new_monthly_no_local();\nlet mut summary_years = IndexedData::new_yearly_no_local();\nlet mut summary_modes = IndexedData::new_modes();\nlet mut activity_months = IndexedData::new_monthly();\nlet mut activity_years = IndexedData::new_yearly();", + "body_hash": "349431d6b6d13ca23de41912d77d706ed41f15f69c6bc5033c777d420cf45154" + }, + "tui/src/page_handler/ui_handler.rs|crate::start_app#free:7a79d4659d75": { + "kind": "Free", + "file": "tui/src/page_handler/ui_handler.rs", + "item_path": "crate::start_app#free:7a79d4659d75", + "comment_kind": "Line", + "comment_text": "// Store the current selected widget on Activity page", + "comment_hash": "7a79d4659d75795970ead4f30e42a3637cbfb30903a2ad015c170b0895aefa00", + "body_text": "let mut activity_tab = ActivityTab::Years;", + "body_hash": "3a14fcab0a9fe88c5e40d33d5f1d0ccdba301cc179ac129ce4e65b4583c18ada" + }, + "tui/src/page_handler/ui_handler.rs|crate::start_app#free:7ca288fc9d78": { + "kind": "Free", + "file": "tui/src/page_handler/ui_handler.rs", + "item_path": "crate::start_app#free:7ca288fc9d78", + "comment_kind": "Line", + "comment_text": "// How summary table will be sorted", + "comment_hash": "7ca288fc9d78d99d31cc9ee55c6a7c04b2661ae1f22c7201c2f65b5f6d60c35a", + "body_text": "let mut summary_sort = SortingType::Tags;", + "body_hash": "8f5c52de7849b80190ec8b31d4a23daa388346a34bb6655ab9c0769134bbf499" + }, + "tui/src/page_handler/ui_handler.rs|crate::start_app#free:7cf9944cb416": { + "kind": "Free", + "file": "tui/src/page_handler/ui_handler.rs", + "item_path": "crate::start_app#free:7cf9944cb416", + "comment_kind": "Line", + "comment_text": "// Holds the data that will be/are inserted into the Recurring page's input fields", + "comment_hash": "7cf9944cb416524163869dd1cb6b16ef8a54e3f6a44bd57d609ba44ea5a26927", + "body_text": "let mut recurring_data = TxData::new_empty();", + "body_hash": "800450793c53067707903941afbc2923a232c195f57117cd050470aeae842407" + }, + "tui/src/page_handler/ui_handler.rs|crate::start_app#free:82766ec0133f": { + "kind": "Free", + "file": "tui/src/page_handler/ui_handler.rs", + "item_path": "crate::start_app#free:82766ec0133f", + "comment_kind": "Line", + "comment_text": "// Wait for key press", + "comment_hash": "82766ec0133f8cc6922cf2fc968acd8173f8824fce2c9c1ee134704491bbe5e1", + "body_text": "if let Event::Key(key) = event::read().map_err(UiHandlingError::Polling)? {}\n}", + "body_hash": "1cdebb05c8b1629da1276ddbdd436c9751b2d3ae28903f8ef1e12b9e9ac3b297" + }, + "tui/src/page_handler/ui_handler.rs|crate::start_app#free:8a15fe64e2e8": { + "kind": "Free", + "file": "tui/src/page_handler/ui_handler.rs", + "item_path": "crate::start_app#free:8a15fe64e2e8", + "comment_kind": "Line", + "comment_text": "// Holds the data that will be/are inserted into the Search page's input fields", + "comment_hash": "8a15fe64e2e8144941f5f30e117f3e294724b639a8b4145fa0b907c6430937ce", + "body_text": "let mut search_data = TxData::new_empty();", + "body_hash": "12c965a6a85f659d2c37a0e7a39402d3c2d542e20a2ed63e61db3f1e658a0efd" + }, + "tui/src/page_handler/ui_handler.rs|crate::start_app#free:9442f1f1e6db": { + "kind": "Free", + "file": "tui/src/page_handler/ui_handler.rs", + "item_path": "crate::start_app#free:9442f1f1e6db", + "comment_kind": "Line", + "comment_text": "// The currently known recurring transaction rules", + "comment_hash": "9442f1f1e6db27e7cdff7bb66141561ebdc2057e8a4db11fc4cf22d58d4c2027", + "body_text": "let mut recurring_txs = conn.get_recurring_txs().unwrap();", + "body_hash": "0f2d69b194277fb34b7283309af5d9e74c316e1e44d91ab6a5f5e57d5528b77d" + }, + "tui/src/page_handler/ui_handler.rs|crate::start_app#free:a54a85e7b204": { + "kind": "Free", + "file": "tui/src/page_handler/ui_handler.rs", + "item_path": "crate::start_app#free:a54a85e7b204", + "comment_kind": "Line", + "comment_text": "// TXs of the selected month and year, used to generate the activity table", + "comment_hash": "a54a85e7b20456353faa7a1c24cfd7ede64069f57313f124206054eb13b343f2", + "body_text": "let mut activity_view = conn\n .get_activity_view_with_str(\n activity_months.get_selected_value(),\n activity_years.get_selected_value(),\n )\n .unwrap();", + "body_hash": "9b06f271373c3fc88d7716b1ea80608d0029ad9c38d3eab8fac6164e769f646e" + }, + "tui/src/page_handler/ui_handler.rs|crate::start_app#free:a5d55aa65129": { + "kind": "Free", + "file": "tui/src/page_handler/ui_handler.rs", + "item_path": "crate::start_app#free:a5d55aa65129", + "comment_kind": "Line", + "comment_text": "// TXs of the selected period, used to generate the summary", + "comment_hash": "a5d55aa65129f47c819be8e655f65087211859d285a674c42ce13d914d261ac8", + "body_text": "let mut summary_view = conn\n .get_summary_with_str(\n summary_months.get_selected_value(),\n summary_years.get_selected_value(),\n FetchNature::Monthly,\n )\n .unwrap();", + "body_hash": "523ebf1ae0303efa49ced6dacfabc2b5d907793327ca8bd540b45cd65d4b9cc5" + }, + "tui/src/page_handler/ui_handler.rs|crate::start_app#free:b0cbb6cacaf3": { + "kind": "Free", + "file": "tui/src/page_handler/ui_handler.rs", + "item_path": "crate::start_app#free:b0cbb6cacaf3", + "comment_kind": "Line", + "comment_text": "// Set up the initial state of every page and widget", + "comment_hash": "b0cbb6cacaf3d0319b2ad5e23510557bb1b29d4bcfd31023df7bb242362a4047", + "body_text": "let mut popup_status = PopupType::Nothing;\nlet recurring_added = match conn.process_due_recurring_txs() {\n Ok(count) => count,\n Err(e) => {}\n };\nlet mut theme = Theme::new_index(config.theme_index.unwrap_or(0));", + "body_hash": "ede00315300191eeada8c9c9320cf1a698ce38888182c8b3095f46e3ade87807" + }, + "tui/src/page_handler/ui_handler.rs|crate::start_app#free:b96f621367e5": { + "kind": "Free", + "file": "tui/src/page_handler/ui_handler.rs", + "item_path": "crate::start_app#free:b96f621367e5", + "comment_kind": "Line", + "comment_text": "// Data for the Summary Page's table", + "comment_hash": "b96f621367e5de6f5a364ec7b2cc41317c2ffcd8216f81d95242b9f6d45815b1", + "body_text": "let mut summary_table = TableData::new(summary_view.tags_array(None, conn));", + "body_hash": "6524edc7c51e99d70a45f7a1f34fe4ac0283b1aa663cd984e55849c01af4c103" + }, + "tui/src/page_handler/ui_handler.rs|crate::start_app#free:bd1fecc8529f": { + "kind": "Free", + "file": "tui/src/page_handler/ui_handler.rs", + "item_path": "crate::start_app#free:bd1fecc8529f", + "comment_kind": "Line", + "comment_text": "// Holds the data that will be/are inserted into the Add TX page's input fields", + "comment_hash": "bd1fecc8529f805602b5986c3fb86546feab4c5572159654482f59fa4259a2bb", + "body_text": "let mut add_tx_data = TxData::new();", + "body_hash": "dca0ccf46c9099035f8b8d7124b50cab49b628d2be33c6de4f45c95a8958e0b7" + }, + "tui/src/page_handler/ui_handler.rs|crate::start_app#free:bde061440291": { + "kind": "Free", + "file": "tui/src/page_handler/ui_handler.rs", + "item_path": "crate::start_app#free:bde061440291", + "comment_kind": "Line", + "comment_text": "// An empty search view, contains relevant data to create the search UI", + "comment_hash": "bde0614402916afe817d1b0d785b5f761919afa912a9c402d55db1bee5d4cc26", + "body_text": "let mut search_txs = SearchView::new_empty();", + "body_hash": "7b9fc871425d91c6a83f4c9281c69c743b61ba798bc24eed79e0d56710f7fbaf" + }, + "tui/src/page_handler/ui_handler.rs|crate::start_app#free:c24799b7fa6e": { + "kind": "Free", + "file": "tui/src/page_handler/ui_handler.rs", + "item_path": "crate::start_app#free:c24799b7fa6e", + "comment_kind": "Line", + "comment_text": "// Store the current selected widget on Summary page", + "comment_hash": "c24799b7fa6e231dc7d104d664ccf237efa2fd9bf69900e479db17e863be45c6", + "body_text": "let mut summary_tab = SummaryTab::ModeSelection;", + "body_hash": "986c4becad71bd0b91a28052da8abb60f60002c5ea9b39c66b63189a3ca2f550" + }, + "tui/src/page_handler/ui_handler.rs|crate::start_app#free:c9ceda20ab2d": { + "kind": "Free", + "file": "tui/src/page_handler/ui_handler.rs", + "item_path": "crate::start_app#free:c9ceda20ab2d", + "comment_kind": "Line", + "comment_text": "// Store the current searching date type", + "comment_hash": "c9ceda20ab2d87bc2daa5aa93d91e964e84c62d91c3c6094e7e3dfc6e40dad6f", + "body_text": "let mut search_date_type = DateType::Exact;", + "body_hash": "ba4f099aad614ad2c06b4d70a53643becfaed5eb6567090f8d57f680d98a7fb3" + }, + "tui/src/page_handler/ui_handler.rs|crate::start_app#free:d2a447788a4a": { + "kind": "Free", + "file": "tui/src/page_handler/ui_handler.rs", + "item_path": "crate::start_app#free:d2a447788a4a", + "comment_kind": "Line", + "comment_text": "// Data for the Activity Page's table", + "comment_hash": "d2a447788a4ac704c3f006b2941d53e682d55fa3843bc11ee5776bc66f043847", + "body_text": "let mut activity_table = TableData::new(activity_view.get_activity_table());", + "body_hash": "fff7bda638aaeb433f4bcb20bcdd7d395930a68191d985f4568e8383d563d4ed" + }, + "tui/src/page_handler/ui_handler.rs|crate::start_app#free:d6420f6feec7": { + "kind": "Free", + "file": "tui/src/page_handler/ui_handler.rs", + "item_path": "crate::start_app#free:d6420f6feec7", + "comment_kind": "Line", + "comment_text": "// Data for the Recurring Page's table", + "comment_hash": "d6420f6feec7831d3555e5ce88202fa6839c73a6665aab554c503a630fe9df9b", + "body_text": "let mut recurring_table = TableData::new(\n recurring_txs\n .iter()\n .map(FullRecurringTx::to_array)\n .collect(),\n );", + "body_hash": "872b243a6dbb50a4681eac11c87999dad287097e95ec3e12f161bd4ed1e7de54" + }, + "tui/src/page_handler/ui_handler.rs|crate::start_app#free:d72d2451448d": { + "kind": "Free", + "file": "tui/src/page_handler/ui_handler.rs", + "item_path": "crate::start_app#free:d72d2451448d", + "comment_kind": "Line", + "comment_text": "// Stores the current selected widget on Add Transaction page", + "comment_hash": "d72d2451448d226a62b492fd2f70a9b73e06492452b2c534d5e5669429ec09e1", + "body_text": "let mut add_tx_tab = TxTab::Nothing;", + "body_hash": "a019cbcd24507c5cf5040317cb4530529ba195d79885857cdd15daec0329c8ee" + }, + "tui/src/page_handler/ui_handler.rs|crate::start_app#free:d834ed61595f": { + "kind": "Free", + "file": "tui/src/page_handler/ui_handler.rs", + "item_path": "crate::start_app#free:d834ed61595f", + "comment_kind": "Line", + "comment_text": "// Startup screen data, loaded once since that page redraws every few milliseconds", + "comment_hash": "d834ed61595f4a59807429c749b49fd25890fd92a20df6e1b666b6db956678f8", + "body_text": "let initial_data = InitialData::load(conn, &recurring_txs, recurring_added);", + "body_hash": "b421a2de0d013e24bd83d528ae68d00c3ca1974b277243614a73d7feaa79109b" + }, + "tui/src/page_handler/ui_handler.rs|crate::start_app#free:e2aabb47ddff": { + "kind": "Free", + "file": "tui/src/page_handler/ui_handler.rs", + "item_path": "crate::start_app#free:e2aabb47ddff", + "comment_kind": "Line", + "comment_text": "// Store the current selected widget on Search page", + "comment_hash": "e2aabb47ddffc35c8216314649967cc185c9c19cab270d113aaaa63e286720c5", + "body_text": "let mut search_tab = TxTab::Nothing;", + "body_hash": "0ba01c61274b2530c438e1b4549e1039c59e942fd198e26e4a4721308e133920" + }, + "tui/src/page_handler/ui_handler.rs|crate::start_app#free:e7903176c3f2": { + "kind": "Free", + "file": "tui/src/page_handler/ui_handler.rs", + "item_path": "crate::start_app#free:e7903176c3f2", + "comment_kind": "Line", + "comment_text": "// With no TX method at all, keep asking the user to create one", + "comment_hash": "e7903176c3f2d6ecdbe040b7f1d1f1ea54336ac95c35118bf9ff98865ba948bb", + "body_text": "if conn.is_tx_method_empty()\n && let PopupType::Nothing = popup_status\n {}", + "body_hash": "f620cd15d4771925b3605a040093a3a0fbe56cfecbf0bba2f2420d7db176f190" + }, + "tui/src/page_handler/ui_handler.rs|crate::start_app#free:ebaef3caed6c": { + "kind": "Free", + "file": "tui/src/page_handler/ui_handler.rs", + "item_path": "crate::start_app#free:ebaef3caed6c", + "comment_kind": "Line", + "comment_text": "// The generated balance section on the Add TX UI", + "comment_hash": "ebaef3caed6c112aa5fbe22188ec5b0391c5e2604beaa35bdb6c26f0eacf218c", + "body_text": "let mut add_tx_balance = Vec::new();\nlet mut lerp_state = LerpState::new(1.0);\nlet mut version_checked = false;", + "body_hash": "5ab3fe693d725f1a37dd2f776b83df2a6a3eb5ba677a17451dd9d95bed73f7d2" + }, + "tui/src/page_handler/ui_handler.rs|crate::start_app#free:ed11d40ef8a5": { + "kind": "Free", + "file": "tui/src/page_handler/ui_handler.rs", + "item_path": "crate::start_app#free:ed11d40ef8a5", + "comment_kind": "Line", + "comment_text": "// Initial page will loop to animate the text", + "comment_hash": "ed11d40ef8a562cb1ef185133a61b77ed59deaa021a61fa45dac80c8960ae82b", + "body_text": "if !poll(Duration::from_millis(40)).map_err(UiHandlingError::Polling)? {}\n}", + "body_hash": "56d7b8e6cb424ebf01a4d90b9ed71e0450136a1c52355a6ff9967535de3cd4a4" + }, + "tui/src/page_handler/ui_handler.rs|crate::start_app#free:f9c7a7a3f770": { + "kind": "Free", + "file": "tui/src/page_handler/ui_handler.rs", + "item_path": "crate::start_app#free:f9c7a7a3f770", + "comment_kind": "Line", + "comment_text": "// TXs of the selected period, used to draw the chart", + "comment_hash": "f9c7a7a3f770b265506d21b1556770d9dd15a7815e62ac31ecb900a2fe8ddbdb", + "body_text": "let mut chart_view = conn\n .get_chart_view_with_str(\n chart_months.get_selected_value(),\n chart_years.get_selected_value(),\n FetchNature::Monthly,\n )\n .unwrap();", + "body_hash": "a8e1c7fc00a8c669d396590b6faabab515401a1058040ffbe503cfb6aa3fec95" + }, + "tui/src/page_handler/ui_state.rs|crate::CurrentUi": { + "kind": "Enum", + "file": "tui/src/page_handler/ui_state.rs", + "item_path": "crate::CurrentUi", + "comment_kind": "OuterDoc", + "comment_text": " Which page is currently shown, used to send keypresses to the right handler\n", + "comment_hash": "3312bc0bfcbc022e00944d4d08b9ccaf191753c04ffef363318fed6c71808910", + "body_text": "pub enum CurrentUi {\n Initial,\n Home,\n AddTx,\n Chart,\n Summary,\n Search,\n Activity,\n Recurring,\n}", + "body_hash": "6b5473e520865011bb31850f49e1f8f2846c6a78d29af5ef56d8e5bcab865cdb" + }, + "tui/src/page_handler/ui_state.rs|crate::HomeTab": { + "kind": "Enum", + "file": "tui/src/page_handler/ui_state.rs", + "item_path": "crate::HomeTab", + "comment_kind": "OuterDoc", + "comment_text": " Which of the homepage's 3 selectable widgets is currently focused. The focus\n\n cycles through all of them, so it never gets stuck at an end.\n", + "comment_hash": "52382cff1bab661f236ef61bb3125456ad1521abddc53a56f3c0f64eea66adaf", + "body_text": "pub enum HomeTab {\n Years,\n Months,\n Table,\n}", + "body_hash": "b469e1d5feb4a75933547b748f0504037b46784e31630476b7074d9bb8383690" + }, + "tui/src/page_handler/ui_state.rs|crate::IndexedData": { + "kind": "Struct", + "file": "tui/src/page_handler/ui_state.rs", + "item_path": "crate::IndexedData", + "comment_kind": "OuterDoc", + "comment_text": " Wraps a list of values and keeps track of which one is currently selected.\n\n Used for the month and year lists of the selectors.\n\n\n\n titles: `[\"January\", \"February\",]`\n", + "comment_hash": "09f6214f46befcbe2a3de8b2990862bb58a9b0795ad6252fa74a22f7e1ccab5a", + "body_text": "#[derive(Clone)]\npub struct IndexedData {\n pub titles: Vec,\n pub index: usize,\n}", + "body_hash": "082b4f869e5254ad37304ede7168855212b236d05312d44014c9ae945dd0d62c" + }, + "tui/src/page_handler/ui_state.rs|crate::TableData": { + "kind": "Struct", + "file": "tui/src/page_handler/ui_state.rs", + "item_path": "crate::TableData", + "comment_kind": "OuterDoc", + "comment_text": " The rows of a transaction table, each row holding the already formatted\n\n columns of one transaction, plus the state ratatui needs to know which row\n\n is selected and where the viewport starts.\n\n\n\n state: `None` when nothing is selected, otherwise the selected row index\n\n items: `[[\"2022-05-01\", \"test\", \"source_1\", \"15.50\", \"Expense\"], ]`\n", + "comment_hash": "0609e2606a9944782880be6221663bae02fc029aafcfcc5daa1f5cda94052a2a", + "body_text": "pub struct TableData {\n pub state: TableState,\n pub items: Vec>,\n}", + "body_hash": "81434115b7ed4e34745eb57c53e1f610df0bf4528fc86e404ff434c9bdde55c3" + }, + "tui/src/page_handler/ui_state.rs|crate::TxTab": { + "kind": "Enum", + "file": "tui/src/page_handler/ui_state.rs", + "item_path": "crate::TxTab", + "comment_kind": "OuterDoc", + "comment_text": " Which field of a transaction form is currently being edited. The Add\n\n Transaction, Search and Recurring pages each keep their own value of this.\n\n The last five values only ever appear on the Recurring page.\n", + "comment_hash": "ded8f54cbc96683453e03374b267f235bf671c0dfe83906f30542f0b31e28944", + "body_text": "#[derive(Debug, PartialEq)]\npub enum TxTab {\n Date,\n Details,\n FromMethod,\n ToMethod,\n Amount,\n TxType,\n Tags,\n Frequency,\n RecurInterval,\n RecurValue,\n RecurMonth,\n EndDate,\n Nothing,\n}", + "body_hash": "f7be94b3919c81bd7284ba66cb7720839a785589faa703a09bd5b8115130c54d" + }, + "tui/src/page_handler/ui_state.rs|crate::impl ChartTab::change_tab_down_all_time": { + "kind": "Function", + "file": "tui/src/page_handler/ui_state.rs", + "item_path": "crate::impl ChartTab::change_tab_down_all_time", + "comment_kind": "OuterDoc", + "comment_text": " Moves the current selected tab to the bottom value. If at the last value, the 1st value is selected.\n", + "comment_hash": "f4c2069470a299f99736a3e0b4400e3ba3c5b7f2d8ace8dbffea2c5f19651165", + "body_text": "pub fn change_tab_down_all_time(&mut self) -> Self {\n match &self {\n ChartTab::ModeSelection => ChartTab::TxMethods,\n ChartTab::TxMethods => ChartTab::ModeSelection,\n ChartTab::Months | ChartTab::Years => unreachable!(),\n }\n }", + "body_hash": "e5bee294a6037f568e914512de86f962d1a9616f70f30d7443f451652c04aef9" + }, + "tui/src/page_handler/ui_state.rs|crate::impl ChartTab::change_tab_down_monthly": { + "kind": "Function", + "file": "tui/src/page_handler/ui_state.rs", + "item_path": "crate::impl ChartTab::change_tab_down_monthly", + "comment_kind": "OuterDoc", + "comment_text": " Moves the current selected tab to the bottom value. If at the last value, the 1st value is selected.\n", + "comment_hash": "f4c2069470a299f99736a3e0b4400e3ba3c5b7f2d8ace8dbffea2c5f19651165", + "body_text": "pub fn change_tab_down_monthly(&mut self) -> Self {\n match &self {\n ChartTab::ModeSelection => ChartTab::Years,\n ChartTab::Years => ChartTab::Months,\n ChartTab::Months => ChartTab::TxMethods,\n ChartTab::TxMethods => ChartTab::ModeSelection,\n }\n }", + "body_hash": "1fd0215b4ae7e8f53934d52f4c232087891cb526ab00948ece7bb7256d193f00" + }, + "tui/src/page_handler/ui_state.rs|crate::impl ChartTab::change_tab_down_yearly": { + "kind": "Function", + "file": "tui/src/page_handler/ui_state.rs", + "item_path": "crate::impl ChartTab::change_tab_down_yearly", + "comment_kind": "OuterDoc", + "comment_text": " Moves the current selected tab to the bottom value. If at the last value, the 1st value is selected.\n", + "comment_hash": "f4c2069470a299f99736a3e0b4400e3ba3c5b7f2d8ace8dbffea2c5f19651165", + "body_text": "pub fn change_tab_down_yearly(&mut self) -> Self {\n match &self {\n ChartTab::ModeSelection => ChartTab::Years,\n ChartTab::Years => ChartTab::TxMethods,\n ChartTab::TxMethods => ChartTab::ModeSelection,\n ChartTab::Months => unreachable!(),\n }\n }", + "body_hash": "dd3ee7843b4fd42d8a2bdfe04638efc22a4d66bd6b2145a51a0b70ae44c5e6ec" + }, + "tui/src/page_handler/ui_state.rs|crate::impl ChartTab::change_tab_up_all_time": { + "kind": "Function", + "file": "tui/src/page_handler/ui_state.rs", + "item_path": "crate::impl ChartTab::change_tab_up_all_time", + "comment_kind": "OuterDoc", + "comment_text": " Moves the current selected tab to the upper value. If at the 1st value, the final value is selected.\n", + "comment_hash": "cb19be82a39943898c26f78c84fea3e784842f6a99f7b10d8a43c30c201b998e", + "body_text": "pub fn change_tab_up_all_time(&mut self) -> Self {\n match &self {\n ChartTab::ModeSelection => ChartTab::TxMethods,\n ChartTab::TxMethods => ChartTab::ModeSelection,\n ChartTab::Months | ChartTab::Years => unreachable!(),\n }\n }", + "body_hash": "b0c88967aad1ea657ed010bcae9b6ceddc3fbf52a0a080e32b46e93e3116ec68" + }, + "tui/src/page_handler/ui_state.rs|crate::impl ChartTab::change_tab_up_monthly": { + "kind": "Function", + "file": "tui/src/page_handler/ui_state.rs", + "item_path": "crate::impl ChartTab::change_tab_up_monthly", + "comment_kind": "OuterDoc", + "comment_text": " Moves the current selected tab to the upper value. If at the 1st value, the final value is selected.\n", + "comment_hash": "cb19be82a39943898c26f78c84fea3e784842f6a99f7b10d8a43c30c201b998e", + "body_text": "pub fn change_tab_up_monthly(&mut self) -> Self {\n match &self {\n ChartTab::ModeSelection => ChartTab::TxMethods,\n ChartTab::Years => ChartTab::ModeSelection,\n ChartTab::Months => ChartTab::Years,\n ChartTab::TxMethods => ChartTab::Months,\n }\n }", + "body_hash": "463c9dc02b6f6bd9a67d46b7cd7d96edd5ec27d5e737ee496e28fa682cdf3889" + }, + "tui/src/page_handler/ui_state.rs|crate::impl ChartTab::change_tab_up_yearly": { + "kind": "Function", + "file": "tui/src/page_handler/ui_state.rs", + "item_path": "crate::impl ChartTab::change_tab_up_yearly", + "comment_kind": "OuterDoc", + "comment_text": " Moves the current selected tab to the upper value. If at the 1st value, the final value is selected.\n", + "comment_hash": "cb19be82a39943898c26f78c84fea3e784842f6a99f7b10d8a43c30c201b998e", + "body_text": "pub fn change_tab_up_yearly(&mut self) -> Self {\n match &self {\n ChartTab::ModeSelection => ChartTab::TxMethods,\n ChartTab::Years => ChartTab::ModeSelection,\n ChartTab::TxMethods => ChartTab::Years,\n ChartTab::Months => unreachable!(),\n }\n }", + "body_hash": "15a7982c0be00eefbbcecf35ac8986fc3db86c5d1e49ab25f0f15a46b7f85817" + }, + "tui/src/page_handler/ui_state.rs|crate::impl HomeTab::change_tab_down": { + "kind": "Function", + "file": "tui/src/page_handler/ui_state.rs", + "item_path": "crate::impl HomeTab::change_tab_down", + "comment_kind": "OuterDoc", + "comment_text": " Moves the current selected tab to the bottom value. If at the last value, the 1st value is selected.\n", + "comment_hash": "f4c2069470a299f99736a3e0b4400e3ba3c5b7f2d8ace8dbffea2c5f19651165", + "body_text": "pub fn change_tab_down(&mut self) -> Self {\n match &self {\n HomeTab::Years => HomeTab::Months,\n HomeTab::Months => HomeTab::Table,\n HomeTab::Table => HomeTab::Years,\n }\n }", + "body_hash": "a4e0d522f5caef4533c91feca8055502865a4eb56fbe8119c8b129622c537a23" + }, + "tui/src/page_handler/ui_state.rs|crate::impl HomeTab::change_tab_up": { + "kind": "Function", + "file": "tui/src/page_handler/ui_state.rs", + "item_path": "crate::impl HomeTab::change_tab_up", + "comment_kind": "OuterDoc", + "comment_text": " Moves the current selected tab to the upper value. If at the 1st value, the final value is selected.\n", + "comment_hash": "cb19be82a39943898c26f78c84fea3e784842f6a99f7b10d8a43c30c201b998e", + "body_text": "pub fn change_tab_up(&mut self) -> Self {\n match &self {\n HomeTab::Years => HomeTab::Table,\n HomeTab::Months => HomeTab::Years,\n HomeTab::Table => HomeTab::Months,\n }\n }", + "body_hash": "1e84a5a85e77776f19bea606607fb042e78550eb4b2d2f60575882e664153037" + }, + "tui/src/page_handler/ui_state.rs|crate::impl IndexedData::next": { + "kind": "Function", + "file": "tui/src/page_handler/ui_state.rs", + "item_path": "crate::impl IndexedData::next", + "comment_kind": "OuterDoc", + "comment_text": " Increases the current index by 1 or goes to 0 if at the final value\n", + "comment_hash": "da87c2af96089bf319e5e6541a2086ccd2137c6c44310444b4a26090bdea22cc", + "body_text": "pub fn next(&mut self) {\n self.index = (self.index + 1) % self.titles.len();\n }", + "body_hash": "c3e735d0971192e4ecfa9720ede956fd64d0c5a0ab2708b94e9410759c4c7ed2" + }, + "tui/src/page_handler/ui_state.rs|crate::impl IndexedData::previous": { + "kind": "Function", + "file": "tui/src/page_handler/ui_state.rs", + "item_path": "crate::impl IndexedData::previous", + "comment_kind": "OuterDoc", + "comment_text": " Decreases the current index by 1 or goes to final index if at 0\n", + "comment_hash": "721e1c56d58053415c66cf67053d2245969d8473e997ed4be4cd59c16b27d89f", + "body_text": "pub fn previous(&mut self) {\n if self.index > 0 {\n self.index -= 1;\n } else {\n self.index = self.titles.len() - 1;\n }\n }", + "body_hash": "538d283dab523a529d10b9e79b37f7d801ea6504cb5ab063fca82d8bab9cfd3b" + }, + "tui/src/page_handler/ui_state.rs|crate::impl SummaryTab::change_tab_down_all_time": { + "kind": "Function", + "file": "tui/src/page_handler/ui_state.rs", + "item_path": "crate::impl SummaryTab::change_tab_down_all_time", + "comment_kind": "OuterDoc", + "comment_text": " Moves the current selected tab to the bottom value. If at the last value, the 1st value is selected.\n", + "comment_hash": "f4c2069470a299f99736a3e0b4400e3ba3c5b7f2d8ace8dbffea2c5f19651165", + "body_text": "pub fn change_tab_down_all_time(&mut self) -> Self {\n match &self {\n SummaryTab::ModeSelection => SummaryTab::Table,\n SummaryTab::Table => SummaryTab::ModeSelection,\n SummaryTab::Years => SummaryTab::Years,\n SummaryTab::Months => SummaryTab::Months,\n }\n }", + "body_hash": "c2a02b6edbb6126f07a57c046e5d8fc979c7bf6d7102fdf74d98a6f8434b9b07" + }, + "tui/src/page_handler/ui_state.rs|crate::impl SummaryTab::change_tab_down_monthly": { + "kind": "Function", + "file": "tui/src/page_handler/ui_state.rs", + "item_path": "crate::impl SummaryTab::change_tab_down_monthly", + "comment_kind": "OuterDoc", + "comment_text": " Moves the current selected tab to the bottom value. If at the last value, the 1st value is selected.\n", + "comment_hash": "f4c2069470a299f99736a3e0b4400e3ba3c5b7f2d8ace8dbffea2c5f19651165", + "body_text": "pub fn change_tab_down_monthly(&mut self) -> Self {\n match &self {\n SummaryTab::ModeSelection => SummaryTab::Years,\n SummaryTab::Years => SummaryTab::Months,\n SummaryTab::Months => SummaryTab::Table,\n SummaryTab::Table => SummaryTab::ModeSelection,\n }\n }", + "body_hash": "1bf86de37077c23a30feb3fe4c4260124c9ee8c85b9b0a6556a42e61d1ccb5d6" + }, + "tui/src/page_handler/ui_state.rs|crate::impl SummaryTab::change_tab_down_yearly": { + "kind": "Function", + "file": "tui/src/page_handler/ui_state.rs", + "item_path": "crate::impl SummaryTab::change_tab_down_yearly", + "comment_kind": "OuterDoc", + "comment_text": " Moves the current selected tab to the bottom value. If at the last value, the 1st value is selected.\n", + "comment_hash": "f4c2069470a299f99736a3e0b4400e3ba3c5b7f2d8ace8dbffea2c5f19651165", + "body_text": "pub fn change_tab_down_yearly(&mut self) -> Self {\n match &self {\n SummaryTab::ModeSelection => SummaryTab::Years,\n SummaryTab::Years => SummaryTab::Table,\n SummaryTab::Table => SummaryTab::ModeSelection,\n SummaryTab::Months => SummaryTab::Months,\n }\n }", + "body_hash": "9ac42d275618364526ed988b3b59e3a10ebe8daf45b68c6ed3c562593381e768" + }, + "tui/src/page_handler/ui_state.rs|crate::impl SummaryTab::change_tab_up_all_time": { + "kind": "Function", + "file": "tui/src/page_handler/ui_state.rs", + "item_path": "crate::impl SummaryTab::change_tab_up_all_time", + "comment_kind": "OuterDoc", + "comment_text": " Moves the current selected tab to the upper value. If at the 1st value, the final value is selected.\n", + "comment_hash": "cb19be82a39943898c26f78c84fea3e784842f6a99f7b10d8a43c30c201b998e", + "body_text": "pub fn change_tab_up_all_time(&mut self) -> Self {\n match &self {\n SummaryTab::ModeSelection => SummaryTab::Table,\n SummaryTab::Table => SummaryTab::ModeSelection,\n SummaryTab::Years => SummaryTab::Years,\n SummaryTab::Months => SummaryTab::Months,\n }\n }", + "body_hash": "bdc740d4798e6d09063d260735c9e612d41d8fa6dd84b76e40963d0c42382871" + }, + "tui/src/page_handler/ui_state.rs|crate::impl SummaryTab::change_tab_up_monthly": { + "kind": "Function", + "file": "tui/src/page_handler/ui_state.rs", + "item_path": "crate::impl SummaryTab::change_tab_up_monthly", + "comment_kind": "OuterDoc", + "comment_text": " Moves the current selected tab to the upper value. If at the 1st value, the final value is selected.\n", + "comment_hash": "cb19be82a39943898c26f78c84fea3e784842f6a99f7b10d8a43c30c201b998e", + "body_text": "pub fn change_tab_up_monthly(&mut self) -> Self {\n match &self {\n SummaryTab::ModeSelection => SummaryTab::Table,\n SummaryTab::Years => SummaryTab::ModeSelection,\n SummaryTab::Months => SummaryTab::Years,\n SummaryTab::Table => SummaryTab::Months,\n }\n }", + "body_hash": "1d3f57b206ffe4b86320fa58352e57f293b1a272ba2d1f237c22c72bebbe06d1" + }, + "tui/src/page_handler/ui_state.rs|crate::impl SummaryTab::change_tab_up_yearly": { + "kind": "Function", + "file": "tui/src/page_handler/ui_state.rs", + "item_path": "crate::impl SummaryTab::change_tab_up_yearly", + "comment_kind": "OuterDoc", + "comment_text": " Moves the current selected tab to the upper value. If at the 1st value, the final value is selected.\n", + "comment_hash": "cb19be82a39943898c26f78c84fea3e784842f6a99f7b10d8a43c30c201b998e", + "body_text": "pub fn change_tab_up_yearly(&mut self) -> Self {\n match &self {\n SummaryTab::ModeSelection => SummaryTab::Table,\n SummaryTab::Years => SummaryTab::ModeSelection,\n SummaryTab::Table => SummaryTab::Years,\n SummaryTab::Months => SummaryTab::Months,\n }\n }", + "body_hash": "287563b40b2ac696b0fb457621ca69d1a6339571a4890e470232202eee65f277" + }, + "tui/src/page_handler/ui_state.rs|crate::impl TableData::new": { + "kind": "Function", + "file": "tui/src/page_handler/ui_state.rs", + "item_path": "crate::impl TableData::new", + "comment_kind": "OuterDoc", + "comment_text": " Wraps the given rows in a table whose selection state is ratatui's\n\n default, meaning nothing is selected yet\n", + "comment_hash": "0e07002c0b0ce41265edb795455e61f704a1dc5cfae784761100c45ac0317889", + "body_text": "#[must_use]\npub fn new(data: Vec>) -> Self {\n TableData {\n state: TableState::default(),\n items: data,\n }\n }", + "body_hash": "c2a60c3e139d18d2f5e1576c8e13fa394665771861f7725ac39f85bf74cccf8f" + }, + "tui/src/page_handler/ui_state.rs|crate::impl TableData::next": { + "kind": "Function", + "file": "tui/src/page_handler/ui_state.rs", + "item_path": "crate::impl TableData::next", + "comment_kind": "OuterDoc", + "comment_text": " Adds 1 to the current index. If at the final value, goes to 0\n", + "comment_hash": "3867b9c44095a165aa45476c45e2a20bed883e7bf8bfdd86e8407149f6488ec3", + "body_text": "pub fn next(&mut self) {\n let i = match self.state.selected() {\n Some(i) => {\n if i >= self.items.len() - 1 {\n 0\n } else {\n i + 1\n }\n }\n None => 0,\n };\n self.state.select(Some(i));\n }", + "body_hash": "31f6ce04926c6bcf9c6b05d4fdf44fa47f0488f7d614dd649b6ec6f7b03d579c" + }, + "tui/src/page_handler/ui_state.rs|crate::impl TableData::previous": { + "kind": "Function", + "file": "tui/src/page_handler/ui_state.rs", + "item_path": "crate::impl TableData::previous", + "comment_kind": "OuterDoc", + "comment_text": " Removes 1 from the current index. If index at 0, goes to the final index\n", + "comment_hash": "ccc8a68916bc749328d187f9e6615e54ea8ea4ec0847c846c9a0170c41fb6946", + "body_text": "pub fn previous(&mut self) {\n let i = match self.state.selected() {\n Some(i) => {\n if i == 0 {\n self.items.len() - 1\n } else {\n i - 1\n }\n }\n None => 0,\n };\n self.state.select(Some(i));\n }", + "body_hash": "973484f626aea0f3f048b6bd1dafc01e9a375d6f8e7c34aec27f0e21b5d7639d" + }, + "tui/src/pages/activity_ui.rs|crate::activity_ui#free:042daf211246": { + "kind": "Free", + "file": "tui/src/pages/activity_ui.rs", + "item_path": "crate::activity_ui#free:042daf211246", + "comment_kind": "Line", + "comment_text": "// Search and edit txs get an extra Status column", + "comment_hash": "042daf211246c81717ba4d2a4b599e3aacde675480dda5d3d767e6901488c7c5", + "body_text": "let activity_tx_header_vec = if add_extra_field {} else {};", + "body_hash": "eda92c8be38ae2d6af9839b7f3fdb14b8095473f09c8dd47feb64978565837cd" + }, + "tui/src/pages/activity_ui.rs|crate::activity_ui#free:081e9d50c8f4": { + "kind": "Free", + "file": "tui/src/pages/activity_ui.rs", + "item_path": "crate::activity_ui#free:081e9d50c8f4", + "comment_kind": "Line", + "comment_text": "// The first column is the date, which gets no separators. A yearly search\n// would otherwise end up with a comma in the year", + "comment_hash": "081e9d50c8f48801307f3f42239afb314fa298eb8c63dee16b8c91647503e07d", + "body_text": "let mut first_index_passed = false;\nlet cells = item.iter().map(|c| {});\nRow::new(cells)\n .height(height as u16)\n .bottom_margin(0)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n}", + "body_hash": "99e4b6647f764cd64af9018bc970ced08be10fa73de2370820467a620af9e230" + }, + "tui/src/pages/activity_ui.rs|crate::activity_ui#free:d4b4b487d811": { + "kind": "Free", + "file": "tui/src/pages/activity_ui.rs", + "item_path": "crate::activity_ui#free:d4b4b487d811", + "comment_kind": "Line", + "comment_text": "// The column widths have to match the extra column above", + "comment_hash": "d4b4b487d81157b9ce4f8421ff2f69bbfa0863ff6c3bea86557a2fe5791d5b15", + "body_text": "let activity_tx_header_widths = if add_extra_field {} else {};\nlet size = f.area();\nlet chunks = Layout::default()\n .direction(Direction::Vertical)\n .margin(2)\n .constraints([\n Constraint::Length(3),\n Constraint::Length(3),\n Constraint::Min(0),\n Constraint::Length(5),\n ])\n .split(size);\nf.render_widget(main_block(theme), size);\nlet tx_count = activity_view.total_activity();\nlet lerp_id = \"activity_tx_count\";\nlet lerp_tx_count = lerp_state.lerp(lerp_id, tx_count as f64, None) as i64;\nlet lerp_row = lerp_state.lerp(ACTIVITY_TABLE_ID, tx_count as f64, Some(0.50)) as usize;\nlet table_name = format!(\"Transactions: {lerp_tx_count}\");\nlet activity_header_cells = [\"Created At\", \"Activity Type\"]\n .iter()\n .map(|h| Cell::from(*h).style(Style::default().fg(theme.background())));\nlet activity_tx_header_cells = activity_tx_header_vec\n .iter()\n .map(|h| Cell::from(*h).style(Style::default().fg(theme.background())));\nlet activity_header = Row::new(activity_header_cells)\n .style(Style::default().bg(theme.header()))\n .height(1)\n .bottom_margin(0);\nlet activity_tx_header = Row::new(activity_tx_header_cells)\n .style(Style::default().bg(theme.header()))\n .height(1)\n .bottom_margin(0);\nlet activity_rows = table_data.items.iter().take(lerp_row).map(|item| {});\nlet activity_tx_rows = activity_txs_table.items.iter().map(|item| {});\nlet mut activity_table_area = Table::new(\n activity_rows,\n [Constraint::Percentage(50), Constraint::Percentage(50)],\n )\n .header(activity_header)\n .block(styled_block(&table_name, theme));\nlet activity_txs_table_area = Table::new(activity_tx_rows, activity_tx_header_widths)\n .header(activity_tx_header)\n .block(styled_block(\"TX Details\", theme));\nlet mut month_tab = create_tab(months, \"Months\", theme);\nlet mut year_tab = create_tab(years, \"Years\", theme);\nmatch current_tab {\n ActivityTab::Months => month_tab = month_tab.highlight_style(tab_highlight_style(theme)),\n ActivityTab::Years => year_tab = year_tab.highlight_style(tab_highlight_style(theme)),\n ActivityTab::List => {}\n }\nif let Some(index) = table_data.state.selected()\n && index > 10\n {}\nf.render_widget(year_tab, chunks[0]);\nf.render_widget(month_tab, chunks[1]);\nf.render_stateful_widget(activity_table_area, chunks[2], &mut table_data.state);\nf.render_stateful_widget(\n activity_txs_table_area,\n chunks[3],\n &mut activity_txs_table.state,\n );\n}", + "body_hash": "08e93a8b76223c175d3a020d51b4f3275fc5a5131c709168bbc15e787d48fc76" + }, + "tui/src/pages/add_tx_ui.rs|crate::add_tx_ui": { + "kind": "Function", + "file": "tui/src/pages/add_tx_ui.rs", + "item_path": "crate::add_tx_ui", + "comment_kind": "OuterDoc", + "comment_text": " The function draws the Add Transaction page of the interface.\n", + "comment_hash": "d79562eebc8b0481e14b1028f3ce2ec68bc3ba4332cbcac738782c1d1759efa2", + "body_text": "pub fn add_tx_ui(\n f: &mut Frame,\n balance: &mut [Vec],\n add_tx_data: &TxData,\n add_tx_tab: &TxTab,\n lerp_state: &mut LerpState,\n theme: &Theme,\n conn: &mut DbConn,\n) {\n let all_methods: Vec = conn\n .get_tx_methods_sorted()\n .iter()\n .map(|m| m.name.clone())\n .collect();\n \n\n let mut width_data = Vec::new();\n let total_columns = all_methods.len() + 2;\n let width_percent = (100 / total_columns) as u16;\n\n for _ in 0..total_columns {\n width_data.push(Constraint::Percentage(width_percent));\n }\n\n let status_data = add_tx_data.get_tx_status();\n \n \n \n let input_data = add_tx_data.get_all_texts();\n \n let current_index = add_tx_data.get_current_index();\n\n let size = f.area();\n\n let tx_type = add_tx_data.get_tx_type();\n\n let from_method_name = match tx_type {\n TxType::IncomeExpense => \"TX Method\",\n TxType::Transfer => \"From Method\",\n };\n\n \n let chunks = Layout::default()\n .direction(Direction::Vertical)\n .margin(2)\n .constraints([\n \n Constraint::Length(5),\n \n Constraint::Length(3),\n \n Constraint::Length(3),\n \n Constraint::Percentage(100),\n ])\n .split(size);\n\n \n \n let input_chunk = {\n match tx_type {\n TxType::IncomeExpense => Layout::default()\n .direction(Direction::Horizontal)\n .constraints([\n Constraint::Percentage(20),\n Constraint::Percentage(20),\n Constraint::Percentage(20),\n Constraint::Percentage(20),\n Constraint::Percentage(20),\n ])\n .split(chunks[1]),\n TxType::Transfer => Layout::default()\n .direction(Direction::Horizontal)\n .constraints([\n Constraint::Percentage(16),\n Constraint::Percentage(16),\n Constraint::Percentage(16),\n Constraint::Percentage(16),\n Constraint::Percentage(16),\n Constraint::Percentage(20),\n ])\n .split(chunks[1]),\n }\n };\n\n \n f.render_widget(main_block(theme), size);\n\n let bal_data = balance.iter().map(|item| {\n let height = 1;\n\n let row_type = HomeRow::get_row(item);\n let mut index = 0;\n\n let cells = item.iter().map(|c| {\n let c = if row_type != HomeRow::TopRow && ![\"Balance\", \"Changes\"].contains(&c.as_str())\n {\n \n let symbol = if c.contains('↑') || c.contains('↓') {\n c.chars().next()\n } else {\n None\n };\n\n \n let actual_data: f64 = if row_type != HomeRow::Changes {\n c.parse().unwrap()\n } else if let Some(sym) = symbol {\n let without_symbol = c.replace(sym, \"\");\n without_symbol.parse().unwrap()\n } else {\n c.parse().unwrap()\n };\n\n index += 1;\n\n let lerp_id = format!(\"{row_type}:{index}\");\n let to_show = lerp_state.lerp(&lerp_id, actual_data, None);\n\n \n \n if let Some(sym) = symbol {\n format!(\"{sym}{to_show:.2}\",).separate_with_commas()\n } else {\n format!(\"{to_show:.2}\").separate_with_commas()\n }\n } else {\n c.separate_with_commas()\n };\n\n if c.contains('↑') {\n Cell::from(c).style(Style::default().fg(theme.positive()))\n } else if c.contains('↓') {\n Cell::from(c).style(Style::default().fg(theme.negative()))\n } else if all_methods.contains(&c) || BALANCE_BOLD.contains(&c.as_str()) {\n Cell::from(c).style(Style::default().add_modifier(Modifier::BOLD))\n } else {\n Cell::from(c)\n }\n });\n Row::new(cells)\n .height(height as u16)\n .bottom_margin(0)\n .style(Style::default().fg(theme.text()))\n });\n\n let balance_area = Table::new(bal_data, width_data.clone())\n .block(styled_block(\"Balance Change\", theme))\n .style(Style::default().fg(theme.border()));\n\n let mut status_text = vec![];\n\n \n \n for i in status_data.iter().rev() {\n let (initial, rest) = i.text.split_once(':').unwrap();\n\n match i.log_type {\n LogType::Info => {\n status_text.push(Line::from(vec![\n Span::styled(\n initial,\n Style::default()\n .fg(theme.positive())\n .add_modifier(Modifier::BOLD),\n ),\n Span::styled(format!(\":{rest}\"), Style::default().fg(theme.positive())),\n ]));\n }\n LogType::Error => {\n status_text.push(Line::from(vec![\n Span::styled(\n initial,\n Style::default()\n .fg(theme.negative())\n .add_modifier(Modifier::BOLD),\n ),\n Span::styled(format!(\":{rest}\"), Style::default().fg(theme.negative())),\n ]));\n }\n }\n }\n\n \n \n let date_text = Line::from(format!(\"{} \", input_data[0]));\n\n let mut details_text = Line::from(format!(\"{} \", input_data[1]));\n\n let mut from_method_text = Line::from(format!(\"{} \", input_data[2]));\n\n let mut to_method_text = Line::from(format!(\"{} \", input_data[3]));\n\n let amount_text = Line::from(format!(\"{} \", input_data[4]));\n\n let mut tx_type_text = Line::from(format!(\"{} \", input_data[5]));\n\n let mut tags_text = Line::from(format!(\"{} \", input_data[6]));\n\n match add_tx_tab {\n TxTab::Details => {\n details_text = Line::from(vec![\n Span::from(format!(\"{} \", input_data[1])),\n Span::styled(input_data[7], Style::default().fg(theme.autocomplete())),\n ]);\n }\n TxTab::FromMethod => {\n from_method_text = Line::from(vec![\n Span::from(format!(\"{} \", input_data[2])),\n Span::styled(input_data[7], Style::default().fg(theme.autocomplete())),\n ]);\n }\n TxTab::ToMethod => {\n to_method_text = Line::from(vec![\n Span::from(format!(\"{} \", input_data[3])),\n Span::styled(input_data[7], Style::default().fg(theme.autocomplete())),\n ]);\n }\n TxTab::Tags => {\n tags_text = Line::from(vec![\n Span::from(format!(\"{} \", input_data[6])),\n Span::styled(input_data[7], Style::default().fg(theme.autocomplete())),\n ]);\n }\n TxTab::TxType => {\n tx_type_text = Line::from(vec![\n Span::from(format!(\"{} \", input_data[5])),\n Span::styled(input_data[7], Style::default().fg(theme.autocomplete())),\n ]);\n }\n _ => {}\n }\n\n let status_sec = Paragraph::new(status_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(\"Status\", theme))\n .alignment(Alignment::Left);\n\n let date_sec = Paragraph::new(date_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(\"Date\", theme))\n .alignment(Alignment::Left);\n\n let from_method_sec = Paragraph::new(from_method_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(from_method_name, theme))\n .alignment(Alignment::Left);\n\n let to_method_sec = Paragraph::new(to_method_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(\"To Method\", theme))\n .alignment(Alignment::Left);\n\n let amount_sec = Paragraph::new(amount_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(\"Amount\", theme))\n .alignment(Alignment::Left);\n\n let tx_type_sec = Paragraph::new(tx_type_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(\"TX Type\", theme))\n .alignment(Alignment::Left);\n\n let details_sec = Paragraph::new(details_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(\"Details\", theme))\n .alignment(Alignment::Left);\n\n let tags_sec = Paragraph::new(tags_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(\"Tags\", theme))\n .alignment(Alignment::Left);\n\n \n \n match add_tx_tab {\n TxTab::Date => f.set_cursor_position(Position {\n x: input_chunk[0].x + current_index as u16 + 1,\n y: input_chunk[0].y + 1,\n }),\n TxTab::Details => f.set_cursor_position(Position {\n x: chunks[2].x + current_index as u16 + 1,\n y: chunks[2].y + 1,\n }),\n TxTab::TxType => f.set_cursor_position(Position {\n x: input_chunk[1].x + current_index as u16 + 1,\n y: input_chunk[1].y + 1,\n }),\n TxTab::FromMethod => f.set_cursor_position(Position {\n x: input_chunk[2].x + current_index as u16 + 1,\n y: input_chunk[2].y + 1,\n }),\n _ => {}\n }\n\n match tx_type {\n TxType::IncomeExpense => match add_tx_tab {\n TxTab::Amount => f.set_cursor_position(Position {\n x: input_chunk[3].x + current_index as u16 + 1,\n y: input_chunk[3].y + 1,\n }),\n\n TxTab::Tags => f.set_cursor_position(Position {\n x: input_chunk[4].x + current_index as u16 + 1,\n y: input_chunk[4].y + 1,\n }),\n _ => {}\n },\n TxType::Transfer => match add_tx_tab {\n TxTab::ToMethod => f.set_cursor_position(Position {\n x: input_chunk[3].x + current_index as u16 + 1,\n y: input_chunk[3].y + 1,\n }),\n TxTab::Amount => f.set_cursor_position(Position {\n x: input_chunk[4].x + current_index as u16 + 1,\n y: input_chunk[4].y + 1,\n }),\n\n TxTab::Tags => f.set_cursor_position(Position {\n x: input_chunk[5].x + current_index as u16 + 1,\n y: input_chunk[5].y + 1,\n }),\n _ => {}\n },\n }\n\n f.render_widget(balance_area, chunks[0]);\n f.render_widget(details_sec, chunks[2]);\n f.render_widget(status_sec, chunks[3]);\n f.render_widget(date_sec, input_chunk[0]);\n f.render_widget(tx_type_sec, input_chunk[1]);\n f.render_widget(from_method_sec, input_chunk[2]);\n\n match tx_type {\n TxType::IncomeExpense => {\n f.render_widget(amount_sec, input_chunk[3]);\n f.render_widget(tags_sec, input_chunk[4]);\n }\n TxType::Transfer => {\n f.render_widget(to_method_sec, input_chunk[3]);\n f.render_widget(amount_sec, input_chunk[4]);\n f.render_widget(tags_sec, input_chunk[5]);\n }\n }\n}", + "body_hash": "fa9ec61efe034a7be71c1b277f4464c9e7cd783f86f04dbd70677b6cfa0fba9c" + }, + "tui/src/pages/add_tx_ui.rs|crate::add_tx_ui#free:37e26499b6b0": { + "kind": "Free", + "file": "tui/src/pages/add_tx_ui.rs", + "item_path": "crate::add_tx_ui#free:37e26499b6b0", + "comment_kind": "Line", + "comment_text": "// Put the symbol back on the Changes row, otherwise the number\n// gets thousands separators", + "comment_hash": "37e26499b6b07f0ceaa3b7856a3c7f335898ba6100b7126e8aebed967112db4b", + "body_text": "if let Some(sym) = symbol {} else {}\n}", + "body_hash": "d8f6d3a68dfe79946d3b6de5571be870184a9a83258751ec3834e12c829edd39" + }, + "tui/src/pages/add_tx_ui.rs|crate::add_tx_ui#free:7daf9b3f5456": { + "kind": "Free", + "file": "tui/src/pages/add_tx_ui.rs", + "item_path": "crate::add_tx_ui#free:7daf9b3f5456", + "comment_kind": "Line", + "comment_text": "// The index of the cursor position", + "comment_hash": "7daf9b3f545601e8ed718e936c10e7ad4637a5471cc11af2b0861fd68d1afc6b", + "body_text": "let current_index = add_tx_data.get_current_index();\nlet size = f.area();\nlet tx_type = add_tx_data.get_tx_type();\nlet from_method_name = match tx_type {\n TxType::IncomeExpense => \"TX Method\",\n TxType::Transfer => \"From Method\",\n };", + "body_hash": "3ebc6ef321183aa7d2e00d1cfedaedd47e447c2fa7b62f78cb3c1893cae9d4fa" + }, + "tui/src/pages/add_tx_ui.rs|crate::add_tx_ui#free:8084f6795638": { + "kind": "Free", + "file": "tui/src/pages/add_tx_ui.rs", + "item_path": "crate::add_tx_ui#free:8084f6795638", + "comment_kind": "Line", + "comment_text": "// Depending on the tx type, split the first chunk into 5 or 6 columns.\n// This chunk holds the input boxes that take the user input", + "comment_hash": "8084f6795638c13ff55e835b2242897cb563a9e04f0597260e180af375fe4dc1", + "body_text": "let input_chunk = {};", + "body_hash": "f5386f9fc17dadfaabbbb0dd05055e60c81076507060de6a1316b5de81d7f6df" + }, + "tui/src/pages/add_tx_ui.rs|crate::add_tx_ui#free:8c1ed3108bba": { + "kind": "Free", + "file": "tui/src/pages/add_tx_ui.rs", + "item_path": "crate::add_tx_ui#free:8c1ed3108bba", + "comment_kind": "Line", + "comment_text": "// Divide the terminal into 3 parts vertically", + "comment_hash": "8c1ed3108bba242b2e26cb4c97715200b770327f47032966b1f2b802bd84b041", + "body_text": "let chunks = Layout::default()\n .direction(Direction::Vertical)\n .margin(2)\n .constraints([\n \n Constraint::Length(5),\n \n Constraint::Length(3),\n \n Constraint::Length(3),\n \n Constraint::Percentage(100),\n ])\n .split(size);", + "body_hash": "79a82fb7f18cecdfb35dd8108bd1829d2c155b3c02cacf358c8eb9322da4d79f" + }, + "tui/src/pages/add_tx_ui.rs|crate::add_tx_ui#free:9c1666ab54c2": { + "kind": "Free", + "file": "tui/src/pages/add_tx_ui.rs", + "item_path": "crate::add_tx_ui#free:9c1666ab54c2", + "comment_kind": "Line", + "comment_text": "// Iterate in reverse so the latest status line, which is the last value of the\n// vector, ends up at the top", + "comment_hash": "9c1666ab54c239438fc77e345d197d3eca728959b0437d44909fcd8eb3599b79", + "body_text": "for i in status_data.iter().rev() {}", + "body_hash": "5aa1224f50427fa656b674baf08c3fe11b253108b75b85c333df26785776133d" + }, + "tui/src/pages/add_tx_ui.rs|crate::add_tx_ui#free:a27428c44abe": { + "kind": "Free", + "file": "tui/src/pages/add_tx_ui.rs", + "item_path": "crate::add_tx_ui#free:a27428c44abe", + "comment_kind": "Line", + "comment_text": "// Get the data to insert into the Status widget of this page", + "comment_hash": "a27428c44abe722d135da31437c0259c6c20c73f813edbd8730dd9a1d58ec0d0", + "body_text": "let mut width_data = Vec::new();\nlet total_columns = all_methods.len() + 2;\nlet width_percent = (100 / total_columns) as u16;\nfor _ in 0..total_columns {}\nlet status_data = add_tx_data.get_tx_status();", + "body_hash": "67195c8551937ae5966c8a2a3956bb1f98e8192e791633b2428f6661fe061eed" + }, + "tui/src/pages/add_tx_ui.rs|crate::add_tx_ui#free:b96e5e75cb1a": { + "kind": "Free", + "file": "tui/src/pages/add_tx_ui.rs", + "item_path": "crate::add_tx_ui#free:b96e5e75cb1a", + "comment_kind": "Line", + "comment_text": "// Date, details, from method, to method, amount, tx type, tags and autofill.\n// The widgets are rendered from this, skipping the to method unless the tx is\n// a transfer", + "comment_hash": "b96e5e75cb1a8c10a0a1c02707720e15429c34fcf5f8d6dd3394d4decadfa49b", + "body_text": "let input_data = add_tx_data.get_all_texts();", + "body_hash": "a5c293a9e08442233cd1a6b01cc29491b559bbad92e0e31e74139b9df1cbd62f" + }, + "tui/src/pages/add_tx_ui.rs|crate::add_tx_ui#free:c15ef47bb7a6": { + "kind": "Free", + "file": "tui/src/pages/add_tx_ui.rs", + "item_path": "crate::add_tx_ui#free:c15ef47bb7a6", + "comment_kind": "Line", + "comment_text": "// If loading was complete then this value is to be shown", + "comment_hash": "c15ef47bb7a687d69fdd18c88a1f707bcb57973c4862845646c66db742091761", + "body_text": "let actual_data: f64 = if row_type != HomeRow::Changes {} else if let Some(sym) = symbol {} else {};\nindex += 1;\nlet lerp_id = format!(\"{row_type}:{index}\");\nlet to_show = lerp_state.lerp(&lerp_id, actual_data, None);", + "body_hash": "5a9085b94015d97b317b206aadd9aff9f0364410de67cef92e1744d6f4c5f6b9" + }, + "tui/src/pages/add_tx_ui.rs|crate::add_tx_ui#free:c938486cd94f": { + "kind": "Free", + "file": "tui/src/pages/add_tx_ui.rs", + "item_path": "crate::add_tx_ui#free:c938486cd94f", + "comment_kind": "Line", + "comment_text": "// Put the cursor at the index of the currently selected field.\n// Adapted from the tui-rs example named user_input.rs", + "comment_hash": "c938486cd94f7843035726b01708e01821ad7e60ad650da2ad714d72a27facf6", + "body_text": "match add_tx_tab {\n TxTab::Date => f.set_cursor_position(Position {\n x: input_chunk[0].x + current_index as u16 + 1,\n y: input_chunk[0].y + 1,\n }),\n TxTab::Details => f.set_cursor_position(Position {\n x: chunks[2].x + current_index as u16 + 1,\n y: chunks[2].y + 1,\n }),\n TxTab::TxType => f.set_cursor_position(Position {\n x: input_chunk[1].x + current_index as u16 + 1,\n y: input_chunk[1].y + 1,\n }),\n TxTab::FromMethod => f.set_cursor_position(Position {\n x: input_chunk[2].x + current_index as u16 + 1,\n y: input_chunk[2].y + 1,\n }),\n _ => {}\n }\nmatch tx_type {\n TxType::IncomeExpense => match add_tx_tab {\n TxTab::Amount => f.set_cursor_position(Position {\n x: input_chunk[3].x + current_index as u16 + 1,\n y: input_chunk[3].y + 1,\n }),\n\n TxTab::Tags => f.set_cursor_position(Position {\n x: input_chunk[4].x + current_index as u16 + 1,\n y: input_chunk[4].y + 1,\n }),\n _ => {}\n },\n TxType::Transfer => match add_tx_tab {\n TxTab::ToMethod => f.set_cursor_position(Position {\n x: input_chunk[3].x + current_index as u16 + 1,\n y: input_chunk[3].y + 1,\n }),\n TxTab::Amount => f.set_cursor_position(Position {\n x: input_chunk[4].x + current_index as u16 + 1,\n y: input_chunk[4].y + 1,\n }),\n\n TxTab::Tags => f.set_cursor_position(Position {\n x: input_chunk[5].x + current_index as u16 + 1,\n y: input_chunk[5].y + 1,\n }),\n _ => {}\n },\n }\nf.render_widget(balance_area, chunks[0]);\nf.render_widget(details_sec, chunks[2]);\nf.render_widget(status_sec, chunks[3]);\nf.render_widget(date_sec, input_chunk[0]);\nf.render_widget(tx_type_sec, input_chunk[1]);\nf.render_widget(from_method_sec, input_chunk[2]);\nmatch tx_type {\n TxType::IncomeExpense => {}\n TxType::Transfer => {}\n }\n}", + "body_hash": "9fe7b1f6748770a65abb3490c5761e06c0d435aade3222be880fd0d89591e4a1" + }, + "tui/src/pages/add_tx_ui.rs|crate::add_tx_ui#free:e680dade6c26": { + "kind": "Free", + "file": "tui/src/pages/add_tx_ui.rs", + "item_path": "crate::add_tx_ui#free:e680dade6c26", + "comment_kind": "Line", + "comment_text": "// The values were already fetched above, so they just need to be assigned\n// and loaded into their widgets", + "comment_hash": "e680dade6c265cfb1e705e9d88888b99fcba4daa656b3481d250e1941d029eaa", + "body_text": "let date_text = Line::from(format!(\"{} \", input_data[0]));\nlet mut details_text = Line::from(format!(\"{} \", input_data[1]));\nlet mut from_method_text = Line::from(format!(\"{} \", input_data[2]));\nlet mut to_method_text = Line::from(format!(\"{} \", input_data[3]));\nlet amount_text = Line::from(format!(\"{} \", input_data[4]));\nlet mut tx_type_text = Line::from(format!(\"{} \", input_data[5]));\nlet mut tags_text = Line::from(format!(\"{} \", input_data[6]));\nmatch add_tx_tab {\n TxTab::Details => {}\n TxTab::FromMethod => {}\n TxTab::ToMethod => {}\n TxTab::Tags => {}\n TxTab::TxType => {}\n _ => {}\n }\nlet status_sec = Paragraph::new(status_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(\"Status\", theme))\n .alignment(Alignment::Left);\nlet date_sec = Paragraph::new(date_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(\"Date\", theme))\n .alignment(Alignment::Left);\nlet from_method_sec = Paragraph::new(from_method_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(from_method_name, theme))\n .alignment(Alignment::Left);\nlet to_method_sec = Paragraph::new(to_method_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(\"To Method\", theme))\n .alignment(Alignment::Left);\nlet amount_sec = Paragraph::new(amount_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(\"Amount\", theme))\n .alignment(Alignment::Left);\nlet tx_type_sec = Paragraph::new(tx_type_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(\"TX Type\", theme))\n .alignment(Alignment::Left);\nlet details_sec = Paragraph::new(details_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(\"Details\", theme))\n .alignment(Alignment::Left);\nlet tags_sec = Paragraph::new(tags_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(\"Tags\", theme))\n .alignment(Alignment::Left);", + "body_hash": "f6be382da8d716cdca228bd7627d78fece7cc38d5a46e37570ddecb77dabef5f" + }, + "tui/src/pages/add_tx_ui.rs|crate::add_tx_ui#free:ed98a63512e5": { + "kind": "Free", + "file": "tui/src/pages/add_tx_ui.rs", + "item_path": "crate::add_tx_ui#free:ed98a63512e5", + "comment_kind": "Line", + "comment_text": "// Changes row can contain arrow symbols", + "comment_hash": "ed98a63512e55d3edabf0fff7754033ff310882cf5f07260d73c736b77407047", + "body_text": "let symbol = if c.contains('↑') || c.contains('↓') {} else {};", + "body_hash": "b42a79c23a7d289c1cac9ab7e6aa00b2ea3e94fe23db4716bd5189b299c04503" + }, + "tui/src/pages/add_tx_ui.rs|crate::add_tx_ui#free:ff561b0d9be3": { + "kind": "Free", + "file": "tui/src/pages/add_tx_ui.rs", + "item_path": "crate::add_tx_ui#free:ff561b0d9be3", + "comment_kind": "Line", + "comment_text": "// Creates border around the entire terminal", + "comment_hash": "ff561b0d9be3224b22319c008da3b83313defef033b8d8625f678bd48234598e", + "body_text": "f.render_widget(main_block(theme), size);\nlet bal_data = balance.iter().map(|item| {});\nlet balance_area = Table::new(bal_data, width_data.clone())\n .block(styled_block(\"Balance Change\", theme))\n .style(Style::default().fg(theme.border()));\nlet mut status_text = vec![];", + "body_hash": "f15e338180e02de426c3258c4a2390df4f9d45898013b8b2460da1552ed981bb" + }, + "tui/src/pages/chart_ui.rs|crate::chart_ui": { + "kind": "Function", + "file": "tui/src/pages/chart_ui.rs", + "item_path": "crate::chart_ui", + "comment_kind": "OuterDoc", + "comment_text": " Creates the balance chart from the transactions\n", + "comment_hash": "44b650027cfb510bc8903182c7ef916705e758de54b81dd9fa200b7d5c4bb749", + "body_text": "pub fn chart_ui(\n f: &mut Frame,\n months: &IndexedData,\n years: &IndexedData,\n mode_selection: &IndexedData,\n chart_tx_methods: &IndexedData,\n current_page: &ChartTab,\n chart_hidden_mode: bool,\n chart_hidden_legends: bool,\n chart_activated_methods: &HashMap,\n lerp_state: &mut LerpState,\n chart_view: &ChartView,\n theme: &Theme,\n conn: &mut DbConn,\n) {\n let size = f.area();\n\n \n let mut main_layout = Layout::default().direction(Direction::Vertical).margin(2);\n\n \n if chart_hidden_mode {\n main_layout = main_layout.constraints([Constraint::Min(0)]);\n } else {\n match mode_selection.index {\n 0 => {\n main_layout = main_layout.constraints([\n \n Constraint::Length(3),\n \n Constraint::Length(3),\n \n Constraint::Length(3),\n \n Constraint::Length(3),\n \n Constraint::Min(0),\n ]);\n }\n 1 => {\n main_layout = main_layout.constraints([\n \n Constraint::Length(3),\n \n Constraint::Length(3),\n \n Constraint::Length(3),\n \n Constraint::Min(0),\n ]);\n }\n 2 => {\n main_layout = main_layout.constraints([\n \n Constraint::Length(3),\n \n Constraint::Length(3),\n \n Constraint::Min(0),\n ]);\n }\n _ => {}\n }\n }\n\n let chunks = main_layout.split(size);\n\n \n f.render_widget(main_block(theme), size);\n\n let mut month_tab = create_tab(months, \"Months\", theme);\n\n let mut year_tab = create_tab(years, \"Years\", theme);\n\n let mut mode_selection_tab = create_tab(mode_selection, \"Modes\", theme);\n\n let mut tx_method_selection_tab = create_tab_activation(\n chart_tx_methods,\n \"Tx Method Selection\",\n chart_activated_methods,\n theme,\n );\n\n let tx_methods = conn.get_tx_methods_sorted();\n\n let mut all_tx_methods: Vec<&str> = tx_methods.iter().map(|t| t.name.as_str()).collect();\n all_tx_methods.push(\"Cumulative\");\n\n \n let mut datasets: Vec> = Vec::new();\n\n let mut last_balances = Vec::new();\n\n \n if chart_view.is_empty() {\n for _ in 0..all_tx_methods.len() {\n datasets.push(vec![(0.0, 0.0)]);\n last_balances.push(0.0);\n }\n }\n\n let mut lowest_balance = 0.0;\n let mut highest_balance = 0.0;\n\n let mut date_labels: Vec = vec![];\n\n let mut current_axis = 0.0;\n\n \n if !chart_view.is_empty() {\n let mut checking_date = chart_view.start_date();\n\n \n let final_date = chart_view.end_date();\n\n \n let total_loop = final_date.signed_duration_since(checking_date).num_days() as f64;\n\n let lerp_id = \"chart_loop_size\";\n let mut to_loop = lerp_state.lerp(lerp_id, total_loop, None);\n\n \n date_labels.push(checking_date.to_string());\n date_labels.push(final_date.to_string());\n\n \n \n \n\n let mut to_add_again = false;\n let mut data_num = 0;\n loop {\n if chart_view.contains_date(&checking_date) {\n let current_balances = chart_view.get_balance(data_num);\n\n \n let mut next_date = NaiveDate::default();\n\n if chart_view.len() > data_num + 1 {\n next_date = chart_view.get_tx(data_num + 1).date.date();\n }\n \n \n \n last_balances = Vec::new();\n\n let mut cumulative_balance = 0.0;\n\n for method_index in 0..all_tx_methods.len() {\n \n let current_balance = if method_index == all_tx_methods.len() - 1 {\n cumulative_balance\n } else {\n let method_at_index = tx_methods[method_index];\n let balance = current_balances.get(&method_at_index.id).unwrap().dollar();\n\n cumulative_balance += balance.value();\n\n balance.value()\n };\n\n \n \n \n \n if chart_activated_methods[all_tx_methods[method_index]] {\n if current_balance > highest_balance {\n highest_balance = current_balance;\n } else if current_balance < lowest_balance {\n lowest_balance = current_balance;\n }\n }\n\n if to_add_again {\n \n \n \n \n\n let (position, _balance) = datasets[method_index].pop().unwrap();\n let to_push = vec![(position, current_balance)];\n datasets[method_index].extend(to_push);\n } else {\n let to_push = vec![(current_axis, current_balance)];\n\n if datasets.get(method_index).is_some() {\n datasets[method_index].extend(to_push);\n } else {\n datasets.push(to_push);\n }\n }\n\n last_balances.push(current_balance);\n }\n\n if next_date == checking_date {\n \n to_add_again = true;\n } else {\n to_add_again = false;\n current_axis += 1.0;\n checking_date += Duration::days(1);\n }\n\n \n data_num += 1;\n } else {\n \n for method_index in 0..all_tx_methods.len() {\n let to_push = vec![(current_axis, last_balances[method_index])];\n datasets[method_index].extend(to_push);\n }\n current_axis += 1.0;\n checking_date += Duration::days(1);\n }\n\n if !to_add_again {\n \n if to_loop <= 0.0 {\n date_labels.pop().unwrap();\n date_labels.push(checking_date.to_string());\n break;\n }\n to_loop += -1.0;\n }\n\n if checking_date == NaiveDate::default() {\n break;\n }\n }\n }\n\n \n highest_balance += highest_balance * 5.0 / 100.0;\n lowest_balance -= lowest_balance * 5.0 / 100.0;\n\n let diff = (highest_balance - lowest_balance) / 10.0;\n\n let mut to_add = lowest_balance;\n\n \n \n let mut labels = vec![lowest_balance.to_string()];\n\n for _i in 0..10 {\n to_add += diff;\n labels.push(format!(\"{to_add:.2}\"));\n }\n\n let mut color_list = vec![\n Color::LightRed,\n Color::LightBlue,\n Color::LightYellow,\n Color::Gray,\n Color::Black,\n Color::Yellow,\n Color::Green,\n Color::Red,\n Color::Blue,\n Color::Magenta,\n ];\n\n let mut final_dataset = vec![];\n\n \n for i in 0..all_tx_methods.len() {\n \n if color_list.is_empty() {\n color_list.push(Color::Cyan);\n }\n\n if !chart_activated_methods[all_tx_methods[i]] {\n continue;\n }\n\n let mut dataset = Dataset::default()\n .marker(Marker::Braille)\n .graph_type(GraphType::Line)\n .style(\n Style::default()\n .fg(color_list.pop().unwrap())\n .bg(theme.background()),\n )\n .data(&datasets[i]);\n\n if !chart_hidden_legends {\n dataset = dataset.name(all_tx_methods[i]);\n }\n\n final_dataset.push(dataset);\n }\n\n let chart = Chart::new(final_dataset)\n .block(Block::default().style(Style::default().bg(theme.background()).fg(theme.border())))\n .style(Style::default().bg(theme.background()).fg(theme.border()))\n .x_axis(\n Axis::default()\n .title(Span::styled(\n \"\",\n Style::default().bg(theme.background()).fg(theme.border()),\n ))\n .style(Style::default().bg(theme.background()).fg(theme.border()))\n .bounds([0.0, current_axis - 1.0])\n .labels(\n date_labels\n .iter()\n .cloned()\n .map(Span::from)\n .collect::>(),\n ),\n )\n .y_axis(\n Axis::default()\n .title(Span::styled(\n \"\",\n Style::default().bg(theme.background()).fg(theme.border()),\n ))\n .style(Style::default().bg(theme.background()).fg(theme.border()))\n .bounds([lowest_balance, highest_balance])\n .labels(labels.iter().cloned().map(Span::from).collect::>()),\n );\n\n match current_page {\n ChartTab::Months => month_tab = month_tab.highlight_style(tab_highlight_style(theme)),\n ChartTab::Years => year_tab = year_tab.highlight_style(tab_highlight_style(theme)),\n ChartTab::ModeSelection => {\n mode_selection_tab = mode_selection_tab.highlight_style(tab_highlight_style(theme));\n }\n ChartTab::TxMethods => {\n tx_method_selection_tab =\n tx_method_selection_tab.highlight_style(tab_highlight_style(theme));\n }\n }\n\n if chart_hidden_mode {\n f.render_widget(chart, chunks[0]);\n } else {\n f.render_widget(mode_selection_tab, chunks[0]);\n\n match mode_selection.index {\n 0 => {\n f.render_widget(year_tab, chunks[1]);\n f.render_widget(month_tab, chunks[2]);\n f.render_widget(tx_method_selection_tab, chunks[3]);\n f.render_widget(chart, chunks[4]);\n }\n 1 => {\n f.render_widget(year_tab, chunks[1]);\n f.render_widget(tx_method_selection_tab, chunks[2]);\n f.render_widget(chart, chunks[3]);\n }\n 2 => {\n f.render_widget(tx_method_selection_tab, chunks[1]);\n f.render_widget(chart, chunks[2]);\n }\n _ => {}\n }\n }\n}", + "body_hash": "43d103f0764d9a78e7a9d28c99c01e000210dbb6aa8282f3880555ce6a1b37e1" + }, + "tui/src/pages/chart_ui.rs|crate::chart_ui#free:07f5d316839a": { + "kind": "Free", + "file": "tui/src/pages/chart_ui.rs", + "item_path": "crate::chart_ui#free:07f5d316839a", + "comment_kind": "Line", + "comment_text": "// labels of the x axis", + "comment_hash": "07f5d316839a47d6f9368bbf0ac05fb893047f81c246c1f19f78cea0075b4b4f", + "body_text": "date_labels.push(checking_date.to_string());\ndate_labels.push(final_date.to_string());", + "body_hash": "6a206a8ee8dc2b361083fb7f480282c0b3734187ca6f14ad4e9f3d1f08018b6b" + }, + "tui/src/pages/chart_ui.rs|crate::chart_ui#free:0ac8b377932d": { + "kind": "Free", + "file": "tui/src/pages/chart_ui.rs", + "item_path": "crate::chart_ui#free:0ac8b377932d", + "comment_kind": "Line", + "comment_text": "// This tx was consumed, so read the next index on the next iteration", + "comment_hash": "0ac8b377932dc0922cf550dec50f2e628a5a5eef4a43a8ba4cb82f4918b3f7f6", + "body_text": "data_num += 1;\n}", + "body_hash": "8e25ed185b9dbb54b79e94988b366c4d86117cc20b7d0bc462e99f6f53211fe2" + }, + "tui/src/pages/chart_ui.rs|crate::chart_ui#free:3e932f6000f7": { + "kind": "Free", + "file": "tui/src/pages/chart_ui.rs", + "item_path": "crate::chart_ui#free:3e932f6000f7", + "comment_kind": "Line", + "comment_text": "// A deactivated method must not move the highest/lowest\n// balance, but it cannot be skipped either since the dataset\n// expects a value at every method's index. The data is there,\n// it just isn't drawn", + "comment_hash": "3e932f6000f7024bbc17f0f42a1691c9d0acd9291bfde990734c43378c72bc88", + "body_text": "if chart_activated_methods[all_tx_methods[method_index]] {}\nif to_add_again {} else {}\nlast_balances.push(current_balance);\n}", + "body_hash": "5f59d81dbe048c5e1b8f576b1e5b92c22c8aaad1eb07c1af25c023037c5b443e" + }, + "tui/src/pages/chart_ui.rs|crate::chart_ui#free:3f9ce259931d": { + "kind": "Free", + "file": "tui/src/pages/chart_ui.rs", + "item_path": "crate::chart_ui#free:3f9ce259931d", + "comment_kind": "Line", + "comment_text": "// Loop through the data that was added for each tx_method and turn them into chart data", + "comment_hash": "3f9ce259931dc4fa701d8e30de8b639c95d65ce118a6f4a7aed83fe37cae77b5", + "body_text": "for i in 0..all_tx_methods.len() {}\nlet chart = Chart::new(final_dataset)\n .block(Block::default().style(Style::default().bg(theme.background()).fg(theme.border())))\n .style(Style::default().bg(theme.background()).fg(theme.border()))\n .x_axis(\n Axis::default()\n .title(Span::styled(\n \"\",\n Style::default().bg(theme.background()).fg(theme.border()),\n ))\n .style(Style::default().bg(theme.background()).fg(theme.border()))\n .bounds([0.0, current_axis - 1.0])\n .labels(\n date_labels\n .iter()\n .cloned()\n .map(Span::from)\n .collect::>(),\n ),\n )\n .y_axis(\n Axis::default()\n .title(Span::styled(\n \"\",\n Style::default().bg(theme.background()).fg(theme.border()),\n ))\n .style(Style::default().bg(theme.background()).fg(theme.border()))\n .bounds([lowest_balance, highest_balance])\n .labels(labels.iter().cloned().map(Span::from).collect::>()),\n );\nmatch current_page {\n ChartTab::Months => month_tab = month_tab.highlight_style(tab_highlight_style(theme)),\n ChartTab::Years => year_tab = year_tab.highlight_style(tab_highlight_style(theme)),\n ChartTab::ModeSelection => {}\n ChartTab::TxMethods => {}\n }\nif chart_hidden_mode {} else {}\n}", + "body_hash": "eb3046dc234e8cf86f86ca7666a124d5f1c640efc99ecb00afd1a2f34b90a88a" + }, + "tui/src/pages/chart_ui.rs|crate::chart_ui#free:4625f66030de": { + "kind": "Free", + "file": "tui/src/pages/chart_ui.rs", + "item_path": "crate::chart_ui#free:4625f66030de", + "comment_kind": "Line", + "comment_text": "// One [X, Y] coordinate per point to draw, one such vec per tx method", + "comment_hash": "4625f66030de10bfad470106ce53e2efc79a713e784ac6c0287265a63da78223", + "body_text": "let mut datasets: Vec> = Vec::new();\nlet mut last_balances = Vec::new();", + "body_hash": "8fc9a98499bd79756ff3a3cb69a5aca51e3410247d64da258e05ef758906d6e7" + }, + "tui/src/pages/chart_ui.rs|crate::chart_ui#free:4d2bff2c0f5a": { + "kind": "Free", + "file": "tui/src/pages/chart_ui.rs", + "item_path": "crate::chart_ui#free:4d2bff2c0f5a", + "comment_kind": "Line", + "comment_text": "// `to_add_again` means the last loop already added a point\n// for this same date, so pop it and re-add it with this\n// loop's balance merged in. As long as the next date stays\n// the same, `last_balance` keeps feeding the merge.", + "comment_hash": "4d2bff2c0f5ae3844e0313de0d272e604c49518c39a824b2611b7be7fcd7de6a", + "body_text": "let (position, _balance) = datasets[method_index].pop().unwrap();\nlet to_push = vec![(position, current_balance)];\ndatasets[method_index].extend(to_push);\n}", + "body_hash": "355258b252ec10acc4dada1baf33c6c3162f0b2f8d2e0066e1306d48d5b847c1" + }, + "tui/src/pages/chart_ui.rs|crate::chart_ui#free:53133aaca19c": { + "kind": "Free", + "file": "tui/src/pages/chart_ui.rs", + "item_path": "crate::chart_ui#free:53133aaca19c", + "comment_kind": "Line", + "comment_text": "// Keep track of the highest and the lowest point of the balance", + "comment_hash": "53133aaca19ca287d7011e2ef7d63e576143b09dfdf0c51d6f951925059168c4", + "body_text": "let current_balance = if method_index == all_tx_methods.len() - 1 {} else {};", + "body_hash": "e6a7eb5f43cc9383d299ca2161d4216e7556e0cebdf7f40f490905dfb749d776" + }, + "tui/src/pages/chart_ui.rs|crate::chart_ui#free:61f4e84f2131": { + "kind": "Free", + "file": "tui/src/pages/chart_ui.rs", + "item_path": "crate::chart_ui#free:61f4e84f2131", + "comment_kind": "Line", + "comment_text": "// If there are no transactions, we will create an empty chart", + "comment_hash": "61f4e84f2131ee5914470c6a9d26415e796080b75b85ed96bf5fecd8032908c6", + "body_text": "if !chart_view.is_empty() {}", + "body_hash": "272ba5a830548ded97a3f066887ab6fb2f868d999633ec41f4600dae8cbcdad4" + }, + "tui/src/pages/chart_ui.rs|crate::chart_ui#free:66f9747b8505": { + "kind": "Free", + "file": "tui/src/pages/chart_ui.rs", + "item_path": "crate::chart_ui#free:66f9747b8505", + "comment_kind": "Line", + "comment_text": "// `data_num` is the index to read the txs and balances from.\n// `to_add_again` becomes true when two or more txs share the same date, as\n// the movements of such a day are combined into a single chart point", + "comment_hash": "66f9747b8505c2bc3de048f77490ed628829226a5e939bc8cc38a702ecca17cc", + "body_text": "let mut to_add_again = false;\nlet mut data_num = 0;\nloop {}\n}", + "body_hash": "1c21cbd369df3e5dd0cc64a0e60a8b1169c2ac2065387a8556c995b2e9feae04" + }, + "tui/src/pages/chart_ui.rs|crate::chart_ui#free:6c917ae71cff": { + "kind": "Free", + "file": "tui/src/pages/chart_ui.rs", + "item_path": "crate::chart_ui#free:6c917ae71cff", + "comment_kind": "Line", + "comment_text": "// Start at the lowest balance and keep adding the difference to reach the\n// highest one. 10 labels, so loop 10 times", + "comment_hash": "6c917ae71cffff626b013fb1de7b5b51f39f4bebd088edddc0ec696f90ffcba0", + "body_text": "let mut labels = vec![lowest_balance.to_string()];\nfor _i in 0..10 {}\nlet mut color_list = vec![\n Color::LightRed,\n Color::LightBlue,\n Color::LightYellow,\n Color::Gray,\n Color::Black,\n Color::Yellow,\n Color::Green,\n Color::Red,\n Color::Blue,\n Color::Magenta,\n ];\nlet mut final_dataset = vec![];", + "body_hash": "2184e51c3912c6aab22e53b2f9831ae6747c1b2f3f1a9330e54a3aed42710e61" + }, + "tui/src/pages/chart_ui.rs|crate::chart_ui#free:77364f1a2420": { + "kind": "Free", + "file": "tui/src/pages/chart_ui.rs", + "item_path": "crate::chart_ui#free:77364f1a2420", + "comment_kind": "Line", + "comment_text": "// Pad the highest and lowest balance by a few % so the chart has some room", + "comment_hash": "77364f1a2420204beb2a223eff1e53ca9aeaa705ee5dbc264ec1b5c92320df27", + "body_text": "highest_balance += highest_balance * 5.0 / 100.0;\nlowest_balance -= lowest_balance * 5.0 / 100.0;\nlet diff = (highest_balance - lowest_balance) / 10.0;\nlet mut to_add = lowest_balance;", + "body_hash": "f2d764815306f0cbcc81cfed67ccf7b0064cb1561b25fcb086a68fad4c884bc7" + }, + "tui/src/pages/chart_ui.rs|crate::chart_ui#free:972196fb7069": { + "kind": "Free", + "file": "tui/src/pages/chart_ui.rs", + "item_path": "crate::chart_ui#free:972196fb7069", + "comment_kind": "Line", + "comment_text": "// Don't create any other chunk if hidden mode is enabled. Create 1 chunk that will be used for the chart itself", + "comment_hash": "972196fb70698847a3fa63a282aee2e457533a4fb709202e20d3a9b09d036b82", + "body_text": "if chart_hidden_mode {} else {}\nlet chunks = main_layout.split(size);", + "body_hash": "11129476d5fb16a559f17a22c9ec33570d35ac55e0d148a919a51da84478dc46" + }, + "tui/src/pages/chart_ui.rs|crate::chart_ui#free:a6d3d57add6a": { + "kind": "Free", + "file": "tui/src/pages/chart_ui.rs", + "item_path": "crate::chart_ui#free:a6d3d57add6a", + "comment_kind": "Line", + "comment_text": "// Total days = number of loops required to render everything", + "comment_hash": "a6d3d57add6a43e0d54cd29370df4417d7071726202db12760b26f846ce63b7d", + "body_text": "let total_loop = final_date.signed_duration_since(checking_date).num_days() as f64;\nlet lerp_id = \"chart_loop_size\";\nlet mut to_loop = lerp_state.lerp(lerp_id, total_loop, None);", + "body_hash": "1d74e0a0fe1424004a1bf776ad7a3b6baea9de0311c71e6387bd8cdc889231c7" + }, + "tui/src/pages/chart_ui.rs|crate::chart_ui#free:ae9c175a60ce": { + "kind": "Free", + "file": "tui/src/pages/chart_ui.rs", + "item_path": "crate::chart_ui#free:ae9c175a60ce", + "comment_kind": "Line", + "comment_text": "// Break the loop if total day amount is reached", + "comment_hash": "ae9c175a60cefee38aa4b8dbdf9d37511dd7a61939591b2df8cb2402087b4cef", + "body_text": "if to_loop <= 0.0 {}\nto_loop += -1.0;\n}", + "body_hash": "f55d3575af0d8e06a4b0f8063d86902d58df6a433c908d7fd440af2258c75a3d" + }, + "tui/src/pages/chart_ui.rs|crate::chart_ui#free:b4af0b743be4": { + "kind": "Free", + "file": "tui/src/pages/chart_ui.rs", + "item_path": "crate::chart_ui#free:b4af0b743be4", + "comment_kind": "Line", + "comment_text": "// The final date where the loop will stop", + "comment_hash": "b4af0b743be447bca16d62f60a56fc308e1ccdd0337ab20aed1c00dd059dc150", + "body_text": "let final_date = chart_view.end_date();", + "body_hash": "985ad9fe65736103048cb7eef361f662a69c36b795e7e2cc937200fbccdd44f8" + }, + "tui/src/pages/chart_ui.rs|crate::chart_ui#free:bec45e5dd20a": { + "kind": "Free", + "file": "tui/src/pages/chart_ui.rs", + "item_path": "crate::chart_ui#free:bec45e5dd20a", + "comment_kind": "Line", + "comment_text": "// Default next_date in case there is no more next_date", + "comment_hash": "bec45e5dd20ad529aec3c5817e233ad14719493785febd2127bd3c110bab536a", + "body_text": "let mut next_date = NaiveDate::default();\nif chart_view.len() > data_num + 1 {}", + "body_hash": "70f87b52dcbadb95ff0753d5e57dfff44c32a4864aade75132342b3f8d5230ea" + }, + "tui/src/pages/chart_ui.rs|crate::chart_ui#free:c8d6900d1398": { + "kind": "Free", + "file": "tui/src/pages/chart_ui.rs", + "item_path": "crate::chart_ui#free:c8d6900d1398", + "comment_kind": "Line", + "comment_text": "// Run out of colors = cyan default", + "comment_hash": "c8d6900d13986d796af2113e8f03d07d793f2b1328797fedba54f4dc2a3654db", + "body_text": "if color_list.is_empty() {}\nif !chart_activated_methods[all_tx_methods[i]] {}\nlet mut dataset = Dataset::default()\n .marker(Marker::Braille)\n .graph_type(GraphType::Line)\n .style(\n Style::default()\n .fg(color_list.pop().unwrap())\n .bg(theme.background()),\n )\n .data(&datasets[i]);\nif !chart_hidden_legends {}\nfinal_dataset.push(dataset);\n}", + "body_hash": "7430ec0e82431742d49a2d092f636f9c678ca9e4441e7301f305e6255c102cc7" + }, + "tui/src/pages/chart_ui.rs|crate::chart_ui#free:d1f714aafc9b": { + "kind": "Free", + "file": "tui/src/pages/chart_ui.rs", + "item_path": "crate::chart_ui#free:d1f714aafc9b", + "comment_kind": "Line", + "comment_text": "// A tx was found for this date, so the balances from the previous\n// valid date are no longer needed. A date without a tx is filled in\n// from `last_balances` instead", + "comment_hash": "d1f714aafc9b93c478d2b0c25452cc56e0f17a16c1ebc40dffbe4724f5dec1e3", + "body_text": "last_balances = Vec::new();\nlet mut cumulative_balance = 0.0;\nfor method_index in 0..all_tx_methods.len() {}\nif next_date == checking_date {} else {}", + "body_hash": "a702dce618f19adaa66cd9cce4e67e4afd06aa0cccd7fb6bce87ea6bea227250" + }, + "tui/src/pages/chart_ui.rs|crate::chart_ui#free:df2fca3f9472": { + "kind": "Free", + "file": "tui/src/pages/chart_ui.rs", + "item_path": "crate::chart_ui#free:df2fca3f9472", + "comment_kind": "Line", + "comment_text": "// No tx on this date, so repeat the last known balance as a point", + "comment_hash": "df2fca3f947255f45df28619c870037f5b32522545dd7ce2bbf45b7660583c92", + "body_text": "for method_index in 0..all_tx_methods.len() {}\ncurrent_axis += 1.0;\nchecking_date += Duration::days(1);\n}", + "body_hash": "c9c029fc1944a18426b7823641db70f23230e2c725fce6ba2a3e694fd202b8a8" + }, + "tui/src/pages/chart_ui.rs|crate::chart_ui#free:e5c0eefe3eda": { + "kind": "Free", + "file": "tui/src/pages/chart_ui.rs", + "item_path": "crate::chart_ui#free:e5c0eefe3eda", + "comment_kind": "Line", + "comment_text": "// Default initial values in case there is no data to load", + "comment_hash": "e5c0eefe3eda7cc90ecfb1a3067f67b2d541e1c55d6cac68908dcc607e746ba5", + "body_text": "if chart_view.is_empty() {}\nlet mut lowest_balance = 0.0;\nlet mut highest_balance = 0.0;\nlet mut date_labels: Vec = vec![];\nlet mut current_axis = 0.0;", + "body_hash": "73e374552fc5ee7c23eec970aaf16eaf82fbf2bafeaa6e9382454c03e881d760" + }, + "tui/src/pages/chart_ui.rs|crate::chart_ui#free:e7007f9ec13a": { + "kind": "Free", + "file": "tui/src/pages/chart_ui.rs", + "item_path": "crate::chart_ui#free:e7007f9ec13a", + "comment_kind": "Line", + "comment_text": "// Split the terminal into the vertical chunks the interface is drawn from", + "comment_hash": "e7007f9ec13a6c8b6208474e0f27a26167d9529196038e5616b5a912489b1da4", + "body_text": "let mut main_layout = Layout::default().direction(Direction::Vertical).margin(2);", + "body_hash": "05209fc6ee9105ad2be40fdf0fee59216b5149fe815beba7893f13260c88e6d4" + }, + "tui/src/pages/chart_ui.rs|crate::chart_ui#free:f8f63e9764a5": { + "kind": "Free", + "file": "tui/src/pages/chart_ui.rs", + "item_path": "crate::chart_ui#free:f8f63e9764a5", + "comment_kind": "Line", + "comment_text": "// The axis stays put while the next date is the same", + "comment_hash": "f8f63e9764a5431a95c5d497d97a03d2dbad760ce16dda8db2fd3b2b4b412670", + "body_text": "to_add_again = true;\n}", + "body_hash": "ab7141650c8375f2abc5f7ecc06705ebd28ec77c0c1bceb4411c10a3fd528255" + }, + "tui/src/pages/chart_ui.rs|crate::chart_ui#free:ff561b0d9be3": { + "kind": "Free", + "file": "tui/src/pages/chart_ui.rs", + "item_path": "crate::chart_ui#free:ff561b0d9be3", + "comment_kind": "Line", + "comment_text": "// Creates border around the entire terminal", + "comment_hash": "ff561b0d9be3224b22319c008da3b83313defef033b8d8625f678bd48234598e", + "body_text": "f.render_widget(main_block(theme), size);\nlet mut month_tab = create_tab(months, \"Months\", theme);\nlet mut year_tab = create_tab(years, \"Years\", theme);\nlet mut mode_selection_tab = create_tab(mode_selection, \"Modes\", theme);\nlet mut tx_method_selection_tab = create_tab_activation(\n chart_tx_methods,\n \"Tx Method Selection\",\n chart_activated_methods,\n theme,\n );\nlet tx_methods = conn.get_tx_methods_sorted();\nlet mut all_tx_methods: Vec<&str> = tx_methods.iter().map(|t| t.name.as_str()).collect();\nall_tx_methods.push(\"Cumulative\");", + "body_hash": "47236b7c4f4d07952c5f69251e43db9cf81da44fc95439b0cf0f3fb680705e9e" + }, + "tui/src/pages/home_ui.rs|crate::home_ui": { + "kind": "Function", + "file": "tui/src/pages/home_ui.rs", + "item_path": "crate::home_ui", + "comment_kind": "OuterDoc", + "comment_text": " The function draws the Homepage of the interface.\n", + "comment_hash": "16e5c1896fecde18d6224c6bc9003ff717ba503e198fe1d4d7968e46e662301e", + "body_text": "pub fn home_ui(\n f: &mut Frame,\n months: &IndexedData,\n years: &IndexedData,\n home_table: &mut TableData,\n current_tab: &HomeTab,\n lerp_state: &mut LerpState,\n view_group: &mut TxViewGroup,\n theme: &Theme,\n conn: &mut DbConn,\n) {\n let all_methods: Vec = conn\n .get_tx_methods_sorted()\n .iter()\n .map(|m| m.name.clone())\n .collect();\n\n let mut width_data = Vec::new();\n let total_columns = all_methods.len() + 2;\n let width_percent = (100 / total_columns) as u16;\n\n for _ in 0..total_columns {\n width_data.push(Constraint::Percentage(width_percent));\n }\n\n let size = f.area();\n\n \n let selected_style_income = Style::default()\n .fg(theme.positive())\n .add_modifier(Modifier::REVERSED);\n let selected_style_expense = Style::default()\n .fg(theme.negative())\n .add_modifier(Modifier::REVERSED);\n\n let tx_count = home_table.items.len();\n let lerp_id = \"home_tx_count\";\n let lerp_tx_count = lerp_state.lerp(lerp_id, tx_count as f64, None) as i64;\n\n let lerp_tx_row = lerp_state.lerp(HOME_TABLE_ID, tx_count as f64, Some(0.50)) as i64;\n\n let table_name = format!(\"Transactions: {lerp_tx_count}\");\n\n \n let header_cells = [\"Date\", \"Details\", \"TX Method\", \"Amount\", \"Type\", \"Tags\"]\n .iter()\n .map(|h| Cell::from(*h).style(Style::default().fg(theme.background())));\n\n let header = Row::new(header_cells)\n .style(Style::default().bg(theme.header()))\n .height(1)\n .bottom_margin(0);\n\n \n let rows = home_table\n .items\n .iter()\n .take(lerp_tx_row as usize)\n .enumerate()\n .map(|(row_index, item)| {\n let cells = item.iter().enumerate().map(|(index, c)| {\n let Ok(parsed_num) = c.parse::() else {\n return Cell::from(c.separate_with_commas());\n };\n\n let lerp_id = format!(\"home_table:{index}:{row_index}\");\n let new_c = lerp_state.lerp(&lerp_id, parsed_num, None);\n\n Cell::from(format!(\"{new_c:.2}\").separate_with_commas())\n });\n Row::new(cells)\n .height(1)\n .bottom_margin(0)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n });\n\n \n \n \n \n let chunks = Layout::default()\n .direction(Direction::Vertical)\n .margin(2)\n .constraints([\n Constraint::Length(9),\n Constraint::Length(3),\n Constraint::Length(3),\n Constraint::Min(0),\n ])\n .split(size);\n\n f.render_widget(main_block(theme), size);\n\n let mut month_tab = create_tab(months, \"Months\", theme);\n\n let mut year_tab = create_tab(years, \"Years\", theme);\n\n \n \n \n\n let mut table_area = Table::new(\n rows,\n [\n Constraint::Percentage(13),\n Constraint::Percentage(35),\n Constraint::Percentage(13),\n Constraint::Percentage(13),\n Constraint::Percentage(8),\n Constraint::Percentage(18),\n ],\n )\n .header(header)\n .block(styled_block(&table_name, theme));\n\n let balance_array = view_group\n .balance_array(home_table.state.selected(), conn)\n .unwrap();\n \n let bal_data = balance_array.into_iter().map(|item| {\n let height = 1_u16;\n\n let row_type = HomeRow::get_row(&item);\n\n let mut index = 0;\n\n let cells = item.iter().map(|c| {\n let c = if row_type != HomeRow::TopRow\n && ![\n \"Balance\",\n \"Changes\",\n \"Income\",\n \"Expense\",\n \"Daily Income\",\n \"Daily Expense\",\n ]\n .contains(&c.as_str())\n {\n \n let symbol = if c.contains('↑') || c.contains('↓') {\n c.chars().next()\n } else {\n None\n };\n\n index += 1;\n\n \n let actual_data: f64 = if row_type != HomeRow::Changes {\n c.parse().unwrap()\n } else if let Some(sym) = symbol {\n let without_symbol = c.replace(sym, \"\");\n without_symbol.parse().unwrap()\n } else {\n c.parse().unwrap()\n };\n\n let lerp_id = format!(\"{row_type}:{index}\");\n let to_show = lerp_state.lerp(&lerp_id, actual_data, None);\n\n \n \n if let Some(sym) = symbol {\n format!(\"{sym}{to_show:.2}\",).separate_with_commas()\n } else {\n format!(\"{to_show:.2}\").separate_with_commas()\n }\n } else {\n c.separate_with_commas()\n };\n\n if c.contains('↑') {\n Cell::from(c).style(Style::default().fg(theme.positive()))\n } else if c.contains('↓') {\n Cell::from(c).style(Style::default().fg(theme.negative()))\n } else if all_methods.contains(&c) || BALANCE_BOLD.contains(&c.as_str()) {\n Cell::from(c).style(Style::default().add_modifier(Modifier::BOLD))\n } else {\n Cell::from(c)\n }\n });\n Row::new(cells)\n .height(height)\n .bottom_margin(0)\n .style(Style::default().fg(theme.text()))\n });\n\n \n \n let balance_area = Table::new(bal_data, width_data.clone())\n .block(styled_block(\"Balance\", theme))\n .style(Style::default().fg(theme.border()));\n\n match current_tab {\n \n \n HomeTab::Months => month_tab = month_tab.highlight_style(tab_highlight_style(theme)),\n HomeTab::Years => year_tab = year_tab.highlight_style(tab_highlight_style(theme)),\n \n HomeTab::Table => {\n if let Some(a) = home_table.state.selected() {\n table_area = table_area.highlight_symbol(\">> \");\n\n let income_strings = [\"Income\", \"Borrow\", \"Lend Repay\"];\n let expense_strings = [\"Expense\", \"Lend\", \"Borrow Repay\"];\n\n let target_string = &home_table.items[a][4].as_str();\n\n if expense_strings.contains(target_string) {\n table_area = table_area.row_highlight_style(selected_style_expense);\n } else if income_strings.contains(target_string) {\n table_area = table_area.row_highlight_style(selected_style_income);\n } else if home_table.items[a][4] == \"Transfer\" {\n let add_modifier = theme.add_reverse_modifier();\n\n let mut style = Style::default();\n\n if add_modifier {\n style = style.fg(theme.selected()).add_modifier(Modifier::REVERSED);\n } else {\n style = style.bg(theme.selected());\n }\n\n table_area = table_area.row_highlight_style(style);\n }\n }\n }\n }\n\n \n if let Some(index) = home_table.state.selected()\n && index > 10\n {\n *home_table.state.offset_mut() = index - 10;\n }\n\n \n \n f.render_widget(balance_area, chunks[0]);\n f.render_widget(month_tab, chunks[2]);\n f.render_widget(year_tab, chunks[1]);\n\n \n f.render_stateful_widget(table_area, chunks[3], &mut home_table.state);\n}", + "body_hash": "09a24e50ddd491248f8aa8bf401b1449722ccacc4f661f8178a329129f14f99f" + }, + "tui/src/pages/home_ui.rs|crate::home_ui#free:041abc10d72c": { + "kind": "Free", + "file": "tui/src/pages/home_ui.rs", + "item_path": "crate::home_ui#free:041abc10d72c", + "comment_kind": "Line", + "comment_text": "// Use the acquired width data to the allocated spaces\n// between columns on Balance widget.", + "comment_hash": "041abc10d72cd9130c87cb15d3002f75ac876994ef7777f4ed079039b2f07f21", + "body_text": "let balance_area = Table::new(bal_data, width_data.clone())\n .block(styled_block(\"Balance\", theme))\n .style(Style::default().fg(theme.border()));\nmatch current_tab {\n \n \n HomeTab::Months => month_tab = month_tab.highlight_style(tab_highlight_style(theme)),\n HomeTab::Years => year_tab = year_tab.highlight_style(tab_highlight_style(theme)),\n \n HomeTab::Table => {}\n }", + "body_hash": "312f8e655c02b53290dfb0b92bfdde0d4646a665ac4cabc17ef066e1a90782a8" + }, + "tui/src/pages/home_ui.rs|crate::home_ui#free:08307f45d3a7": { + "kind": "Free", + "file": "tui/src/pages/home_ui.rs", + "item_path": "crate::home_ui#free:08307f45d3a7", + "comment_kind": "Line", + "comment_text": "// Transaction widget top row/header to highlight what each data will mean", + "comment_hash": "08307f45d3a7ce475942395f350aa677f2ea1cc9f9216fa1d134692d0b0880bb", + "body_text": "let header_cells = [\"Date\", \"Details\", \"TX Method\", \"Amount\", \"Type\", \"Tags\"]\n .iter()\n .map(|h| Cell::from(*h).style(Style::default().fg(theme.background())));\nlet header = Row::new(header_cells)\n .style(Style::default().bg(theme.header()))\n .height(1)\n .bottom_margin(0);", + "body_hash": "7156b061bf28eafbcd792a72dcb33288e1cc9696f795c686be55a492b60b386b" + }, + "tui/src/pages/home_ui.rs|crate::home_ui#free:0bc5cdbabec4": { + "kind": "Free", + "file": "tui/src/pages/home_ui.rs", + "item_path": "crate::home_ui#free:0bc5cdbabec4", + "comment_kind": "Line", + "comment_text": "// Used to highlight Changes on Balance section of Home Page", + "comment_hash": "0bc5cdbabec43c5636a4f15835d16929e355d5f4f49c3652f022bf2a78665a42", + "body_text": "let selected_style_income = Style::default()\n .fg(theme.positive())\n .add_modifier(Modifier::REVERSED);\nlet selected_style_expense = Style::default()\n .fg(theme.negative())\n .add_modifier(Modifier::REVERSED);\nlet tx_count = home_table.items.len();\nlet lerp_id = \"home_tx_count\";\nlet lerp_tx_count = lerp_state.lerp(lerp_id, tx_count as f64, None) as i64;\nlet lerp_tx_row = lerp_state.lerp(HOME_TABLE_ID, tx_count as f64, Some(0.50)) as i64;\nlet table_name = format!(\"Transactions: {lerp_tx_count}\");", + "body_hash": "e711e85c281ba6b2e1aca2a2625686c0cab77b60169733c0214f8ccd8cff4102" + }, + "tui/src/pages/home_ui.rs|crate::home_ui#free:37e26499b6b0": { + "kind": "Free", + "file": "tui/src/pages/home_ui.rs", + "item_path": "crate::home_ui#free:37e26499b6b0", + "comment_kind": "Line", + "comment_text": "// Put the symbol back on the Changes row, otherwise the number\n// gets thousands separators", + "comment_hash": "37e26499b6b07f0ceaa3b7856a3c7f335898ba6100b7126e8aebed967112db4b", + "body_text": "if let Some(sym) = symbol {} else {}\n}", + "body_hash": "d8f6d3a68dfe79946d3b6de5571be870184a9a83258751ec3834e12c829edd39" + }, + "tui/src/pages/home_ui.rs|crate::home_ui#free:3e380bb21330": { + "kind": "Free", + "file": "tui/src/pages/home_ui.rs", + "item_path": "crate::home_ui#free:3e380bb21330", + "comment_kind": "Line", + "comment_text": "// set up the table columns and their size\n// resizing the table headers to match an % of the\n// terminal space", + "comment_hash": "3e380bb21330e0509edd04ea896fff2a47443dfb8e68a580ec0c7f21722caacf", + "body_text": "let mut table_area = Table::new(\n rows,\n [\n Constraint::Percentage(13),\n Constraint::Percentage(35),\n Constraint::Percentage(13),\n Constraint::Percentage(13),\n Constraint::Percentage(8),\n Constraint::Percentage(18),\n ],\n )\n .header(header)\n .block(styled_block(&table_name, theme));\nlet balance_array = view_group\n .balance_array(home_table.state.selected(), conn)\n .unwrap();", + "body_hash": "41bd94ebb3c60a00df5589cc638d65b58cdf1d905eafcceb45324ced02705866" + }, + "tui/src/pages/home_ui.rs|crate::home_ui#free:896748a72625": { + "kind": "Free", + "file": "tui/src/pages/home_ui.rs", + "item_path": "crate::home_ui#free:896748a72625", + "comment_kind": "Line", + "comment_text": "// - The Balance tab\n// - The year tab\n// - The month tab\n// - The transaction list/Table", + "comment_hash": "896748a7262559cfd130cf8ca61fac60a6127812c477d9a250124d79ec397a29", + "body_text": "let chunks = Layout::default()\n .direction(Direction::Vertical)\n .margin(2)\n .constraints([\n Constraint::Length(9),\n Constraint::Length(3),\n Constraint::Length(3),\n Constraint::Min(0),\n ])\n .split(size);\nf.render_widget(main_block(theme), size);\nlet mut month_tab = create_tab(months, \"Months\", theme);\nlet mut year_tab = create_tab(years, \"Years\", theme);", + "body_hash": "67a1b36d0ad4850508724e3c0f2ede20ef5f1bbb35b9d451dd4bc449441f9e21" + }, + "tui/src/pages/home_ui.rs|crate::home_ui#free:b801cf2e87a3": { + "kind": "Free", + "file": "tui/src/pages/home_ui.rs", + "item_path": "crate::home_ui#free:b801cf2e87a3", + "comment_kind": "Line", + "comment_text": "// Now that every widget holds its data, render them one by one into the\n// chunks laid out above", + "comment_hash": "b801cf2e87a3e7db4f3f8076dff7c42da3ce42a201aae67a1a46877aebf2781d", + "body_text": "f.render_widget(balance_area, chunks[0]);\nf.render_widget(month_tab, chunks[2]);\nf.render_widget(year_tab, chunks[1]);", + "body_hash": "3fa9be1267b2fcb97aff888fe518da2c980b0262469599847fefdc12e4202f36" + }, + "tui/src/pages/home_ui.rs|crate::home_ui#free:c15ef47bb7a6": { + "kind": "Free", + "file": "tui/src/pages/home_ui.rs", + "item_path": "crate::home_ui#free:c15ef47bb7a6", + "comment_kind": "Line", + "comment_text": "// If loading was complete then this value is to be shown", + "comment_hash": "c15ef47bb7a687d69fdd18c88a1f707bcb57973c4862845646c66db742091761", + "body_text": "let actual_data: f64 = if row_type != HomeRow::Changes {} else if let Some(sym) = symbol {} else {};\nlet lerp_id = format!(\"{row_type}:{index}\");\nlet to_show = lerp_state.lerp(&lerp_id, actual_data, None);", + "body_hash": "b33860f90e5d2176a15e85920f7819b14b05352fb7a026e9ae19f5f2bd86a124" + }, + "tui/src/pages/home_ui.rs|crate::home_ui#free:dddf92343493": { + "kind": "Free", + "file": "tui/src/pages/home_ui.rs", + "item_path": "crate::home_ui#free:dddf92343493", + "comment_kind": "Line", + "comment_text": "// Go through all data of the Balance widget and style it as necessary", + "comment_hash": "dddf92343493891577e78074b564d550d6c5e9d1d6ebc7e1f4867c132dff7c94", + "body_text": "let bal_data = balance_array.into_iter().map(|item| {});", + "body_hash": "8b8328b797bb825215e57cdaba918804aa6f2b5913346fdb7f31ddbb5b0eb928" + }, + "tui/src/pages/home_ui.rs|crate::home_ui#free:df80b7fc8997": { + "kind": "Free", + "file": "tui/src/pages/home_ui.rs", + "item_path": "crate::home_ui#free:df80b7fc8997", + "comment_kind": "Line", + "comment_text": "// Iter through table data and turn them into rows and columns", + "comment_hash": "df80b7fc89977d974135583633f71a1b0888d340f4921cec9bba2d7a0cfe4e82", + "body_text": "let rows = home_table\n .items\n .iter()\n .take(lerp_tx_row as usize)\n .enumerate()\n .map(|(row_index, item)| {});", + "body_hash": "724597e773c2352456cb59b57d1c686ea7ab897800047ce71934b9078c2079c2" + }, + "tui/src/pages/home_ui.rs|crate::home_ui#free:ed98a63512e5": { + "kind": "Free", + "file": "tui/src/pages/home_ui.rs", + "item_path": "crate::home_ui#free:ed98a63512e5", + "comment_kind": "Line", + "comment_text": "// Changes row can contain arrow symbols", + "comment_hash": "ed98a63512e55d3edabf0fff7754033ff310882cf5f07260d73c736b77407047", + "body_text": "let symbol = if c.contains('↑') || c.contains('↓') {} else {};\nindex += 1;", + "body_hash": "d808b8c5f50fcde387ba69e15805c657d0c227296be44dc19ce62fec20fc0e54" + }, + "tui/src/pages/home_ui.rs|crate::home_ui#free:f4dbdead76d3": { + "kind": "Free", + "file": "tui/src/pages/home_ui.rs", + "item_path": "crate::home_ui#free:f4dbdead76d3", + "comment_kind": "Line", + "comment_text": "// Always keep some items rendered on the upper side of the table", + "comment_hash": "f4dbdead76d3251e69e582a35158024cff1ec2ccd6a2f1048ad198c56055cd33", + "body_text": "if let Some(index) = home_table.state.selected()\n && index > 10\n {}", + "body_hash": "aca88a97826831e56451d4eda14285e76d2e0c3d0cd58542c71530d72108be6c" + }, + "tui/src/pages/home_ui.rs|crate::home_ui#free:fd5e7524c6b6": { + "kind": "Free", + "file": "tui/src/pages/home_ui.rs", + "item_path": "crate::home_ui#free:fd5e7524c6b6", + "comment_kind": "Line", + "comment_text": "// The table is the only stateful widget, as its selection has to be tracked", + "comment_hash": "fd5e7524c6b6fb7c4d5bcee65617a8f8f54fac298cc547b45420190f65c56ebd", + "body_text": "f.render_stateful_widget(table_area, chunks[3], &mut home_table.state);\n}", + "body_hash": "101bd4b24dfa5d6d92e234546632ae0b9bf5f1d358bd7957437eb263c2e0e50c" + }, + "tui/src/pages/initial_ui.rs|crate::InitialData": { + "kind": "Struct", + "file": "tui/src/pages/initial_ui.rs", + "item_path": "crate::InitialData", + "comment_kind": "OuterDoc", + "comment_text": " Everything the startup screen shows besides the static key reference. Loaded once\n\n before the UI loop starts since the page is redrawn every few milliseconds.\n", + "comment_hash": "8eacd1d60d71531711a2747eacfa30ed05097cc91ef2e82f0402b180254d4dac", + "body_text": "pub struct InitialData {\n pub recurring_added: usize,\n pub balances: Vec<(String, String, bool)>,\n pub total: Option<(String, bool)>,\n pub upcoming: Vec,\n pub paused_count: usize,\n}", + "body_hash": "7006e20b575dd27b1d2db3d0952cd3b7e8165b29832621a65cd1a67d430287f2" + }, + "tui/src/pages/initial_ui.rs|crate::InitialData::balances": { + "kind": "Field", + "file": "tui/src/pages/initial_ui.rs", + "item_path": "crate::InitialData::balances", + "comment_kind": "OuterDoc", + "comment_text": " Method name, formatted balance, whether the balance is negative\n", + "comment_hash": "6bee1e88b76af5c362f13e4b8be3046cdbcfc3908e2b2a7044dd510852992641", + "body_text": "pub balances: Vec<(String, String, bool)>", + "body_hash": "d769118d6bf0d506193771a50793f9d2d070f67a0a5a69ce4f35e4ce058dcc5c" + }, + "tui/src/pages/initial_ui.rs|crate::UPCOMING_DAYS": { + "kind": "Const", + "file": "tui/src/pages/initial_ui.rs", + "item_path": "crate::UPCOMING_DAYS", + "comment_kind": "OuterDoc", + "comment_text": " How many days ahead the startup screen looks for upcoming recurring transactions\n", + "comment_hash": "2781248b0446f8096e9acea6afd9147e0b022212b1e81da7e2839709ab700042", + "body_text": "const UPCOMING_DAYS: u64 = 7;", + "body_hash": "8c62eec760dc1dfa945ea2747bb791742ba9989a864e2c3eb94d6c59f846b827" + }, + "tui/src/pages/initial_ui.rs|crate::UPCOMING_MAX_ROWS": { + "kind": "Const", + "file": "tui/src/pages/initial_ui.rs", + "item_path": "crate::UPCOMING_MAX_ROWS", + "comment_kind": "OuterDoc", + "comment_text": " The most upcoming recurring transactions listed on the startup screen\n", + "comment_hash": "ddb9a58140db42649728107c86309d88b8e9e466e5ef0006eaefe353db2201ac", + "body_text": "const UPCOMING_MAX_ROWS: usize = 10;", + "body_hash": "975ef5e1722ec8765936af7e9971cbc55375eadb3f22f2f963f9493569ca4d64" + }, + "tui/src/pages/initial_ui.rs|crate::initial_ui": { + "kind": "Function", + "file": "tui/src/pages/initial_ui.rs", + "item_path": "crate::initial_ui", + "comment_kind": "OuterDoc", + "comment_text": " The function draws the Initial page of the interface.\n", + "comment_hash": "dbd3b49a0743c4242cc5e00d79ad0e1b541ccd4dc58237a55b630833beb28fae", + "body_text": "pub fn initial_ui(f: &mut Frame, start_from: usize, data: &InitialData, theme: &Theme) {\n let size = f.area();\n let chunks = Layout::default()\n .direction(Direction::Vertical)\n .margin(2)\n .constraints([\n Constraint::Length(8),\n Constraint::Length(1),\n Constraint::Min(5),\n ])\n .split(size);\n\n let columns = Layout::default()\n .direction(Direction::Horizontal)\n .constraints([Constraint::Percentage(55), Constraint::Percentage(45)])\n .split(chunks[2]);\n\n let left_column = Layout::default()\n .direction(Direction::Vertical)\n .constraints([Constraint::Percentage(40), Constraint::Percentage(60)])\n .split(columns[0]);\n\n let right_column = Layout::default()\n .direction(Direction::Vertical)\n .constraints([Constraint::Length(11), Constraint::Min(0)])\n .split(columns[1]);\n\n f.render_widget(main_block(theme), size);\n\n \n let text = r\" _____ ______ __ __\n | __ \\ | ____| \\ \\ / /\n | |__) | | |__ \\ V / \n | _ / | __| > < \n | | \\ \\ | |____ / . \\ \n |_| \\_\\ |______| /_/ \\_\\\"\n .to_string();\n\n \n \n \n let split_text = text.split('\\n').collect::>();\n let mut upper_text = String::new();\n\n for line in split_text {\n \n \n let mut to_add_from_start = 0;\n \n let mut total_to_add = 10;\n\n if start_from + total_to_add > line.len() {\n let extra_index = (start_from + total_to_add) - line.len();\n \n \n total_to_add -= extra_index;\n to_add_from_start += extra_index;\n }\n\n \n for (index, char) in line.chars().enumerate() {\n if to_add_from_start != 0 {\n \n upper_text.push(char);\n to_add_from_start -= 1;\n } else if total_to_add != 0 && index >= start_from {\n \n upper_text.push(char);\n total_to_add -= 1;\n } else if index != start_from || total_to_add == 0 {\n \n upper_text.push(' ');\n }\n }\n upper_text.push('\\n');\n }\n\n let unmodified_first_help = format!(\n \"{F}\n{A}\n{R}\n{Z}\n{Y}\n{W}\n{U}\n{J}\n{Q}\"\n );\n\n let unmodified_second_help = \"H: Show the keys of the page you are currently on\nArrow Up/Down: Cycle between widgets\nArrow Left/Right: Cycle values of a widget\nNumber keys: Jump to a field on Add Transaction/Search/Recurring pages\nT: Cycle through themes\nX: Change sort type on Summary/Change date type on Search page\nDouble r/R/Z: Hide chart top widgets/legends or summary top widget\";\n\n \n let first_text = create_bolded_text(&unmodified_first_help);\n let second_text = create_bolded_text(unmodified_second_help);\n\n let middle_text = \"Press Any Key To Continue\";\n\n let paragraph = Paragraph::new(upper_text)\n .style(\n Style::default()\n .bg(theme.background())\n .fg(theme.text())\n .add_modifier(Modifier::BOLD),\n )\n .alignment(Alignment::Center);\n\n let paragraph_2 = Paragraph::new(middle_text)\n .style(\n Style::default()\n .bg(theme.background())\n .fg(theme.negative())\n .add_modifier(Modifier::BOLD),\n )\n .alignment(Alignment::Center);\n\n let help_1 = Paragraph::new(first_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(\"Page Keys\", theme))\n .wrap(Wrap { trim: true });\n\n let help_2 = Paragraph::new(second_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(\"Other Keys\", theme))\n .wrap(Wrap { trim: true });\n\n f.render_widget(paragraph, chunks[0]);\n f.render_widget(paragraph_2, chunks[1]);\n render_balances(f, left_column[0], data, theme);\n render_recurring(f, left_column[1], data, theme);\n f.render_widget(help_1, right_column[0]);\n f.render_widget(help_2, right_column[1]);\n}", + "body_hash": "3571bd7636e662859b558e0a0184cf0d91c888ba01902994f9840229c81cd62a" + }, + "tui/src/pages/initial_ui.rs|crate::initial_ui#free:0e63b004325b": { + "kind": "Free", + "file": "tui/src/pages/initial_ui.rs", + "item_path": "crate::initial_ui#free:0e63b004325b", + "comment_kind": "Line", + "comment_text": "// Wrapping around, so keep the chars until the count runs out", + "comment_hash": "0e63b004325b21fd180fdb4edce3b4405aed87e975e7fab33653721ef79e485e", + "body_text": "upper_text.push(char);\nto_add_from_start -= 1;\n}", + "body_hash": "02bf5b211065c8e58b248c5af147e7e41a8ef1295077826d7f4b61d1c81ce086" + }, + "tui/src/pages/initial_ui.rs|crate::initial_ui#free:2de85a101557": { + "kind": "Free", + "file": "tui/src/pages/initial_ui.rs", + "item_path": "crate::initial_ui#free:2de85a101557", + "comment_kind": "Line", + "comment_text": "// Everything past the 10 char limit stays blank", + "comment_hash": "2de85a10155721da42c55d67d0da8a2c353ed6ba377d185090981833facd19ea", + "body_text": "upper_text.push(' ');\n}", + "body_hash": "a80e4543e4c9d55b258ba66c5175348e8de984e3b3a0713c6a97d9920819c51f" + }, + "tui/src/pages/initial_ui.rs|crate::initial_ui#free:35efba7f7b27": { + "kind": "Free", + "file": "tui/src/pages/initial_ui.rs", + "item_path": "crate::initial_ui#free:35efba7f7b27", + "comment_kind": "Line", + "comment_text": "// Past the start point and the limit isn't reached yet", + "comment_hash": "35efba7f7b27394dfc300b3c664e4d1ddb19ac9d7a1b4ae858f78d8d0ee0b8b0", + "body_text": "upper_text.push(char);\ntotal_to_add -= 1;\n}", + "body_hash": "d27e48b33600aabe2e69529cd035d10133e809b735fd35e0d60593f489b7defd" + }, + "tui/src/pages/initial_ui.rs|crate::initial_ui#free:4eb5ec6f5086": { + "kind": "Free", + "file": "tui/src/pages/initial_ui.rs", + "item_path": "crate::initial_ui#free:4eb5ec6f5086", + "comment_kind": "Line", + "comment_text": "// Take that many chars from the start instead, since taking them from\n// the starting point would go out of bounds", + "comment_hash": "4eb5ec6f5086ab38d6f9df1e54902befee8cb25814f2a94fc6c03ca9cb0d877e", + "body_text": "total_to_add -= extra_index;\nto_add_from_start += extra_index;\n}", + "body_hash": "35914e0bcdf731d83c497aba1983650e519ecde1e08e2958033eac66450aeaa5" + }, + "tui/src/pages/initial_ui.rs|crate::initial_ui#free:5d9c6d113826": { + "kind": "Free", + "file": "tui/src/pages/initial_ui.rs", + "item_path": "crate::initial_ui#free:5d9c6d113826", + "comment_kind": "Line", + "comment_text": "// A line of 20 chars at index 15 takes chars 15-20 and then 0-3\n// This var stores how many to take from 0 index", + "comment_hash": "5d9c6d113826410efb7a57413fa66781ff835dce05dc3491123dcd210b7dd971", + "body_text": "let mut to_add_from_start = 0;", + "body_hash": "44606e5f8f6bd04663a34c776422fb00d66cdb491ceb4e3d8d0c1d3ccd419bf8" + }, + "tui/src/pages/initial_ui.rs|crate::initial_ui#free:6382d873c8d1": { + "kind": "Free", + "file": "tui/src/pages/initial_ui.rs", + "item_path": "crate::initial_ui#free:6382d873c8d1", + "comment_kind": "Line", + "comment_text": "// This is the text that is shown in the startup which is the project's name in ASCII format.", + "comment_hash": "6382d873c8d1329308f703068e6411e6fef4f5f1d2b5c89bae729ecaaac90095", + "body_text": "let text = r\" _____ ______ __ __\n | __ \\ | ____| \\ \\ / /\n | |__) | | |__ \\ V / \n | _ / | __| > < \n | | \\ \\ | |____ / . \\ \n |_| \\_\\ |______| /_/ \\_\\\"\n .to_string();", + "body_hash": "72a5970699b17244655f4de3b2b97d67606bb30edd1adf7957664cf40e782c9f" + }, + "tui/src/pages/initial_ui.rs|crate::initial_ui#free:a28c6b792f85": { + "kind": "Free", + "file": "tui/src/pages/initial_ui.rs", + "item_path": "crate::initial_ui#free:a28c6b792f85", + "comment_kind": "Line", + "comment_text": "// Go through each char of the line", + "comment_hash": "a28c6b792f85d57bf34e9b9d60f19ad04a9514ae03964fcf3d76798968155f03", + "body_text": "for (index, char) in line.chars().enumerate() {}\nupper_text.push('\\n');\n}", + "body_hash": "7639d03e5dd254b41be35c2c1ff790c9fea37f129506b4429e75c1d148275227" + }, + "tui/src/pages/initial_ui.rs|crate::initial_ui#free:c0888c2ea5af": { + "kind": "Free", + "file": "tui/src/pages/initial_ui.rs", + "item_path": "crate::initial_ui#free:c0888c2ea5af", + "comment_kind": "Line", + "comment_text": "// amount of chars per line", + "comment_hash": "c0888c2ea5af5e14b6825aa9d2519c227bf835acc9cd0352cce06ddc8962258d", + "body_text": "let mut total_to_add = 10;\nif start_from + total_to_add > line.len() {}", + "body_hash": "aa8b04bbff2b40d3b7ef9fbed4ae45baa3ec4f8a813ff3804864ceb2f7bb9246" + }, + "tui/src/pages/initial_ui.rs|crate::initial_ui#free:eb2db1eb9a92": { + "kind": "Free", + "file": "tui/src/pages/initial_ui.rs", + "item_path": "crate::initial_ui#free:eb2db1eb9a92", + "comment_kind": "Line", + "comment_text": "// To animate it, the text is split by \\n and each line reveals `total_to_add`\n// chars starting at `start_from`, wrapping around to the beginning of the line\n// when the end is reached", + "comment_hash": "eb2db1eb9a92368791f732d700f3d327875a51cf6acd2d26af885279c8b6b222", + "body_text": "let split_text = text.split('\\n').collect::>();\nlet mut upper_text = String::new();\nfor line in split_text {}\nlet unmodified_first_help = format!(\n \"{F}\n{A}\n{R}\n{Z}\n{Y}\n{W}\n{U}\n{J}\n{Q}\"\n );\nlet unmodified_second_help = \"H: Show the keys of the page you are currently on\nArrow Up/Down: Cycle between widgets\nArrow Left/Right: Cycle values of a widget\nNumber keys: Jump to a field on Add Transaction/Search/Recurring pages\nT: Cycle through themes\nX: Change sort type on Summary/Change date type on Search page\nDouble r/R/Z: Hide chart top widgets/legends or summary top widget\";", + "body_hash": "d96a1c3df6cfdac54eda266c212e50d982a9429e32ec2a8eabfe048c0c3a8647" + }, + "tui/src/pages/initial_ui.rs|crate::initial_ui#free:fd264be9f0a3": { + "kind": "Free", + "file": "tui/src/pages/initial_ui.rs", + "item_path": "crate::initial_ui#free:fd264be9f0a3", + "comment_kind": "Line", + "comment_text": "// create_bolded_text does the bolding while rendering", + "comment_hash": "fd264be9f0a323a2232be65eb235c436b9c1d50f1102f0d05360e8a843facc4d", + "body_text": "let first_text = create_bolded_text(&unmodified_first_help);\nlet second_text = create_bolded_text(unmodified_second_help);\nlet middle_text = \"Press Any Key To Continue\";\nlet paragraph = Paragraph::new(upper_text)\n .style(\n Style::default()\n .bg(theme.background())\n .fg(theme.text())\n .add_modifier(Modifier::BOLD),\n )\n .alignment(Alignment::Center);\nlet paragraph_2 = Paragraph::new(middle_text)\n .style(\n Style::default()\n .bg(theme.background())\n .fg(theme.negative())\n .add_modifier(Modifier::BOLD),\n )\n .alignment(Alignment::Center);\nlet help_1 = Paragraph::new(first_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(\"Page Keys\", theme))\n .wrap(Wrap { trim: true });\nlet help_2 = Paragraph::new(second_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(\"Other Keys\", theme))\n .wrap(Wrap { trim: true });\nf.render_widget(paragraph, chunks[0]);\nf.render_widget(paragraph_2, chunks[1]);\nrender_balances(f, left_column[0], data, theme);\nrender_recurring(f, left_column[1], data, theme);\nf.render_widget(help_1, right_column[0]);\nf.render_widget(help_2, right_column[1]);\n}", + "body_hash": "0a9d11ab8f6f5dc176736d9ebc5d837a33d93fee896e893f413c9bea560d8376" + }, + "tui/src/pages/initial_ui.rs|crate::is_upcoming": { + "kind": "Function", + "file": "tui/src/pages/initial_ui.rs", + "item_path": "crate::is_upcoming", + "comment_kind": "OuterDoc", + "comment_text": " Whether a recurring rule's next occurrence falls within the next `days` days and will\n\n actually fire (not paused, not past its end date)\n", + "comment_hash": "3390bdd285a61b0452b58adb228dcb4a810c3684345c82267870c13ad130b39b", + "body_text": "fn is_upcoming(\n next: NaiveDate,\n end_date: Option,\n is_paused: bool,\n today: NaiveDate,\n days: u64,\n) -> bool {\n if is_paused || end_date.is_some_and(|end| next > end) {\n return false;\n }\n\n next >= today && next <= today + Days::new(days)\n}", + "body_hash": "47355efd20ebd57a27eb08d5d802212e0b970fd972e11a9a3ba64ee286ed36d0" + }, + "tui/src/pages/initial_ui.rs|crate::row_style": { + "kind": "Function", + "file": "tui/src/pages/initial_ui.rs", + "item_path": "crate::row_style", + "comment_kind": "OuterDoc", + "comment_text": " Tables inherit their block's border-coloured foreground for unstyled cells, so every row\n\n gets the theme's text colour explicitly, like the other pages do.\n", + "comment_hash": "35ceb2aab03fa87dfc2d5b725e61bfc30aa6daf2f6c14527766f727ffc1c54a8", + "body_text": "fn row_style(theme: &Theme) -> Style {\n Style::default().bg(theme.background()).fg(theme.text())\n}", + "body_hash": "41ef0294fe87b7f07d95c588155041ca18312f4f7205ce3d164cdd83c9a5becc" + }, + "tui/src/pages/recurring_ui.rs|crate::recurring_ui#free:092b6e90c7f6": { + "kind": "Free", + "file": "tui/src/pages/recurring_ui.rs", + "item_path": "crate::recurring_ui#free:092b6e90c7f6", + "comment_kind": "Line", + "comment_text": "// Same field set/order as the Add Transaction page, plus Tags", + "comment_hash": "092b6e90c7f64d126f240e58f5c8e7315c78853a396e0e242073329b837d4b7a", + "body_text": "let core_chunk = match tx_type {\n TxType::IncomeExpense => Layout::default()\n .direction(Direction::Horizontal)\n .constraints([\n Constraint::Percentage(20),\n Constraint::Percentage(20),\n Constraint::Percentage(20),\n Constraint::Percentage(20),\n Constraint::Percentage(20),\n ])\n .split(chunks[0]),\n TxType::Transfer => Layout::default()\n .direction(Direction::Horizontal)\n .constraints([\n Constraint::Percentage(16),\n Constraint::Percentage(16),\n Constraint::Percentage(16),\n Constraint::Percentage(16),\n Constraint::Percentage(16),\n Constraint::Percentage(20),\n ])\n .split(chunks[0]),\n };", + "body_hash": "8bbfd3f385d9c415d8bd5ddafdf530d231c055645ceb1409959867012718eafd" + }, + "tui/src/pages/recurring_ui.rs|crate::recurring_ui#free:316758805782": { + "kind": "Free", + "file": "tui/src/pages/recurring_ui.rs", + "item_path": "crate::recurring_ui#free:316758805782", + "comment_kind": "Line", + "comment_text": "// Divide the terminal into 5 parts vertically", + "comment_hash": "316758805782464b37baa689cfbcfe2dc33e041b924a787a2e8679315a9e99b6", + "body_text": "let chunks = Layout::default()\n .direction(Direction::Vertical)\n .margin(2)\n .constraints([\n \n Constraint::Length(3),\n \n Constraint::Length(3),\n \n Constraint::Length(3),\n \n Constraint::Length(10),\n \n Constraint::Min(0),\n ])\n .split(size);", + "body_hash": "5e39b183ab1a1df7f8815b0e961b67326ecbd08a956b0b51d910e2d1aec80d20" + }, + "tui/src/pages/recurring_ui.rs|crate::recurring_ui#free:7daf9b3f5456": { + "kind": "Free", + "file": "tui/src/pages/recurring_ui.rs", + "item_path": "crate::recurring_ui#free:7daf9b3f5456", + "comment_kind": "Line", + "comment_text": "// The index of the cursor position", + "comment_hash": "7daf9b3f545601e8ed718e936c10e7ad4637a5471cc11af2b0861fd68d1afc6b", + "body_text": "let current_index = recurring_data.get_current_index();\nlet tx_type = recurring_data.get_tx_type();\nlet frequency = recurring_data.get_frequency();\nlet size = f.area();\nlet from_method_name = match tx_type {\n TxType::IncomeExpense => \"TX Method\",\n TxType::Transfer => \"From Method\",\n };\nlet tx_count = recurring_table.items.len();\nlet lerp_id = \"recurring_count\";\nlet lerp_tx_count = lerp_state.lerp(lerp_id, tx_count as f64, None) as i64;\nlet lerp_row = lerp_state.lerp(RECURRING_TABLE_ID, tx_count as f64, Some(0.50)) as usize;\nlet table_name = format!(\"Recurring Transactions: {lerp_tx_count}\");\nlet header_cells = [\n \"Next Date\",\n \"Frequency\",\n \"Details\",\n \"Method\",\n \"Amount\",\n \"Type\",\n \"Tags\",\n \"Paused\",\n ]\n .iter()\n .map(|h| Cell::from(*h).style(Style::default().fg(theme.background())));\nlet header = Row::new(header_cells)\n .style(Style::default().bg(theme.header()))\n .height(1)\n .bottom_margin(0);\nlet rows = recurring_table.items.iter().take(lerp_row).map(|item| {});\nlet mut table_area = Table::new(\n rows,\n [\n Constraint::Percentage(12),\n Constraint::Percentage(13),\n Constraint::Percentage(25),\n Constraint::Percentage(13),\n Constraint::Percentage(11),\n Constraint::Percentage(8),\n Constraint::Percentage(11),\n Constraint::Percentage(7),\n ],\n )\n .header(header)\n .block(styled_block(&table_name, theme));", + "body_hash": "44d4d8f170b966156fb9987b4b45ec49086e4834773d6315de230dbac49155b1" + }, + "tui/src/pages/recurring_ui.rs|crate::recurring_ui#free:8a18521a9ae1": { + "kind": "Free", + "file": "tui/src/pages/recurring_ui.rs", + "item_path": "crate::recurring_ui#free:8a18521a9ae1", + "comment_kind": "Line", + "comment_text": "// date, details, from method, to method, amount, tx type, tags, autofill", + "comment_hash": "8a18521a9ae1f96492067e3a86b4a4dbea5bbb34984ad0ce8942497c8121ef61", + "body_text": "let input_data = recurring_data.get_all_texts();", + "body_hash": "3264ac00c46008b97bdfb0fb5fed2baedaf6cca781291a10bd9d24a90fa6123e" + }, + "tui/src/pages/recurring_ui.rs|crate::recurring_ui#free:9bed407148bb": { + "kind": "Free", + "file": "tui/src/pages/recurring_ui.rs", + "item_path": "crate::recurring_ui#free:9bed407148bb", + "comment_kind": "Line", + "comment_text": "// frequency, recur interval, recur value, recur month, end date", + "comment_hash": "9bed407148bb9ac93253218a5d5eb3ce0fd087012fe019b1eb13ce735960e96c", + "body_text": "let recur_data = recurring_data.get_recurring_texts();", + "body_hash": "789d57372d4e399a6cd4389fb597e2f51c72f4e13fed5a1ed94103569b863118" + }, + "tui/src/pages/recurring_ui.rs|crate::recurring_ui#free:a1c963af5713": { + "kind": "Free", + "file": "tui/src/pages/recurring_ui.rs", + "item_path": "crate::recurring_ui#free:a1c963af5713", + "comment_kind": "Line", + "comment_text": "// Number of recurrence boxes to render: frequency and interval are always\n// shown, then the frequency decides whether a value, a month and the end date\n// follow", + "comment_hash": "a1c963af5713f113e77a2d94d8167dc46ec105263c4e425315e3ab456f45fcbe", + "body_text": "let recur_box_count = match frequency {\n RecurrenceFrequency::Daily => 3,\n RecurrenceFrequency::Weekly | RecurrenceFrequency::Monthly => 4,\n RecurrenceFrequency::Yearly => 5,\n };\nlet recur_percent = 100 / recur_box_count;\nlet recur_chunk = Layout::default()\n .direction(Direction::Horizontal)\n .constraints(vec![\n Constraint::Percentage(recur_percent);\n recur_box_count as usize\n ])\n .split(chunks[1]);\nf.render_widget(main_block(theme), size);\nlet mut status_text = vec![];\nfor i in status_data.iter().rev() {}\nlet date_text = Line::from(format!(\"{} \", input_data[0]));\nlet mut details_text = Line::from(format!(\"{} \", input_data[1]));\nlet mut from_method_text = Line::from(format!(\"{} \", input_data[2]));\nlet mut to_method_text = Line::from(format!(\"{} \", input_data[3]));\nlet amount_text = Line::from(format!(\"{} \", input_data[4]));\nlet mut tx_type_text = Line::from(format!(\"{} \", input_data[5]));\nlet mut tags_text = Line::from(format!(\"{} \", input_data[6]));\nlet mut frequency_text = Line::from(format!(\"{} \", recur_data[0]));\nlet recur_interval_text = Line::from(format!(\"{} \", recur_data[1]));\nlet mut recur_value_text = Line::from(format!(\"{} \", recur_data[2]));\nlet mut recur_month_text = Line::from(format!(\"{} \", recur_data[3]));\nlet end_date_text = Line::from(format!(\"{} \", recur_data[4]));\nmatch recurring_tab {\n TxTab::Details => {}\n TxTab::FromMethod => {}\n TxTab::ToMethod => {}\n TxTab::Tags => {}\n TxTab::Frequency => {}\n TxTab::RecurValue => {}\n TxTab::RecurMonth => {}\n TxTab::TxType => {}\n _ => {}\n }\nlet status_sec = Paragraph::new(status_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(\"Status\", theme))\n .alignment(Alignment::Left);\nlet date_sec = Paragraph::new(date_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(\"Start Date\", theme))\n .alignment(Alignment::Left);\nlet from_method_sec = Paragraph::new(from_method_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(from_method_name, theme))\n .alignment(Alignment::Left);\nlet to_method_sec = Paragraph::new(to_method_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(\"To Method\", theme))\n .alignment(Alignment::Left);\nlet amount_sec = Paragraph::new(amount_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(\"Amount\", theme))\n .alignment(Alignment::Left);\nlet tx_type_sec = Paragraph::new(tx_type_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(\"TX Type\", theme))\n .alignment(Alignment::Left);\nlet details_sec = Paragraph::new(details_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(\"Details\", theme))\n .alignment(Alignment::Left);\nlet tags_sec = Paragraph::new(tags_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(\"Tags\", theme))\n .alignment(Alignment::Left);\nlet frequency_sec = Paragraph::new(frequency_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(\"Frequency\", theme))\n .alignment(Alignment::Left);\nlet recur_interval_title = match frequency {\n RecurrenceFrequency::Daily => \"Every N Days\",\n RecurrenceFrequency::Weekly => \"Every N Weeks\",\n RecurrenceFrequency::Monthly => \"Every N Months\",\n RecurrenceFrequency::Yearly => \"Every N Years\",\n };\nlet recur_interval_sec = Paragraph::new(recur_interval_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(recur_interval_title, theme))\n .alignment(Alignment::Left);\nlet recur_value_title = match frequency {\n RecurrenceFrequency::Weekly => \"Day of Week\",\n _ => \"Day of Month\",\n };\nlet recur_value_sec = Paragraph::new(recur_value_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(recur_value_title, theme))\n .alignment(Alignment::Left);\nlet recur_month_sec = Paragraph::new(recur_month_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(\"Month\", theme))\n .alignment(Alignment::Left);\nlet end_date_sec = Paragraph::new(end_date_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(\"End Date (Optional)\", theme))\n .alignment(Alignment::Left);\nmatch recurring_tab {\n TxTab::Date => f.set_cursor_position(Position {\n x: core_chunk[0].x + current_index as u16 + 1,\n y: core_chunk[0].y + 1,\n }),\n TxTab::Details => f.set_cursor_position(Position {\n x: chunks[2].x + current_index as u16 + 1,\n y: chunks[2].y + 1,\n }),\n TxTab::TxType => f.set_cursor_position(Position {\n x: core_chunk[1].x + current_index as u16 + 1,\n y: core_chunk[1].y + 1,\n }),\n TxTab::FromMethod => f.set_cursor_position(Position {\n x: core_chunk[2].x + current_index as u16 + 1,\n y: core_chunk[2].y + 1,\n }),\n _ => {}\n }\nmatch tx_type {\n TxType::IncomeExpense => match recurring_tab {\n TxTab::Amount => f.set_cursor_position(Position {\n x: core_chunk[3].x + current_index as u16 + 1,\n y: core_chunk[3].y + 1,\n }),\n TxTab::Tags => f.set_cursor_position(Position {\n x: core_chunk[4].x + current_index as u16 + 1,\n y: core_chunk[4].y + 1,\n }),\n _ => {}\n },\n TxType::Transfer => match recurring_tab {\n TxTab::ToMethod => f.set_cursor_position(Position {\n x: core_chunk[3].x + current_index as u16 + 1,\n y: core_chunk[3].y + 1,\n }),\n TxTab::Amount => f.set_cursor_position(Position {\n x: core_chunk[4].x + current_index as u16 + 1,\n y: core_chunk[4].y + 1,\n }),\n TxTab::Tags => f.set_cursor_position(Position {\n x: core_chunk[5].x + current_index as u16 + 1,\n y: core_chunk[5].y + 1,\n }),\n _ => {}\n },\n }\nmatch recurring_tab {\n TxTab::Frequency => f.set_cursor_position(Position {\n x: recur_chunk[0].x + current_index as u16 + 1,\n y: recur_chunk[0].y + 1,\n }),\n TxTab::RecurInterval => f.set_cursor_position(Position {\n x: recur_chunk[1].x + current_index as u16 + 1,\n y: recur_chunk[1].y + 1,\n }),\n TxTab::RecurValue => f.set_cursor_position(Position {\n x: recur_chunk[2].x + current_index as u16 + 1,\n y: recur_chunk[2].y + 1,\n }),\n TxTab::RecurMonth if recur_box_count == 5 => f.set_cursor_position(Position {\n x: recur_chunk[3].x + current_index as u16 + 1,\n y: recur_chunk[3].y + 1,\n }),\n TxTab::EndDate => {}\n _ => {}\n }\nf.render_widget(details_sec, chunks[2]);\nf.render_widget(status_sec, chunks[3]);\nf.render_widget(date_sec, core_chunk[0]);\nf.render_widget(tx_type_sec, core_chunk[1]);\nf.render_widget(from_method_sec, core_chunk[2]);\nmatch tx_type {\n TxType::IncomeExpense => {}\n TxType::Transfer => {}\n }\nf.render_widget(frequency_sec, recur_chunk[0]);\nf.render_widget(recur_interval_sec, recur_chunk[1]);\nmatch frequency {\n RecurrenceFrequency::Daily => {}\n RecurrenceFrequency::Weekly | RecurrenceFrequency::Monthly => {}\n RecurrenceFrequency::Yearly => {}\n }\nif recurring_table.state.selected().is_some() {}\nif let Some(index) = recurring_table.state.selected()\n && index > 10\n {}\nf.render_stateful_widget(table_area, chunks[4], &mut recurring_table.state);\n}", + "body_hash": "cdbdb121ca296d7053aee314a749b4fd900a1c722b22ca0e33d04d97e58f8f49" + }, + "tui/src/pages/recurring_ui.rs|crate::recurring_ui#free:ab5b6ff9f743": { + "kind": "Free", + "file": "tui/src/pages/recurring_ui.rs", + "item_path": "crate::recurring_ui#free:ab5b6ff9f743", + "comment_kind": "Line", + "comment_text": "// Index 4 is the Amount column - the only one that should get thousands\n// separators", + "comment_hash": "ab5b6ff9f74391ac7829a60064af7160c419876e89307880cf0c849169771793", + "body_text": "if index == 4 {} else {}\n}", + "body_hash": "33f61081048c6e996d25253715055524f8f42208bd186b979c38f6f05d5e278b" + }, + "tui/src/pages/recurring_ui.rs|crate::tests::suggestion_shows_next_to_the_selected_recurrence_field_only#free:23c33b247f32": { + "kind": "Free", + "file": "tui/src/pages/recurring_ui.rs", + "item_path": "crate::tests::suggestion_shows_next_to_the_selected_recurrence_field_only#free:23c33b247f32", + "comment_kind": "Line", + "comment_text": "// Yearly is the only frequency that shows all three boxes at once", + "comment_hash": "23c33b247f3210a5c97398ef89177ea494d2b16f008a92c5f37a32d18275edcf", + "body_text": "data.frequency = \"y\".to_string();\ndata.recur_value = \"mon\".to_string();\ndata.recur_month = \"sep\".to_string();\ndata.autofill = \"SUGGESTION\".to_string();\nfor tab in [TxTab::Frequency, TxTab::RecurValue, TxTab::RecurMonth] {}\nlet screen = render(&data, &TxTab::Nothing);\nassert!(!screen.contains(\"SUGGESTION\"));\n}", + "body_hash": "e0945efb724da0913f67a05abb7cee5e67debb92d69cab336a924a0b88322bdd" + }, + "tui/src/pages/search_ui.rs|crate::search_ui#free:055eae7029c0": { + "kind": "Free", + "file": "tui/src/pages/search_ui.rs", + "item_path": "crate::search_ui#free:055eae7029c0", + "comment_kind": "Line", + "comment_text": "// Contains date, details, from method, to method, amount, tx type, tags.\n// Except to method, rest will be used for the widgets", + "comment_hash": "055eae7029c0dd79aa48c7044292c8620af006904540ceed9481f5031b10b850", + "body_text": "let input_data = search_data.get_all_texts();", + "body_hash": "ccb9baf966ae136ce69a420b3002bd628c1056087b155997e5d33d17cbc32450" + }, + "tui/src/pages/search_ui.rs|crate::search_ui#free:7daf9b3f5456": { + "kind": "Free", + "file": "tui/src/pages/search_ui.rs", + "item_path": "crate::search_ui#free:7daf9b3f5456", + "comment_kind": "Line", + "comment_text": "// The index of the cursor position", + "comment_hash": "7daf9b3f545601e8ed718e936c10e7ad4637a5471cc11af2b0861fd68d1afc6b", + "body_text": "let current_index = search_data.get_current_index();\nlet selected_style_income = Style::default()\n .fg(theme.positive())\n .add_modifier(Modifier::REVERSED);\nlet selected_style_expense = Style::default()\n .fg(theme.negative())\n .add_modifier(Modifier::REVERSED);\nlet size = f.area();\nlet tx_type = search_data.get_tx_type();\nlet tx_count = search_table.items.len();\nlet lerp_id = \"home_tx_count\";\nlet lerp_tx_count = lerp_state.lerp(lerp_id, tx_count as f64, None) as i64;\nlet lerp_row = lerp_state.lerp(SEARCH_TABLE_ID, tx_count as f64, Some(0.50)) as usize;\nlet table_name = format!(\"Transactions: {lerp_tx_count}\");\nlet rows = search_table\n .items\n .iter()\n .take(lerp_row)\n .enumerate()\n .map(|(row_index, item)| {});\nlet from_method_name = match tx_type {\n TxType::IncomeExpense => \"TX Method\",\n TxType::Transfer => \"From Method\",\n };\nlet date_name = match date_type {\n DateType::Exact => \"Search by Exact Date\",\n DateType::Monthly => \"Search by Month\",\n DateType::Yearly => \"Search by Year\",\n };\nlet header_cells = [\"Date\", \"Details\", \"TX Method\", \"Amount\", \"Type\", \"Tags\"]\n .iter()\n .map(|h| Cell::from(*h).style(Style::default().fg(theme.background())));\nlet header = Row::new(header_cells)\n .style(Style::default().bg(theme.header()))\n .height(1)\n .bottom_margin(0);", + "body_hash": "378552294710b36cbe000a3acd4dc34364758e1e4e59c584cb53bd985e418520" + }, + "tui/src/pages/search_ui.rs|crate::search_ui#free:8084f6795638": { + "kind": "Free", + "file": "tui/src/pages/search_ui.rs", + "item_path": "crate::search_ui#free:8084f6795638", + "comment_kind": "Line", + "comment_text": "// Depending on the tx type, split the first chunk into 5 or 6 columns.\n// This chunk holds the input boxes that take the user input", + "comment_hash": "8084f6795638c13ff55e835b2242897cb563a9e04f0597260e180af375fe4dc1", + "body_text": "let input_chunk = {};", + "body_hash": "f5386f9fc17dadfaabbbb0dd05055e60c81076507060de6a1316b5de81d7f6df" + }, + "tui/src/pages/search_ui.rs|crate::search_ui#free:9c1666ab54c2": { + "kind": "Free", + "file": "tui/src/pages/search_ui.rs", + "item_path": "crate::search_ui#free:9c1666ab54c2", + "comment_kind": "Line", + "comment_text": "// Iterate in reverse so the latest status line, which is the last value of the\n// vector, ends up at the top", + "comment_hash": "9c1666ab54c239438fc77e345d197d3eca728959b0437d44909fcd8eb3599b79", + "body_text": "for i in status_data.iter().rev() {}", + "body_hash": "5aa1224f50427fa656b674baf08c3fe11b253108b75b85c333df26785776133d" + }, + "tui/src/pages/search_ui.rs|crate::search_ui#free:a27428c44abe": { + "kind": "Free", + "file": "tui/src/pages/search_ui.rs", + "item_path": "crate::search_ui#free:a27428c44abe", + "comment_kind": "Line", + "comment_text": "// Get the data to insert into the Status widget of this page", + "comment_hash": "a27428c44abe722d135da31437c0259c6c20c73f813edbd8730dd9a1d58ec0d0", + "body_text": "let status_data = search_data.get_tx_status();", + "body_hash": "9af084e5809325b6e63132196692f4e607135de6b73bf0f0dc00df9e349fc589" + }, + "tui/src/pages/search_ui.rs|crate::search_ui#free:c938486cd94f": { + "kind": "Free", + "file": "tui/src/pages/search_ui.rs", + "item_path": "crate::search_ui#free:c938486cd94f", + "comment_kind": "Line", + "comment_text": "// Put the cursor at the index of the currently selected field.\n// Adapted from the tui-rs example named user_input.rs", + "comment_hash": "c938486cd94f7843035726b01708e01821ad7e60ad650da2ad714d72a27facf6", + "body_text": "match search_tab {\n TxTab::Date => f.set_cursor_position(Position {\n x: input_chunk[0].x + current_index as u16 + 1,\n y: input_chunk[0].y + 1,\n }),\n TxTab::Details => f.set_cursor_position(Position {\n x: chunks[1].x + current_index as u16 + 1,\n y: chunks[1].y + 1,\n }),\n TxTab::TxType => f.set_cursor_position(Position {\n x: input_chunk[1].x + current_index as u16 + 1,\n y: input_chunk[1].y + 1,\n }),\n TxTab::FromMethod => f.set_cursor_position(Position {\n x: input_chunk[2].x + current_index as u16 + 1,\n y: input_chunk[2].y + 1,\n }),\n _ => {}\n }\nmatch tx_type {\n TxType::IncomeExpense => match search_tab {\n TxTab::Amount => f.set_cursor_position(Position {\n x: input_chunk[3].x + current_index as u16 + 1,\n y: input_chunk[3].y + 1,\n }),\n\n TxTab::Tags => f.set_cursor_position(Position {\n x: input_chunk[4].x + current_index as u16 + 1,\n y: input_chunk[4].y + 1,\n }),\n _ => {}\n },\n TxType::Transfer => match search_tab {\n TxTab::ToMethod => f.set_cursor_position(Position {\n x: input_chunk[3].x + current_index as u16 + 1,\n y: input_chunk[3].y + 1,\n }),\n TxTab::Amount => f.set_cursor_position(Position {\n x: input_chunk[4].x + current_index as u16 + 1,\n y: input_chunk[4].y + 1,\n }),\n TxTab::Tags => f.set_cursor_position(Position {\n x: input_chunk[5].x + current_index as u16 + 1,\n y: input_chunk[5].y + 1,\n }),\n _ => {}\n },\n }\nif let Some(a) = search_table.state.selected() {}\nif let Some(index) = search_table.state.selected()\n && index > 10\n {}", + "body_hash": "e47e9d6a2a48e9554d79893c6ecb7bfb4d6e4027929f1d5a25c159d95e8e50d2" + }, + "tui/src/pages/search_ui.rs|crate::search_ui#free:c9739dce856f": { + "kind": "Free", + "file": "tui/src/pages/search_ui.rs", + "item_path": "crate::search_ui#free:c9739dce856f", + "comment_kind": "Line", + "comment_text": "// Creates the widgets to ready it for rendering", + "comment_hash": "c9739dce856f5b9418c8bbd32f3362b86b258c99b7d7bb67c8f26423bf4cb3e6", + "body_text": "let status_sec = Paragraph::new(status_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(\"Status\", theme))\n .alignment(Alignment::Left);\nlet date_sec = Paragraph::new(date_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(date_name, theme))\n .alignment(Alignment::Left);\nlet from_method_sec = Paragraph::new(from_method_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(from_method_name, theme))\n .alignment(Alignment::Left);\nlet to_method_sec = Paragraph::new(to_method_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(\"To Method\", theme))\n .alignment(Alignment::Left);\nlet amount_sec = Paragraph::new(amount_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(\"Amount\", theme))\n .alignment(Alignment::Left);\nlet tx_type_sec = Paragraph::new(tx_type_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(\"TX Type\", theme))\n .alignment(Alignment::Left);\nlet details_sec = Paragraph::new(details_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(\"Details\", theme))\n .alignment(Alignment::Left);\nlet tags_sec = Paragraph::new(tags_text)\n .style(Style::default().bg(theme.background()).fg(theme.text()))\n .block(styled_block(\"Tags\", theme))\n .alignment(Alignment::Left);", + "body_hash": "5cdc3f50132a1f2b40d1fe0b0a4106bee282b1f06749a9efb09d5a022c899ff9" + }, + "tui/src/pages/search_ui.rs|crate::search_ui#free:dc19b433c138": { + "kind": "Free", + "file": "tui/src/pages/search_ui.rs", + "item_path": "crate::search_ui#free:dc19b433c138", + "comment_kind": "Line", + "comment_text": "// Divide the terminal into 4 parts vertically", + "comment_hash": "dc19b433c138954ca0abd6a4d0d771530bd6984be758be70cf6fa50c92e14dc3", + "body_text": "let chunks = Layout::default()\n .direction(Direction::Vertical)\n .margin(2)\n .constraints([\n \n Constraint::Length(3),\n \n Constraint::Length(3),\n \n Constraint::Length(10),\n \n Constraint::Min(0),\n ])\n .split(size);", + "body_hash": "c2cdb66181616e8f91f02ebac055fd3f00a84ee780e3eff26d682d409f39e960" + }, + "tui/src/pages/search_ui.rs|crate::search_ui#free:e680dade6c26": { + "kind": "Free", + "file": "tui/src/pages/search_ui.rs", + "item_path": "crate::search_ui#free:e680dade6c26", + "comment_kind": "Line", + "comment_text": "// The values were already fetched above, so they just need to be assigned\n// and loaded into their widgets", + "comment_hash": "e680dade6c265cfb1e705e9d88888b99fcba4daa656b3481d250e1941d029eaa", + "body_text": "let date_text = Line::from(format!(\"{} \", input_data[0]));\nlet mut details_text = Line::from(format!(\"{} \", input_data[1]));\nlet mut from_method_text = Line::from(format!(\"{} \", input_data[2]));\nlet mut to_method_text = Line::from(format!(\"{} \", input_data[3]));\nlet amount_text = Line::from(format!(\"{} \", input_data[4]));\nlet mut tx_type_text = Line::from(format!(\"{} \", input_data[5]));\nlet mut tags_text = Line::from(format!(\"{} \", input_data[6]));\nmatch search_tab {\n TxTab::Details => {}\n TxTab::FromMethod => {}\n TxTab::ToMethod => {}\n TxTab::Tags => {}\n TxTab::TxType => {}\n _ => {}\n }", + "body_hash": "16d806e60a6948d2a8d0cd7b8e09b89f0317d1d57293fda1e0326289ef0abb27" + }, + "tui/src/pages/search_ui.rs|crate::search_ui#free:e71197256d46": { + "kind": "Free", + "file": "tui/src/pages/search_ui.rs", + "item_path": "crate::search_ui#free:e71197256d46", + "comment_kind": "Line", + "comment_text": "// Render the previously generated data into an interface", + "comment_hash": "e71197256d46213ef496cb0dc5aeeae4a74bb6cc1b952be85d429776139eb21a", + "body_text": "f.render_widget(details_sec, chunks[1]);\nf.render_widget(status_sec, chunks[2]);\nf.render_widget(date_sec, input_chunk[0]);\nf.render_widget(tx_type_sec, input_chunk[1]);\nf.render_widget(from_method_sec, input_chunk[2]);\nmatch tx_type {\n TxType::IncomeExpense => {}\n TxType::Transfer => {}\n }\nf.render_stateful_widget(table_area, chunks[3], &mut search_table.state);\n}", + "body_hash": "626fa42449b9a2e8ddc25aa5fecf9425b9884cef489fb22dd82c16d1ef968ff3" + }, + "tui/src/pages/search_ui.rs|crate::search_ui#free:ff561b0d9be3": { + "kind": "Free", + "file": "tui/src/pages/search_ui.rs", + "item_path": "crate::search_ui#free:ff561b0d9be3", + "comment_kind": "Line", + "comment_text": "// Creates border around the entire terminal", + "comment_hash": "ff561b0d9be3224b22319c008da3b83313defef033b8d8625f678bd48234598e", + "body_text": "f.render_widget(main_block(theme), size);\nlet mut table_area = Table::new(\n rows,\n [\n Constraint::Percentage(10),\n Constraint::Percentage(37),\n Constraint::Percentage(13),\n Constraint::Percentage(13),\n Constraint::Percentage(8),\n Constraint::Percentage(18),\n ],\n )\n .header(header)\n .block(styled_block(&table_name, theme));\nlet mut status_text = vec![];", + "body_hash": "6de96ac4f2de411667da76c092a96c8e9f43c2130db2381bb5a41e08f2d27369" + }, + "tui/src/pages/summary_ui.rs|crate::summary_ui": { + "kind": "Function", + "file": "tui/src/pages/summary_ui.rs", + "item_path": "crate::summary_ui", + "comment_kind": "OuterDoc", + "comment_text": " The function draws the Summary page of the interface.\n", + "comment_hash": "298cd9d5427f6be27f6c515fe0949d815b411d67bfa387ddfdaea4f0d0779fe0", + "body_text": "pub fn summary_ui(\n f: &mut Frame,\n months: &IndexedData,\n years: &IndexedData,\n mode_selection: &IndexedData,\n table_data: &mut TableData,\n current_page: &SummaryTab,\n summary_hidden_mode: bool,\n summary_sort: &SortingType,\n lerp_state: &mut LerpState,\n full_summary: &FullSummary,\n theme: &Theme,\n conn: &mut DbConn,\n) {\n let size = f.area();\n\n let tags_count = table_data.items.len();\n let lerp_id = \"summary_tags_count\";\n let lerp_tags_count = lerp_state.lerp(lerp_id, tags_count as f64, None) as i64;\n\n let lerp_row = lerp_state.lerp(SUMMARY_TABLE_ID, tags_count as f64, Some(0.50)) as usize;\n\n let tag_header = if let SortingType::Tags = summary_sort {\n \"Tags ↑\"\n } else {\n \"Tags\"\n };\n\n let total_income_header = if let SortingType::Income = summary_sort {\n \"Total Income ↑\"\n } else {\n \"Total Income\"\n };\n\n let total_expense_header = if let SortingType::Expense = summary_sort {\n \"Total Expense ↑\"\n } else {\n \"Total Expense\"\n };\n\n let mut tag_table_headers = vec![\n tag_header,\n total_income_header,\n total_expense_header,\n \"Income %\",\n \"Expense %\",\n ];\n\n let mut lend_borrow_headers = vec![\"Borrow\", \"Lend\"];\n\n if mode_selection.index == 0 {\n tag_table_headers.push(\"MoM Income %\");\n tag_table_headers.push(\"MoM Expense %\");\n\n lend_borrow_headers.push(\"MoM Borrow %\");\n lend_borrow_headers.push(\"MoM Lend %\");\n } else if mode_selection.index == 1 {\n tag_table_headers.push(\"YoY Income %\");\n tag_table_headers.push(\"YoY Expense %\");\n\n lend_borrow_headers.push(\"YoY Borrow %\");\n lend_borrow_headers.push(\"YoY Lend %\");\n }\n\n tag_table_headers.push(\"Borrow\");\n tag_table_headers.push(\"Lend\");\n\n let mut method_headers = vec![\"Method\", \"Total Income\", \"Total Expense\"];\n\n if mode_selection.index != 0 {\n method_headers.push(\"Average Income\");\n method_headers.push(\"Average Expense\");\n }\n\n method_headers.push(\"Income %\");\n method_headers.push(\"Expense %\");\n\n if mode_selection.index == 0 {\n method_headers.push(\"MoM Income %\");\n method_headers.push(\"MoM Expense %\");\n } else if mode_selection.index == 1 {\n method_headers.push(\"YoY Income %\");\n method_headers.push(\"YoY Expense %\");\n }\n\n let method_header_cells = method_headers\n .iter()\n .map(|h| Cell::from(*h).style(Style::default().fg(theme.background())));\n\n let tag_table_header_cells = tag_table_headers\n .into_iter()\n .map(|h| Cell::from(h).style(Style::default().fg(theme.background())));\n\n let lend_borrow_header_cells = lend_borrow_headers\n .into_iter()\n .map(|h| Cell::from(h).style(Style::default().fg(theme.background())));\n\n let tag_table_header = Row::new(tag_table_header_cells)\n .style(Style::default().bg(theme.header()))\n .height(1)\n .bottom_margin(0);\n\n let method_header = Row::new(method_header_cells)\n .style(Style::default().bg(theme.header()))\n .height(1)\n .bottom_margin(0);\n\n let lend_borrow_header = Row::new(lend_borrow_header_cells)\n .style(Style::default().bg(theme.header()))\n .height(1)\n .bottom_margin(0);\n\n let method_len = conn.get_tx_methods().len() as u16;\n\n let mut main_layout = Layout::default().direction(Direction::Vertical).margin(2);\n let mut summary_layout = Layout::default().direction(Direction::Horizontal);\n\n if summary_hidden_mode {\n main_layout = main_layout.constraints([\n Constraint::Length(method_len + 2),\n Constraint::Length(3),\n Constraint::Length(4),\n Constraint::Length(4),\n Constraint::Min(0),\n ]);\n summary_layout =\n summary_layout.constraints([Constraint::Percentage(50), Constraint::Percentage(50)]);\n } else {\n match mode_selection.index {\n 0 => {\n main_layout = main_layout.constraints([\n Constraint::Length(3),\n Constraint::Length(3),\n Constraint::Length(3),\n Constraint::Length(method_len + 2),\n Constraint::Length(3),\n Constraint::Length(4),\n Constraint::Length(4),\n Constraint::Min(0),\n ]);\n summary_layout = summary_layout\n .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]);\n }\n 1 => {\n main_layout = main_layout.constraints([\n Constraint::Length(3),\n Constraint::Length(3),\n Constraint::Length(method_len + 2),\n Constraint::Length(3),\n Constraint::Length(4),\n Constraint::Length(4),\n Constraint::Min(0),\n ]);\n summary_layout = summary_layout\n .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]);\n }\n 2 => {\n main_layout = main_layout.constraints([\n Constraint::Length(3),\n Constraint::Length(method_len + 2),\n Constraint::Length(3),\n Constraint::Length(4),\n Constraint::Length(4),\n Constraint::Min(0),\n ]);\n summary_layout = summary_layout\n .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]);\n }\n _ => {}\n }\n }\n\n let chunks = main_layout.split(size);\n\n \n \n let summary_chunk = if summary_hidden_mode {\n summary_layout.split(chunks[3])\n } else {\n summary_layout.split(chunks[6 - mode_selection.index])\n };\n\n f.render_widget(main_block(theme), size);\n\n let mut month_tab = create_tab(months, \"Months\", theme);\n\n let mut year_tab = create_tab(years, \"Years\", theme);\n\n let mut mode_selection_tab = create_tab(mode_selection, \"Modes\", theme);\n\n \n let rows = table_data\n .items\n .iter()\n .take(lerp_row)\n .enumerate()\n .map(|(row_index, item)| {\n let cells = item.iter().enumerate().map(|(index, c)| {\n if index == 0 {\n return Cell::from(c.clone());\n }\n\n let lerp_id = format!(\"summary_table_main:{index}:{row_index}\");\n\n let Ok(parsed_num) = c.parse::() else {\n if c == \"∞\" {\n let new_c = lerp_state.lerp(&lerp_id, 0.0, None);\n return Cell::from(format!(\"{new_c:.2}\").separate_with_commas());\n }\n\n let symbol = if c.contains('↑') || c.contains('↓') {\n c.chars().next()\n } else {\n None\n };\n\n if let Some(sym) = symbol {\n let c = c.replace(sym, \"\");\n if let Ok(parsed_num) = c.parse::() {\n let new_c = lerp_state.lerp(&lerp_id, parsed_num, None);\n\n return Cell::from(format!(\"{sym}{new_c:.2}\").separate_with_commas());\n }\n }\n return Cell::from(c.separate_with_commas());\n };\n\n let new_c = lerp_state.lerp(&lerp_id, parsed_num, None);\n\n Cell::from(format!(\"{new_c:.2}\").separate_with_commas())\n });\n Row::new(cells)\n .height(1)\n .bottom_margin(0)\n .style(Style::default().fg(theme.text()))\n });\n\n let table_width = if mode_selection.index == 2 {\n vec![\n Constraint::Percentage(15),\n Constraint::Percentage(15),\n Constraint::Percentage(14),\n Constraint::Percentage(14),\n Constraint::Percentage(14),\n Constraint::Percentage(14),\n Constraint::Percentage(14),\n ]\n } else {\n vec![\n Constraint::Percentage(12),\n Constraint::Percentage(11),\n Constraint::Percentage(11),\n Constraint::Percentage(11),\n Constraint::Percentage(11),\n Constraint::Percentage(11),\n Constraint::Percentage(11),\n Constraint::Percentage(11),\n Constraint::Percentage(11),\n ]\n };\n\n let tags_text = format!(\"Tags: {lerp_tags_count}\");\n\n let mut table_area = Table::new(rows, table_width)\n .header(tag_table_header)\n .block(styled_block(&tags_text, theme))\n .style(Style::default().fg(theme.border()));\n\n let summary_rows_largest =\n full_summary\n .largest_array()\n .into_iter()\n .enumerate()\n .map(|(row_index, item)| {\n let cells = item.into_iter().enumerate().map(|(index, c)| {\n let mut cell = if let Ok(parsed_num) = c.parse::() {\n let lerp_id = format!(\"summary_table_largest:{index}:{row_index}\");\n let new_c = lerp_state.lerp(&lerp_id, parsed_num, None);\n\n Cell::from(format!(\"{new_c:.2}\").separate_with_commas())\n } else {\n Cell::from(c.separate_with_commas())\n };\n\n if index == 0 {\n cell = cell.style(\n Style::default()\n .fg(theme.text())\n .add_modifier(Modifier::BOLD),\n );\n }\n cell\n });\n Row::new(cells)\n .height(1)\n .bottom_margin(0)\n .style(Style::default().fg(theme.text()))\n });\n\n let summary_area_largest = Table::new(\n summary_rows_largest,\n [\n Constraint::Percentage(25),\n Constraint::Percentage(25),\n Constraint::Percentage(25),\n Constraint::Percentage(25),\n ],\n )\n .block(styled_block(\"\", theme))\n .style(Style::default().fg(theme.border()));\n\n let summary_rows_peak =\n full_summary\n .peak_array()\n .into_iter()\n .enumerate()\n .map(|(row_index, item)| {\n let height = 1;\n let cells = item.into_iter().enumerate().map(|(index, c)| {\n let mut cell = if let Ok(parsed_num) = c.parse::() {\n let lerp_id = format!(\"summary_table_peak:{index}:{row_index}\");\n let new_c = lerp_state.lerp(&lerp_id, parsed_num, None);\n\n let text = if index == 1 && row_index == 2 {\n \n let new_c = new_c as i64;\n format!(\"{new_c}\").separate_with_commas()\n } else {\n format!(\"{new_c:.2}\").separate_with_commas()\n };\n\n Cell::from(text)\n } else {\n Cell::from(c.separate_with_commas())\n };\n\n if index == 0 {\n cell = cell.style(\n Style::default()\n .fg(theme.text())\n .add_modifier(Modifier::BOLD),\n );\n }\n cell\n });\n Row::new(cells)\n .height(height)\n .bottom_margin(0)\n .style(Style::default().fg(theme.text()))\n });\n\n let summary_area_peak = Table::new(\n summary_rows_peak,\n [\n Constraint::Percentage(33),\n Constraint::Percentage(33),\n Constraint::Percentage(33),\n ],\n )\n .block(styled_block(\"\", theme))\n .style(Style::default().fg(theme.border()));\n\n let method_rows =\n full_summary\n .method_array()\n .into_iter()\n .enumerate()\n .map(|(row_index, item)| {\n let cells = item.into_iter().enumerate().map(|(index, c)| {\n let mut cell = if let Ok(parsed_num) = c.parse::() {\n let lerp_id = format!(\"method_table:{index}:{row_index}\");\n let new_c = lerp_state.lerp(&lerp_id, parsed_num, None);\n\n Cell::from(format!(\"{new_c:.2}\").separate_with_commas())\n } else {\n let symbol = if c.contains('↑') || c.contains('↓') {\n c.chars().next()\n } else {\n None\n };\n\n if let Some(sym) = symbol {\n let c = c.replace(sym, \"\");\n if let Ok(parsed_num) = c.parse::() {\n let lerp_id = format!(\"method_table:{index}:{row_index}\");\n let new_c = lerp_state.lerp(&lerp_id, parsed_num, None);\n\n return Cell::from(\n format!(\"{sym}{new_c:.2}\").separate_with_commas(),\n );\n }\n }\n\n if c == \"∞\" {\n let lerp_id = format!(\"method_table:{index}:{row_index}\");\n lerp_state.lerp(&lerp_id, 0.0, None);\n }\n Cell::from(c.separate_with_commas())\n };\n\n if index == 0 {\n cell = cell.style(\n Style::default()\n .fg(theme.text())\n .add_modifier(Modifier::BOLD),\n );\n }\n cell\n });\n Row::new(cells)\n .height(1)\n .bottom_margin(0)\n .style(Style::default().fg(theme.text()))\n });\n\n let method_widths = if mode_selection.index == 2 {\n vec![\n Constraint::Percentage(10),\n Constraint::Percentage(14),\n Constraint::Percentage(14),\n Constraint::Percentage(14),\n Constraint::Percentage(14),\n Constraint::Percentage(16),\n Constraint::Percentage(16),\n ]\n } else if mode_selection.index == 1 {\n vec![\n Constraint::Percentage(10),\n Constraint::Percentage(12),\n Constraint::Percentage(12),\n Constraint::Percentage(10),\n Constraint::Percentage(10),\n Constraint::Percentage(12),\n Constraint::Percentage(12),\n Constraint::Percentage(10),\n Constraint::Percentage(10),\n ]\n } else {\n vec![\n Constraint::Percentage(10),\n Constraint::Percentage(14),\n Constraint::Percentage(14),\n Constraint::Percentage(14),\n Constraint::Percentage(14),\n Constraint::Percentage(16),\n Constraint::Percentage(16),\n ]\n };\n\n let method_area = Table::new(method_rows, &method_widths)\n .header(method_header)\n .block(styled_block_no_bottom(\"\", theme))\n .style(Style::default().fg(theme.border()));\n\n let lend_borrow_rows = full_summary\n .lend_borrows_array()\n .into_iter()\n .enumerate()\n .map(|(row_index, item)| {\n let cells = item.into_iter().enumerate().map(|(index, c)| {\n let lerp_id = format!(\"lend_borrow_table:{index}:{row_index}\");\n\n if let Ok(parsed_num) = c.parse::() {\n let new_c = lerp_state.lerp(&lerp_id, parsed_num, None);\n\n Cell::from(format!(\"{new_c:.2}\").separate_with_commas())\n } else {\n let symbol = if c.contains('↑') || c.contains('↓') {\n c.chars().next()\n } else {\n None\n };\n\n if let Some(sym) = symbol {\n let c = c.replace(sym, \"\");\n if let Ok(parsed_num) = c.parse::() {\n let new_c = lerp_state.lerp(&lerp_id, parsed_num, None);\n\n return Cell::from(format!(\"{sym}{new_c:.2}\").separate_with_commas());\n }\n }\n\n if c == \"∞\" {\n lerp_state.lerp(&lerp_id, 0.0, None);\n }\n Cell::from(c.separate_with_commas())\n }\n });\n Row::new(cells)\n .height(1)\n .bottom_margin(0)\n .style(Style::default().fg(theme.text()))\n });\n\n let lend_borrow_widths = if mode_selection.index == 2 {\n vec![Constraint::Percentage(50), Constraint::Percentage(50)]\n } else {\n vec![\n Constraint::Percentage(25),\n Constraint::Percentage(25),\n Constraint::Percentage(25),\n Constraint::Percentage(25),\n ]\n };\n\n let lend_borrow_area = Table::new(lend_borrow_rows, &lend_borrow_widths)\n .header(lend_borrow_header)\n .block(styled_block(\"\", theme))\n .style(Style::default().fg(theme.border()));\n\n let net_row = full_summary\n .net_array()\n .into_iter()\n .enumerate()\n .map(|(row_index, item)| {\n let cells = item.iter().enumerate().map(|(index, c)| {\n let mut cell = if let Ok(parsed_num) = c.parse::() {\n let lerp_id = format!(\"summary_rows_net:{index}:{row_index}\");\n let new_c = lerp_state.lerp(&lerp_id, parsed_num, None);\n\n Cell::from(format!(\"{new_c:.2}\").separate_with_commas())\n } else {\n if c == \"∞\" {\n let lerp_id = format!(\"summary_rows_net:{index}:{row_index}\");\n lerp_state.lerp(&lerp_id, 0.0, None);\n }\n\n let symbol = if c.contains('↑') || c.contains('↓') {\n c.chars().next()\n } else {\n None\n };\n\n if let Some(sym) = symbol {\n let c = c.replace(sym, \"\");\n if let Ok(parsed_num) = c.parse::() {\n let lerp_id = format!(\"summary_net_rows:{index}:{row_index}\");\n let new_c = lerp_state.lerp(&lerp_id, parsed_num, None);\n\n return Cell::from(format!(\"{sym}{new_c:.2}\").separate_with_commas());\n }\n }\n Cell::from(c.separate_with_commas())\n };\n\n if index == 0 {\n cell = cell.style(\n Style::default()\n .fg(theme.text())\n .add_modifier(Modifier::BOLD),\n );\n }\n cell\n });\n Row::new(cells)\n .height(1)\n .bottom_margin(0)\n .style(Style::default().fg(theme.text()))\n });\n\n let net_area = Table::new(net_row, &method_widths)\n .block(styled_block_no_top(\"\", theme))\n .style(Style::default().fg(theme.border()));\n\n match current_page {\n \n \n SummaryTab::Months => month_tab = month_tab.highlight_style(tab_highlight_style(theme)),\n SummaryTab::Years => year_tab = year_tab.highlight_style(tab_highlight_style(theme)),\n SummaryTab::ModeSelection => {\n mode_selection_tab = mode_selection_tab.highlight_style(tab_highlight_style(theme));\n }\n SummaryTab::Table => {\n let add_modifier = theme.add_reverse_modifier();\n\n let mut style = Style::default();\n\n if add_modifier {\n style = style.fg(theme.selected()).add_modifier(Modifier::REVERSED);\n } else {\n style = style.bg(theme.selected());\n }\n\n table_area = table_area\n .row_highlight_style(style)\n .highlight_symbol(\">> \");\n }\n }\n\n \n if let Some(index) = table_data.state.selected()\n && index > 7\n {\n *table_data.state.offset_mut() = index - 7;\n }\n\n f.render_widget(summary_area_largest, summary_chunk[1]);\n f.render_widget(summary_area_peak, summary_chunk[0]);\n\n if summary_hidden_mode {\n f.render_stateful_widget(table_area, chunks[4], &mut table_data.state);\n f.render_widget(lend_borrow_area, chunks[2]);\n f.render_widget(net_area, chunks[1]);\n f.render_widget(method_area, chunks[0]);\n } else {\n f.render_widget(mode_selection_tab, chunks[0]);\n\n match mode_selection.index {\n 0 => {\n f.render_widget(year_tab, chunks[1]);\n f.render_widget(month_tab, chunks[2]);\n f.render_stateful_widget(table_area, chunks[7], &mut table_data.state);\n f.render_widget(net_area, chunks[4]);\n f.render_widget(lend_borrow_area, chunks[5]);\n f.render_widget(method_area, chunks[3]);\n }\n 1 => {\n f.render_widget(year_tab, chunks[1]);\n f.render_stateful_widget(table_area, chunks[6], &mut table_data.state);\n f.render_widget(net_area, chunks[3]);\n f.render_widget(lend_borrow_area, chunks[4]);\n f.render_widget(method_area, chunks[2]);\n }\n 2 => {\n f.render_stateful_widget(table_area, chunks[5], &mut table_data.state);\n f.render_widget(net_area, chunks[2]);\n f.render_widget(lend_borrow_area, chunks[3]);\n f.render_widget(method_area, chunks[1]);\n }\n _ => {}\n }\n }\n}", + "body_hash": "ffc8bdd10cd63b6cedfdec042e85020b2bd99beca97f307438ab49b37adb4ca9" + }, + "tui/src/pages/summary_ui.rs|crate::summary_ui#free:34babd4dfed9": { + "kind": "Free", + "file": "tui/src/pages/summary_ui.rs", + "item_path": "crate::summary_ui#free:34babd4dfed9", + "comment_kind": "Line", + "comment_text": "// The peak + largest area sits in a horizontal split: take the matching\n// vertical chunk of the main layout and split that one horizontally again", + "comment_hash": "34babd4dfed9f766f7fab683bfd3915cbfc96e2c056223d47982dc07da80d3ef", + "body_text": "let summary_chunk = if summary_hidden_mode {} else {};\nf.render_widget(main_block(theme), size);\nlet mut month_tab = create_tab(months, \"Months\", theme);\nlet mut year_tab = create_tab(years, \"Years\", theme);\nlet mut mode_selection_tab = create_tab(mode_selection, \"Modes\", theme);", + "body_hash": "15339c172ff99e7819ada9e6c22c495141766d2b8421abadf54dc8e79e75280f" + }, + "tui/src/pages/summary_ui.rs|crate::summary_ui#free:bb1db8402aa1": { + "kind": "Free", + "file": "tui/src/pages/summary_ui.rs", + "item_path": "crate::summary_ui#free:bb1db8402aa1", + "comment_kind": "Line", + "comment_text": "// Goes through all tags provided and creates row for the table", + "comment_hash": "bb1db8402aa18ebafd3016fdc3f7ef56028e8da4eb1f856e1397334d70de8880", + "body_text": "let rows = table_data\n .items\n .iter()\n .take(lerp_row)\n .enumerate()\n .map(|(row_index, item)| {});\nlet table_width = if mode_selection.index == 2 {} else {};\nlet tags_text = format!(\"Tags: {lerp_tags_count}\");\nlet mut table_area = Table::new(rows, table_width)\n .header(tag_table_header)\n .block(styled_block(&tags_text, theme))\n .style(Style::default().fg(theme.border()));\nlet summary_rows_largest =\n full_summary\n .largest_array()\n .into_iter()\n .enumerate()\n .map(|(row_index, item)| {});\nlet summary_area_largest = Table::new(\n summary_rows_largest,\n [\n Constraint::Percentage(25),\n Constraint::Percentage(25),\n Constraint::Percentage(25),\n Constraint::Percentage(25),\n ],\n )\n .block(styled_block(\"\", theme))\n .style(Style::default().fg(theme.border()));\nlet summary_rows_peak =\n full_summary\n .peak_array()\n .into_iter()\n .enumerate()\n .map(|(row_index, item)| {});\nlet summary_area_peak = Table::new(\n summary_rows_peak,\n [\n Constraint::Percentage(33),\n Constraint::Percentage(33),\n Constraint::Percentage(33),\n ],\n )\n .block(styled_block(\"\", theme))\n .style(Style::default().fg(theme.border()));\nlet method_rows =\n full_summary\n .method_array()\n .into_iter()\n .enumerate()\n .map(|(row_index, item)| {});\nlet method_widths = if mode_selection.index == 2 {} else if mode_selection.index == 1 {} else {};\nlet method_area = Table::new(method_rows, &method_widths)\n .header(method_header)\n .block(styled_block_no_bottom(\"\", theme))\n .style(Style::default().fg(theme.border()));\nlet lend_borrow_rows = full_summary\n .lend_borrows_array()\n .into_iter()\n .enumerate()\n .map(|(row_index, item)| {});\nlet lend_borrow_widths = if mode_selection.index == 2 {} else {};\nlet lend_borrow_area = Table::new(lend_borrow_rows, &lend_borrow_widths)\n .header(lend_borrow_header)\n .block(styled_block(\"\", theme))\n .style(Style::default().fg(theme.border()));\nlet net_row = full_summary\n .net_array()\n .into_iter()\n .enumerate()\n .map(|(row_index, item)| {});\nlet net_area = Table::new(net_row, &method_widths)\n .block(styled_block_no_top(\"\", theme))\n .style(Style::default().fg(theme.border()));\nmatch current_page {\n \n \n SummaryTab::Months => month_tab = month_tab.highlight_style(tab_highlight_style(theme)),\n SummaryTab::Years => year_tab = year_tab.highlight_style(tab_highlight_style(theme)),\n SummaryTab::ModeSelection => {}\n SummaryTab::Table => {}\n }", + "body_hash": "19509cd3cd46a182cef66682f1de2d2040d8f66e09e08fcc122a86fe70642e84" + }, + "tui/src/pages/summary_ui.rs|crate::summary_ui#free:f37c8defa895": { + "kind": "Free", + "file": "tui/src/pages/summary_ui.rs", + "item_path": "crate::summary_ui#free:f37c8defa895", + "comment_kind": "Line", + "comment_text": "// Month count, so whole numbers are enough here", + "comment_hash": "f37c8defa895ac98b01b36085e90c35e8c90b5a0188ea7b68c6a0f09f6d7ba5a", + "body_text": "let new_c = new_c as i64;\nformat!(\"{new_c}\").separate_with_commas()\n}", + "body_hash": "2f7b439f04fb043ae9063b5d3d7978c7c82e2df2084b3519e630b17de1e01cc7" + }, + "tui/src/pages/summary_ui.rs|crate::summary_ui#free:f4dbdead76d3": { + "kind": "Free", + "file": "tui/src/pages/summary_ui.rs", + "item_path": "crate::summary_ui#free:f4dbdead76d3", + "comment_kind": "Line", + "comment_text": "// Always keep some items rendered on the upper side of the table", + "comment_hash": "f4dbdead76d3251e69e582a35158024cff1ec2ccd6a2f1048ad198c56055cd33", + "body_text": "if let Some(index) = table_data.state.selected()\n && index > 7\n {}\nf.render_widget(summary_area_largest, summary_chunk[1]);\nf.render_widget(summary_area_peak, summary_chunk[0]);\nif summary_hidden_mode {} else {}\n}", + "body_hash": "b32f67ba1897d4375205abd641086cdada6d26226a33d61667c6648213a4a54b" + }, + "tui/src/tx_handler/tx_data.rs|crate::TxData": { + "kind": "Struct", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::TxData", + "comment_kind": "OuterDoc", + "comment_text": " Every field of the Add TX, Search and Recurring page forms, plus the cursor\n\n index, the status log shown under the inputs and the editing state\n", + "comment_hash": "2057fed997b13b9f72446348b2b551ee4a507bb756e031b3f798d922fe868336", + "body_text": "#[derive(Default)]\npub struct TxData {\n pub date: String,\n pub details: String,\n pub from_method: String,\n pub to_method: String,\n pub amount: String,\n pub tx_type: String,\n pub tags: String,\n pub frequency: String,\n pub recur_interval: String,\n pub recur_value: String,\n pub recur_month: String,\n pub end_date: String,\n pub tx_status: Vec,\n pub editing_tx: bool,\n pub id_num: i32,\n pub current_index: usize,\n pub autofill: String,\n pub from_search: bool,\n}", + "body_hash": "ba106694587698f8a77d1486b652968dd702d00a4b3172d21e67a7e04fe4cd94" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::add_recurring_tx": { + "kind": "Function", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::add_recurring_tx", + "comment_kind": "OuterDoc", + "comment_text": " Takes all Recurring page data and creates or updates a recurring transaction rule\n", + "comment_hash": "a5ca75101d9cbb634fd4557f23b49284ffcec00eb5a110eef94b4d6c27afa2f0", + "body_text": "pub fn add_recurring_tx(&mut self, conn: &mut DbConn) -> AResult<()> {\n self.check_all_recurring_fields()?;\n\n let editing_tx = self.editing_tx;\n\n let parsed = parse_recurring_tx_fields(\n &self.date,\n &self.details,\n &self.from_method,\n &self.to_method,\n &self.amount,\n &self.tx_type,\n &self.frequency,\n &self.recur_interval,\n &self.recur_value,\n &self.recur_month,\n &self.end_date,\n conn,\n )?;\n\n if editing_tx {\n conn.edit_recurring_tx(self.id_num, parsed, &self.tags)\n } else {\n conn.add_recurring_tx(parsed, &self.tags)\n }\n }", + "body_hash": "1ed1db268962328bbe476cbe8deb6227bd5f3d7a31d168ca602de3db3c4906d7" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::add_tx": { + "kind": "Function", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::add_tx", + "comment_kind": "OuterDoc", + "comment_text": " Takes all data and adds it as a transaction\n", + "comment_hash": "6aa762eb217fdba74cea45d028ded91220353fcab16f2c6174f770ddb7def1ab", + "body_text": "pub fn add_tx(&mut self, tx_view: &TxViewGroup, conn: &mut DbConn) -> AResult<()> {\n self.check_all_fields()?;\n\n let editing_tx = self.editing_tx;\n let parsed_tx = parse_tx_fields(\n &self.date,\n &self.details,\n &self.from_method,\n &self.to_method,\n &self.amount,\n &self.tx_type,\n conn,\n )?;\n\n if editing_tx {\n let old_tx_id = self.id_num;\n let old_tx = if let Some(tx) = tx_view.get_tx_by_id(old_tx_id) {\n tx\n } else {\n &conn.fetch_tx_with_id(old_tx_id)?\n };\n\n conn.edit_tx(old_tx, parsed_tx, &self.tags)\n } else {\n conn.add_new_tx(parsed_tx, &self.tags)\n }\n }", + "body_hash": "f13d01ac2a7c2ddbbc97732618e201b3d578a128e0b764f5a049ac54ff1f6308" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::add_tx_status": { + "kind": "Function", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::add_tx_status", + "comment_kind": "OuterDoc", + "comment_text": " Adds a value to tx status\n", + "comment_hash": "1a38166724262de89e17d713afeaae4f485014be757ca14f6d0f754d75be4ef8", + "body_text": "pub fn add_tx_status(&mut self, text: String, log_type: LogType) {\n if self.tx_status.len() == 30 {\n self.tx_status.remove(0);\n }\n\n let data = LogData::new(text, log_type);\n self.tx_status.push(data);\n }", + "body_hash": "b4eab2c629bf624d760e91ad8902e71660382041af64a9fb18eae4278e3ca28b" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::check_all_fields": { + "kind": "Function", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::check_all_fields", + "comment_kind": "OuterDoc", + "comment_text": " Ensures every field that must be filled in before saving a tx is filled in\n", + "comment_hash": "54b5da7544ae7efb5f845973becad3784520839006c63165fb0793a9856c0fce", + "body_text": "pub fn check_all_fields(&mut self) -> Result<(), CheckingError> {\n if self.date.is_empty() {\n return Err(CheckingError::EmptyDate);\n } else if self.from_method.is_empty() && self.tx_type != \"Transfer\" {\n return Err(CheckingError::EmptyMethod);\n } else if self.amount.is_empty() {\n return Err(CheckingError::EmptyAmount);\n } else if self.tx_type.is_empty() {\n return Err(CheckingError::EmptyTxType);\n } else if self.tx_type == \"Transfer\" && self.from_method == self.to_method {\n return Err(CheckingError::SameTxMethod);\n } else if self.tx_type == \"Transfer\"\n && (self.from_method.is_empty() || self.to_method.is_empty())\n {\n return Err(CheckingError::EmptyMethod);\n }\n \n if self.tags.is_empty() {\n self.tags = \"Unknown\".to_string();\n }\n Ok(())\n }", + "body_hash": "8f8294e5b2191ccf80c0410ad1823a9c17bb961a63263b8ff32d83a7b785bd24" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::check_all_fields#free:4719ce0852d5": { + "kind": "Free", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::check_all_fields#free:4719ce0852d5", + "comment_kind": "Line", + "comment_text": "// A tx saved without any tag is stored as \"Unknown\"", + "comment_hash": "4719ce0852d5fe2986223a33bf75dfa9fdf2a70565879e3a8bacd9f598adc489", + "body_text": "if self.tags.is_empty() {}\nOk(())\n}", + "body_hash": "bc63b40c98100b9347c1472107f0807bdae1a28f7bdb88717dda7c703adcc38e" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::check_all_recurring_fields": { + "kind": "Function", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::check_all_recurring_fields", + "comment_kind": "OuterDoc", + "comment_text": " Ensures every field that must be filled in before saving a recurring rule\n\n is filled in, plus the ones the chosen frequency needs\n", + "comment_hash": "64aeedcdd38d41a7978f1d675e4d7ebf22be42f0430b6741a1813fc9fe4d6daa", + "body_text": "pub fn check_all_recurring_fields(&mut self) -> Result<(), CheckingError> {\n if self.date.is_empty() {\n return Err(CheckingError::EmptyDate);\n } else if self.from_method.is_empty() && self.tx_type != \"Transfer\" {\n return Err(CheckingError::EmptyMethod);\n } else if self.amount.is_empty() {\n return Err(CheckingError::EmptyAmount);\n } else if self.tx_type.is_empty() {\n return Err(CheckingError::EmptyTxType);\n } else if self.tx_type == \"Transfer\" && self.from_method == self.to_method {\n return Err(CheckingError::SameTxMethod);\n } else if self.tx_type == \"Transfer\"\n && (self.from_method.is_empty() || self.to_method.is_empty())\n {\n return Err(CheckingError::EmptyMethod);\n } else if self.frequency.is_empty() {\n return Err(CheckingError::EmptyFrequency);\n } else if self.recur_interval.is_empty() {\n return Err(CheckingError::EmptyRecurInterval);\n }\n\n match self.get_frequency() {\n RecurrenceFrequency::Daily => {}\n RecurrenceFrequency::Weekly | RecurrenceFrequency::Monthly => {\n if self.recur_value.is_empty() {\n return Err(CheckingError::EmptyRecurValue);\n }\n }\n RecurrenceFrequency::Yearly => {\n if self.recur_value.is_empty() {\n return Err(CheckingError::EmptyRecurValue);\n } else if self.recur_month.is_empty() {\n return Err(CheckingError::EmptyRecurMonth);\n }\n }\n }\n\n \n if self.tags.is_empty() {\n self.tags = \"Unknown\".to_string();\n }\n Ok(())\n }", + "body_hash": "e349b14eb44a3dea82df5ed6fe5738e0e51b850aa74eff38ea84daf183b406df" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::check_all_recurring_fields#free:4719ce0852d5": { + "kind": "Free", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::check_all_recurring_fields#free:4719ce0852d5", + "comment_kind": "Line", + "comment_text": "// A tx saved without any tag is stored as \"Unknown\"", + "comment_hash": "4719ce0852d5fe2986223a33bf75dfa9fdf2a70565879e3a8bacd9f598adc489", + "body_text": "if self.tags.is_empty() {}\nOk(())\n}", + "body_hash": "bc63b40c98100b9347c1472107f0807bdae1a28f7bdb88717dda7c703adcc38e" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::check_amount": { + "kind": "Function", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::check_amount", + "comment_kind": "OuterDoc", + "comment_text": " Checks the inputted Amount by the user upon pressing Enter/Esc for various error.\n", + "comment_hash": "c65ebcbfcacb3dc2f74523659140351a46fa5a48f44a76752b5b526f54556f8a", + "body_text": "pub fn check_amount(\n &mut self,\n is_search: bool,\n conn: &mut DbConn,\n ) -> Result {\n self.check_b_field(conn)?;\n\n let mut comparison_symbol = None;\n\n let mut user_amount = self.amount.clone().to_lowercase();\n\n if is_search {\n match check_comparison(&user_amount) {\n ComparisonType::Equal => comparison_symbol = None,\n ComparisonType::BiggerThan => comparison_symbol = Some(\">\"),\n ComparisonType::SmallerThan => comparison_symbol = Some(\"<\"),\n ComparisonType::EqualOrBigger => comparison_symbol = Some(\">=\"),\n ComparisonType::EqualOrSmaller => comparison_symbol = Some(\"<=\"),\n }\n }\n\n if let Some(symbol) = comparison_symbol {\n user_amount = user_amount.replace(symbol, \"\");\n }\n\n let status = conn.verify().amount(&mut user_amount);\n\n if let Some(symbol) = comparison_symbol {\n user_amount = format!(\"{symbol}{user_amount}\");\n }\n\n self.amount = user_amount;\n\n self.go_current_index(&TxTab::Amount);\n status\n }", + "body_hash": "b1cc001e24b85d1ca91d433a3850a8b1b6f91591454453bac2e24e013f3eb293" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::check_b_field": { + "kind": "Function", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::check_b_field", + "comment_kind": "OuterDoc", + "comment_text": " Replaces a `b` in the amount with the current balance of the from method\n", + "comment_hash": "ab0584dc7cc435a64856468e0ffa16405b0e11467f771cf6bc884f30b356aa46", + "body_text": "fn check_b_field(&mut self, conn: &mut DbConn) -> Result<(), VerifierError> {\n self.check_suffixes();\n let user_amount = self.amount.to_lowercase();\n\n \n if user_amount.contains('b') && !self.from_method.is_empty() {\n let final_balances = conn\n .get_final_balances()\n .map_err(|e| VerifierError::Others(e.to_string()))?;\n\n let target_method = conn\n .get_tx_method_by_name(self.from_method.as_str())\n .map_err(|e| VerifierError::Others(e.to_string()))?;\n\n \n for balance in final_balances.values() {\n if balance.method_id == target_method.id {\n self.amount = user_amount\n .replace('b', &(Cent::new(balance.balance).dollar()).to_string());\n }\n }\n } else if user_amount.contains('b') && self.from_method.is_empty() {\n return Err(VerifierError::InvalidBValue);\n }\n Ok(())\n }", + "body_hash": "d0e5d4fe5e6a927b0f75496ca722dfac0f30f636012fd9c794532d63397a35f3" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::check_b_field#free:852210a19376": { + "kind": "Free", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::check_b_field#free:852210a19376", + "comment_kind": "Line", + "comment_text": "// Find the final balance of the target method", + "comment_hash": "852210a19376348daae8b00a1b0ff66cfa3ef07c2560bfd8585652b552c36def", + "body_text": "for balance in final_balances.values() {}\n}", + "body_hash": "b059bbd3b855ee8775ae3b4cb9de86fbfb7aabf7cbf061d84df7d0fc22411df6" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::check_b_field#free:9a2b3262c55a": { + "kind": "Free", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::check_b_field#free:9a2b3262c55a", + "comment_kind": "Line", + "comment_text": "// 'b' stands for the current balance of the from method", + "comment_hash": "9a2b3262c55acfca4aaac7386409c6a47d070e8937867e213f2f843316428763", + "body_text": "if user_amount.contains('b') && !self.from_method.is_empty() {} else if user_amount.contains('b') && self.from_method.is_empty() {}\nOk(())\n}", + "body_hash": "b43dd45966c266fdcaf3766c58af8bc7545e4ffb0d90d59d77aa102e1860fe17" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::check_date": { + "kind": "Function", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::check_date", + "comment_kind": "OuterDoc", + "comment_text": " Checks the inputted Date by the user upon pressing Enter/Esc for various error.\n", + "comment_hash": "d2c2bb30e1ebd3d3f902c84d1e7df9b87c07c0f6000e33feb231c6f961b33c9b", + "body_text": "pub fn check_date(\n &mut self,\n date_type: DateType,\n conn: &mut DbConn,\n ) -> Result {\n let mut user_date = self.date.clone();\n let status = conn.verify().date(&mut user_date, date_type);\n\n self.date = user_date;\n self.go_current_index(&TxTab::Date);\n status\n }", + "body_hash": "605903515ddd3497402d882c4bb53152339a021d8a82dad33113ff8d1b3a4caa" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::check_end_date": { + "kind": "Function", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::check_end_date", + "comment_kind": "OuterDoc", + "comment_text": " Checks the inputted end date by the user upon pressing Enter/Esc. Empty is valid -\n\n it means the recurrence never ends.\n", + "comment_hash": "7ead3c341f44e238d78f0d96628cc3c6612bdc0bcc5548161c249b7532ef8e30", + "body_text": "pub fn check_end_date(&mut self, conn: &mut DbConn) -> Result {\n let mut end_date = self.end_date.clone();\n\n let status = conn.verify().date(&mut end_date, DateType::Exact);\n\n self.end_date = end_date;\n self.go_current_index(&TxTab::EndDate);\n status\n }", + "body_hash": "4688aee54d81a70d7ab3bfe585d49c5ef1f6bbd0b4d237f59a68250c91dbf2c5" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::check_frequency": { + "kind": "Function", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::check_frequency", + "comment_kind": "OuterDoc", + "comment_text": " Checks the inputted recurrence frequency by the user upon pressing Enter/Esc\n", + "comment_hash": "164fac176f4e56666d05f4cb04b80a1d46fd516e26126ae68c5dc226507d496e", + "body_text": "pub fn check_frequency(&mut self, conn: &mut DbConn) -> Result {\n let mut frequency = self.frequency.clone();\n\n let status = conn.verify().frequency(&mut frequency);\n\n self.frequency = frequency;\n self.go_current_index(&TxTab::Frequency);\n status\n }", + "body_hash": "101f282fca6379ce726749ac9c80f9679c76b40303f9f6ed330fa2c89f0a16cd" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::check_from_method": { + "kind": "Function", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::check_from_method", + "comment_kind": "OuterDoc", + "comment_text": " Checks the inputted From Method by the user upon pressing Enter/Esc for various error.\n", + "comment_hash": "5a9c3ba5c547a4c2df1991cb07dd834c62620d389d153de21f82354627f22b70", + "body_text": "pub fn check_from_method(&mut self, conn: &mut DbConn) -> Result {\n let mut current_method = self.from_method.clone();\n\n let status = conn.verify().tx_method(&mut current_method);\n\n self.from_method = current_method;\n self.go_current_index(&TxTab::FromMethod);\n\n status\n }", + "body_hash": "535612f4d8b5e68324591e56fa56c93b32fbb60abf86d358763d86a48057c446" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::check_recur_interval": { + "kind": "Function", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::check_recur_interval", + "comment_kind": "OuterDoc", + "comment_text": " Checks the inputted recur interval by the user upon pressing Enter/Esc\n", + "comment_hash": "59e768b594a231685f9014f7ad5b91aec8a89102cd2b824d29980780e37b9f9d", + "body_text": "pub fn check_recur_interval(&mut self, conn: &mut DbConn) -> Result {\n let mut recur_interval = self.recur_interval.clone();\n\n let status = conn.verify().recur_interval(&mut recur_interval);\n\n self.recur_interval = recur_interval;\n self.go_current_index(&TxTab::RecurInterval);\n status\n }", + "body_hash": "367c03b3e70ea927e3084bde0c29200b3feffa714ab4083ba30da73a1edb49cb" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::check_recur_month": { + "kind": "Function", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::check_recur_month", + "comment_kind": "OuterDoc", + "comment_text": " Checks the inputted recur month by the user upon pressing Enter/Esc\n", + "comment_hash": "3b296e773f348e5bb744bb3aa3becb0a19729583f795bca169dc0bf4005ad863", + "body_text": "pub fn check_recur_month(&mut self, conn: &mut DbConn) -> Result {\n let mut recur_month = self.recur_month.clone();\n\n let status = conn.verify().recur_month(&mut recur_month);\n\n self.recur_month = recur_month;\n self.go_current_index(&TxTab::RecurMonth);\n status\n }", + "body_hash": "ccdce3040f62feb569317f6b2170dca42aba0eff3df01b0509911385ee2ca24e" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::check_recur_value": { + "kind": "Function", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::check_recur_value", + "comment_kind": "OuterDoc", + "comment_text": " Checks the inputted recur value by the user upon pressing Enter/Esc\n", + "comment_hash": "f5bad2e3eb0f9ccd2bf0edd71806130f3ae2af8fcb5687531baf1bcb5285b7d3", + "body_text": "pub fn check_recur_value(&mut self, conn: &mut DbConn) -> Result {\n let mut recur_value = self.recur_value.clone();\n let frequency = self.get_frequency();\n\n let status = conn.verify().recur_value(&mut recur_value, frequency);\n\n self.recur_value = recur_value;\n self.go_current_index(&TxTab::RecurValue);\n status\n }", + "body_hash": "ef7cb11d6e2421f628ad93bc5a7ef3a88468c3e71de9bf94b33f511312835e36" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::check_suffixes": { + "kind": "Function", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::check_suffixes", + "comment_kind": "OuterDoc", + "comment_text": " Multiplies a number by 1 thousand for each `k` and by 1 million for each `m`\n\n that follows it\n", + "comment_hash": "0d1b2ba5982e00561dec4631af79ca6a3465a24f8e92af557e2e049ea76474f8", + "body_text": "fn check_suffixes(&mut self) {\n let mut user_amount = self.amount.to_lowercase();\n let mut failed_to_parse = false;\n\n \n let target_letters = ['k', 'm'];\n\n 'starter: for letter in target_letters {\n \n let count = user_amount.chars().filter(|c| c == &letter).count();\n\n for _ in 0..count {\n \n let amount_vec: Vec = user_amount.chars().collect();\n\n \n let target_index = user_amount.find(letter).unwrap();\n let mut gathered_value = String::new();\n\n \n \n \n let mut ending_index = 0;\n\n for index in (0..target_index).rev() {\n let value = amount_vec[index];\n if value == ' ' {\n continue;\n }\n\n if value == '.' {\n gathered_value = format!(\"{value}{gathered_value}\");\n continue;\n }\n\n \n if let Ok(num_amount) = value.to_string().parse::() {\n gathered_value = format!(\"{num_amount}{gathered_value}\");\n } else {\n \n \n ending_index = index + 1;\n break;\n }\n }\n\n \n \n for value in amount_vec\n .iter()\n .take(user_amount.len())\n .skip(target_index + 1)\n {\n if *value == ' ' {\n continue;\n }\n\n if value.to_string().parse::().is_ok() {\n failed_to_parse = true;\n break 'starter;\n }\n break;\n }\n\n if let Ok(parsed_value) = gathered_value.parse::() {\n let suffixed_added_value = match letter {\n 'k' => parsed_value * 1_000.0,\n 'm' => parsed_value * 1_000_000.0,\n _ => unreachable!(),\n };\n\n \n \n \n user_amount = user_amount.replacen(\n &user_amount[ending_index..=target_index],\n &suffixed_added_value.to_string(),\n 1,\n );\n }\n }\n }\n\n if !failed_to_parse {\n self.amount = user_amount;\n }\n }", + "body_hash": "cd7a06dc6f80975692397a6fb257c47ea89a1c4846de7afba87c47c8543ab2c2" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::check_suffixes#free:120d3c2a2efc": { + "kind": "Free", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::check_suffixes#free:120d3c2a2efc", + "comment_kind": "Line", + "comment_text": "// e.g. `100 + 5k`: the target index is the index of `k` and\n// `ending_index` is the index of `+` + 1, so everything in\n// between is replaced with the expanded number", + "comment_hash": "120d3c2a2efc3f24da02c8a65a07498543cfca1ee69267908c69e8373d550c18", + "body_text": "user_amount = user_amount.replacen(\n &user_amount[ending_index..=target_index],\n &suffixed_added_value.to_string(),\n 1,\n );\n}", + "body_hash": "24609c22313f784e6ab174b68618199ee738c47a4664d4eb5497e454bc65500c" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::check_suffixes#free:21c519847750": { + "kind": "Free", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::check_suffixes#free:21c519847750", + "comment_kind": "Line", + "comment_text": "// If the char is a number, convert to u16 and save it on gathered value", + "comment_hash": "21c5198477508f33c08d2d0a739666c1c9ab78a27571f13559a6fedbde688cdf", + "body_text": "if let Ok(num_amount) = value.to_string().parse::() {} else {}\n}", + "body_hash": "dc5c2da55329e6ce24226b57c34ed056b09e3d0f5879de45256c05d07f66b70e" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::check_suffixes#free:2bfe593c4ceb": { + "kind": "Free", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::check_suffixes#free:2bfe593c4ceb", + "comment_kind": "Line", + "comment_text": "// Convert the string to a vec of char for easier indexing", + "comment_hash": "2bfe593c4ceb6299c523a3a6b984a062f6511e911a1fa9aea767f81beb0916fe", + "body_text": "let amount_vec: Vec = user_amount.chars().collect();", + "body_hash": "35a86a23f8506c2cd897bb2f034d3eed5d26cd20c0d2557007b7398850bbcdfa" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::check_suffixes#free:612642e00e23": { + "kind": "Free", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::check_suffixes#free:612642e00e23", + "comment_kind": "Line", + "comment_text": "// The index where the 'k' or 'm' is in the string", + "comment_hash": "612642e00e23df1b150b890677fd29830572dc826ba5a5fc623e1916521c03f4", + "body_text": "let target_index = user_amount.find(letter).unwrap();\nlet mut gathered_value = String::new();", + "body_hash": "15ccbc8b46bda6a6cdd7a769c6a7b3c0f5297f13d3a8d2e79a5c6a2a5f663c29" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::check_suffixes#free:813046256174": { + "kind": "Free", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::check_suffixes#free:813046256174", + "comment_kind": "Line", + "comment_text": "// A digit right after the suffix (`1k1`, `5m12`) is invalid, so give\n// up and leave the amount untouched", + "comment_hash": "8130462561741602d882ff468ba394b27ec1b4c30e8761b566fb9e4358cd08cd", + "body_text": "for value in amount_vec\n .iter()\n .take(user_amount.len())\n .skip(target_index + 1)\n {}\nif let Ok(parsed_value) = gathered_value.parse::() {}\n}", + "body_hash": "d74e270c07a5eb5ae86e7f21e531b954bd7da4d3c46a922a26e36e87922d0f2c" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::check_suffixes#free:88cf9ec20b2a": { + "kind": "Free", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::check_suffixes#free:88cf9ec20b2a", + "comment_kind": "Line", + "comment_text": "// One replacement per occurrence, so this runs `count` times", + "comment_hash": "88cf9ec20b2aec02ff9827a543446191ab74acc41a61b9db599894b244dd4335", + "body_text": "let count = user_amount.chars().filter(|c| c == &letter).count();\nfor _ in 0..count {}\n}", + "body_hash": "452e18da03b8f63315c6b33cd6b4acd5129be30c3f97f34924097bf9e76f4be7" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::check_suffixes#free:8bd2b034468e": { + "kind": "Free", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::check_suffixes#free:8bd2b034468e", + "comment_kind": "Line", + "comment_text": "// Walk left from the suffix to collect the number it applies to.\n// `ending_index` marks the char right after that number, so the\n// suffix can be replaced without touching the rest of the string", + "comment_hash": "8bd2b034468e6af8168562847e9bee4cec7a2a4b86c91be5210d5a670514744b", + "body_text": "let mut ending_index = 0;\nfor index in (0..target_index).rev() {}", + "body_hash": "9e9f15d5ef5e90336cecb86b1ee6c7712d3601ed89113f7d60d6c5784cdd75d6" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::check_suffixes#free:b06db63368e8": { + "kind": "Free", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::check_suffixes#free:b06db63368e8", + "comment_kind": "Line", + "comment_text": "// target char list", + "comment_hash": "b06db63368e86b999008669e84d845259869f108c48198798eab82f535a3cf5a", + "body_text": "let target_letters = ['k', 'm'];\n'starter: for letter in target_letters {}\nif !failed_to_parse {}\n}", + "body_hash": "bf5317c58238eb8eb2afa8fb18d1d43c1b1d2bb8da08302e65d72d2ae84927dc" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::check_suffixes#free:c6c1a2015270": { + "kind": "Free", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::check_suffixes#free:c6c1a2015270", + "comment_kind": "Line", + "comment_text": "// e.g. `100 + 5k`: stop at the `+` and set `ending_index`\n// to the char right after it, so the `+` itself is kept", + "comment_hash": "c6c1a2015270b703989aeb232243a99b6b87449a30d80f8ff21aa09997b90047", + "body_text": "ending_index = index + 1;\nbreak;\n}", + "body_hash": "1c3be93c2681f5d562811aacba3a8d4533400b9b8ced2198622883a713b38336" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::check_tags": { + "kind": "Function", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::check_tags", + "comment_kind": "OuterDoc", + "comment_text": " Trims and de-duplicates the inputted tags, keeping unknown ones\n", + "comment_hash": "6ff310dfa45ea42148fc318bcc85313a0571744145e290f40e4a0c68a51f10e0", + "body_text": "pub fn check_tags(&mut self, conn: &mut DbConn) {\n let mut tags = self.tags.clone();\n\n conn.verify().tags(&mut tags);\n\n self.tags = tags;\n self.go_current_index(&TxTab::Tags);\n }", + "body_hash": "98ab5583826f6f647e6ede943aeedcddd8b0af40003b64f107d441c96cea00ae" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::check_tags_forced": { + "kind": "Function", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::check_tags_forced", + "comment_kind": "OuterDoc", + "comment_text": " Same as [`Self::check_tags`], but tags that don't exist in the database are\n\n dropped and reported back as an error\n", + "comment_hash": "92965c9b91ce396adeddb4d437535d9a60f110600f8a901a36a5855ba5cfbe2a", + "body_text": "pub fn check_tags_forced(&mut self, conn: &mut DbConn) -> Result {\n let mut tags = self.tags.clone();\n\n let status = conn.verify().tags_forced(&mut tags);\n\n self.tags = tags;\n self.go_current_index(&TxTab::Tags);\n status\n }", + "body_hash": "9e07002f0feb71594150e7a9908fe97c1db7ccca954d32b608569309b30e0ed6" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::check_to_method": { + "kind": "Function", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::check_to_method", + "comment_kind": "OuterDoc", + "comment_text": " Checks the inputted To Method by the user upon pressing Enter/Esc for various error.\n", + "comment_hash": "32451a00837bb59b63131116a0410152c9ca89f36e39c84aeb375966f82eb9f4", + "body_text": "pub fn check_to_method(&mut self, conn: &mut DbConn) -> Result {\n let mut current_method = self.to_method.clone();\n\n let status = conn.verify().tx_method(&mut current_method);\n\n self.to_method = current_method;\n self.go_current_index(&TxTab::ToMethod);\n status\n }", + "body_hash": "c0c3b542d9366c99ffb8aebc84405af52a3f3bf744d4e50b33eb425c62bfafe4" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::check_tx_type": { + "kind": "Function", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::check_tx_type", + "comment_kind": "OuterDoc", + "comment_text": " Checks the inputted Transaction Type by the user upon pressing Enter/Esc for various error.\n", + "comment_hash": "d57b0d7161fe960e4c9f7bb381b270bbee306b06b21aa77fae9157fa333a4af1", + "body_text": "pub fn check_tx_type(&mut self, conn: &mut DbConn) -> Result {\n let mut tx_type = self.tx_type.clone();\n\n let status = conn.verify().tx_type(&mut tx_type);\n\n self.tx_type = tx_type;\n self.go_current_index(&TxTab::TxType);\n status\n }", + "body_hash": "ee089863fec23369b9b6c1d0a1631a9ba4ff9e4b6a73d0922b70e5813aeb6021" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::custom": { + "kind": "Function", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::custom", + "comment_kind": "OuterDoc", + "comment_text": " Builds a `TxData` with only the given fields prefilled. Currently used to\n\n open the Search page with a single tag already filled in.\n\n `date` is expected as `DD-MM-YYYY` and gets stored as `YYYY-MM-DD`.\n", + "comment_hash": "bb82aa69e04ce3440c606ab867194c8dd596e686d901f2dddc047c38cc59b671", + "body_text": "#[must_use]\npub fn custom(\n date: &str,\n details: &str,\n from_method: &str,\n to_method: &str,\n amount: &str,\n tx_type: &str,\n tags: &str,\n id_num: i32,\n ) -> Self {\n let new_date = if date.is_empty() {\n String::new()\n } else {\n let data = date.split('-').collect::>();\n let year = data[2];\n let month = data[1];\n let day = data[0];\n format!(\"{year}-{month}-{day}\")\n };\n\n TxData {\n date: new_date,\n details: details.to_string(),\n from_method: from_method.to_string(),\n to_method: to_method.to_string(),\n amount: amount.to_string(),\n tx_type: tx_type.to_string(),\n tags: tags.to_string(),\n frequency: String::new(),\n recur_interval: String::new(),\n recur_value: String::new(),\n recur_month: String::new(),\n end_date: String::new(),\n tx_status: Vec::new(),\n editing_tx: true,\n id_num,\n current_index: 0,\n autofill: String::new(),\n from_search: true,\n }\n }", + "body_hash": "7de8bd42550ba4ff68d9b7930511ceb76703437f4545893576a3ed0b4993c8c6" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::edit_amount": { + "kind": "Function", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::edit_amount", + "comment_kind": "OuterDoc", + "comment_text": " Insert or remove from amount field according to the index point\n", + "comment_hash": "f13f257d688ba81d401d19e69b8f94f3a7710ba184ed016e76bc0b36e5004f17", + "body_text": "pub fn edit_amount(&mut self, to_add: Option) {\n add_char_to(to_add, &mut self.current_index, &mut self.amount);\n }", + "body_hash": "a41af0cea78b571e69daa06fc71822f8e6a474f11238921b0c936dbc167e2383" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::edit_date": { + "kind": "Function", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::edit_date", + "comment_kind": "OuterDoc", + "comment_text": " Insert or remove from date field according to the index point\n", + "comment_hash": "5b517d71417ade68365c7588bf7d809f84fe5e6e08283e0fd73475601574a45a", + "body_text": "pub fn edit_date(&mut self, to_add: Option) {\n add_char_to(to_add, &mut self.current_index, &mut self.date);\n }", + "body_hash": "140bb9588c46e9518817809bca04f3fc3ae8a2e038aa52616e228aac9567dc7d" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::edit_details": { + "kind": "Function", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::edit_details", + "comment_kind": "OuterDoc", + "comment_text": " Insert or remove from details field according to the index point\n", + "comment_hash": "a07c28bb3d5e85ab639d72108337cac5d31dcee2a9a63ec626841d454b739c76", + "body_text": "pub fn edit_details(&mut self, to_add: Option) {\n add_char_to(to_add, &mut self.current_index, &mut self.details);\n }", + "body_hash": "df8f162a6cf5c2759ddfe297bbb0d99b5afb7cc92a472d2ddbe7b255ca7f7c09" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::edit_end_date": { + "kind": "Function", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::edit_end_date", + "comment_kind": "OuterDoc", + "comment_text": " Insert or remove from end date field according to the index point\n", + "comment_hash": "3269e34631e2b43f4bb0e10e0cd572920dca8c884cf9e0b43aa0a90f7b876a8c", + "body_text": "pub fn edit_end_date(&mut self, to_add: Option) {\n add_char_to(to_add, &mut self.current_index, &mut self.end_date);\n }", + "body_hash": "97bd6048dd5f06eaca3dc02647d4da82acf55882d506256901079c7a734028e4" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::edit_frequency": { + "kind": "Function", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::edit_frequency", + "comment_kind": "OuterDoc", + "comment_text": " Insert or remove from frequency field according to the index point\n", + "comment_hash": "dde34f615bc80d147e3af1a320fe5911d3074567cb5294e4a1b4567904d8a240", + "body_text": "pub fn edit_frequency(&mut self, to_add: Option) {\n add_char_to(to_add, &mut self.current_index, &mut self.frequency);\n }", + "body_hash": "e45dfd38a475e22607bb577e0abab9517fab228e2c2b718db43777eb22fbf367" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::edit_from_method": { + "kind": "Function", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::edit_from_method", + "comment_kind": "OuterDoc", + "comment_text": " Insert or remove from method field according to the index point\n", + "comment_hash": "605c5161e4de4e8f1a61bb3e9befdfdd973fa9c7d56d000a91874e7562e8946a", + "body_text": "pub fn edit_from_method(&mut self, to_add: Option) {\n add_char_to(to_add, &mut self.current_index, &mut self.from_method);\n }", + "body_hash": "2248fda6231c491e4189d5858535a50b4cadb7ae16768454a5bbf0b794535d8d" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::edit_recur_interval": { + "kind": "Function", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::edit_recur_interval", + "comment_kind": "OuterDoc", + "comment_text": " Insert or remove from recur interval field according to the index point\n", + "comment_hash": "b63928f412df37547dadae6c7c1eafc3e811fbb0943f61d4b9f53101dd54380a", + "body_text": "pub fn edit_recur_interval(&mut self, to_add: Option) {\n add_char_to(to_add, &mut self.current_index, &mut self.recur_interval);\n }", + "body_hash": "cbdcfa668e3903dd8c9859f57e87234f5491a6c76b5927d77470342ac21a5fe3" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::edit_recur_month": { + "kind": "Function", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::edit_recur_month", + "comment_kind": "OuterDoc", + "comment_text": " Insert or remove from recur month field according to the index point\n", + "comment_hash": "73c0176ef713bc765b21eb8e7bc8d14766bf8a7306cfe4dea117da9d362c1224", + "body_text": "pub fn edit_recur_month(&mut self, to_add: Option) {\n add_char_to(to_add, &mut self.current_index, &mut self.recur_month);\n }", + "body_hash": "786cda58c7b5d9ea01af53eadf68f08700d76615895f188c2669b67764100100" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::edit_recur_value": { + "kind": "Function", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::edit_recur_value", + "comment_kind": "OuterDoc", + "comment_text": " Insert or remove from recur value field according to the index point\n", + "comment_hash": "6257e76fc68c7fda7fdab6e62113ce094e4aeb99570ab6adfbf6ebe42eaf4aa5", + "body_text": "pub fn edit_recur_value(&mut self, to_add: Option) {\n add_char_to(to_add, &mut self.current_index, &mut self.recur_value);\n }", + "body_hash": "c09213691242f1f46143a2a8cef2ab8a8e21acf0ef24e6e930f10dbd1d2c9474" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::edit_tags": { + "kind": "Function", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::edit_tags", + "comment_kind": "OuterDoc", + "comment_text": " Insert or remove from tags field according to the index point\n", + "comment_hash": "77a8f025401ccf067b642822d6fce228f442d1add9154906c828f193c2de1db9", + "body_text": "pub fn edit_tags(&mut self, to_add: Option) {\n add_char_to(to_add, &mut self.current_index, &mut self.tags);\n }", + "body_hash": "bda459a0b5d36e77162c63afd94bb1fb419b57f90f76bb82a83161aca8220ac9" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::edit_to_method": { + "kind": "Function", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::edit_to_method", + "comment_kind": "OuterDoc", + "comment_text": " Insert or remove from to method field according to the index point\n", + "comment_hash": "29e709246b8f9a3923e1de4e3d2c784b5a66d317265a657b6fddb07c55671a29", + "body_text": "pub fn edit_to_method(&mut self, to_add: Option) {\n add_char_to(to_add, &mut self.current_index, &mut self.to_method);\n }", + "body_hash": "601315a53e9d15406cfcd7f7fb3afa65a5e668de4abbea8fdf04eec686ca307e" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::edit_tx_type": { + "kind": "Function", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::edit_tx_type", + "comment_kind": "OuterDoc", + "comment_text": " Insert or remove from tx type field according to the index point\n", + "comment_hash": "39bcb67ddd1a64bb36069f3676ac7da0bbb2c751b1c194b1a2721d894fe43b0e", + "body_text": "pub fn edit_tx_type(&mut self, to_add: Option) {\n add_char_to(to_add, &mut self.current_index, &mut self.tx_type);\n }", + "body_hash": "3e84ae9ab192663d177fdaff548a987e6300888a85523d9ee3f9237a4d7d6bd2" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::from_full_recurring_tx": { + "kind": "Function", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::from_full_recurring_tx", + "comment_kind": "OuterDoc", + "comment_text": " Populates the Recurring page's form from an existing recurring rule, for editing.\n", + "comment_hash": "bcd7da5ce72347374d3c1009d547120d18266b4210a4095d1aa20ebc704df098", + "body_text": "#[must_use]\npub fn from_full_recurring_tx(tx: &FullRecurringTx, edit: bool) -> Self {\n let recur_value = match tx.frequency {\n RecurrenceFrequency::Daily => String::new(),\n RecurrenceFrequency::Weekly => tx\n .recur_value\n .and_then(|v| num_to_weekday_name(v).ok())\n .unwrap_or_default()\n .to_string(),\n RecurrenceFrequency::Monthly | RecurrenceFrequency::Yearly => {\n tx.recur_value.map(|v| v.to_string()).unwrap_or_default()\n }\n };\n\n let recur_month = tx\n .recur_month\n .and_then(|m| num_to_month_name(m as u32).ok())\n .unwrap_or_default()\n .to_string();\n\n Self {\n date: tx.next_recurring_date.format(\"%Y-%m-%d\").to_string(),\n details: tx.details.clone().unwrap_or_default(),\n from_method: tx.from_method.name.clone(),\n to_method: tx.to_method.clone().map(|t| t.name).unwrap_or_default(),\n amount: format!(\"{:.2}\", tx.amount.dollar()),\n tx_type: tx.tx_type.to_string(),\n tags: tx\n .tags\n .iter()\n .map(|t| t.name.as_str())\n .collect::>()\n .join(\", \"),\n frequency: tx.frequency.to_string(),\n recur_interval: tx.recur_interval.to_string(),\n recur_value,\n recur_month,\n end_date: tx\n .end_date\n .map(|d| d.format(\"%Y-%m-%d\").to_string())\n .unwrap_or_default(),\n tx_status: Vec::new(),\n editing_tx: edit,\n id_num: tx.id,\n current_index: 0,\n autofill: String::new(),\n from_search: false,\n }\n }", + "body_hash": "368c83be8ae0afccc5d136d361ee587404d8eb86da87f0e0d7840b120796b780" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::generate_balance_section": { + "kind": "Function", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::generate_balance_section", + "comment_kind": "OuterDoc", + "comment_text": " Generate Add Transaction page Balance section's balance data based on what fields are filled up\n", + "comment_hash": "1f555e73d1b7155788b252fb22c7bc2f55ad7cb68c4d7db798f50606a2f0d931", + "body_text": "pub fn generate_balance_section(\n &self,\n tx_view: &TxViewGroup,\n index: Option,\n conn: &mut DbConn,\n ) -> AResult>> {\n if self.generation_fields_exists() {\n let partial_tx = PartialTx {\n from_method: &self.from_method,\n to_method: &self.to_method,\n amount: &self.amount,\n tx_type: &self.tx_type,\n };\n\n tx_view.add_tx_balance_array(index, Some(partial_tx), conn)\n } else {\n tx_view.add_tx_balance_array(index, None, conn)\n }\n }", + "body_hash": "991c3d91a054f766599ceda0943d70e2ca3f1dcc97541925df143d7ffad49576" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::generation_fields_exists": { + "kind": "Function", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::generation_fields_exists", + "comment_kind": "OuterDoc", + "comment_text": " Whether the required fields for balance section data generate is filled up\n", + "comment_hash": "bcfd9bc4f42bf7d8ea44e2750d45d019866445f33f0a8d522c98aa48d9b32649", + "body_text": "fn generation_fields_exists(&self) -> bool {\n if self.amount.is_empty() {\n return false;\n }\n\n if self.tx_type.is_empty() {\n return false;\n }\n\n if self.from_method.is_empty() {\n return false;\n }\n\n if self.tx_type == \"Transfer\" && self.to_method.is_empty() {\n return false;\n }\n true\n }", + "body_hash": "29789f8f2d020d1fa3c8b745113470569fa22cc6c2c56e6d6fc608ecd53cca46" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::get_all_texts": { + "kind": "Function", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::get_all_texts", + "comment_kind": "OuterDoc", + "comment_text": " Returns the Add TX page's text fields in widget order, autofill included\n", + "comment_hash": "eae7f129499ee8dba365782a9f2b61304e8bef95914ae46201072f9aac230ed2", + "body_text": "#[must_use]\npub fn get_all_texts(&self) -> Vec<&str> {\n vec![\n &self.date,\n &self.details,\n &self.from_method,\n &self.to_method,\n &self.amount,\n &self.tx_type,\n &self.tags,\n &self.autofill,\n ]\n }", + "body_hash": "3e47fecaf1c20874e30560afe6b21791a009dc5c4195e09d19280f91084aca76" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::get_current_index": { + "kind": "Function", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::get_current_index", + "comment_kind": "OuterDoc", + "comment_text": " Returns the current index\n", + "comment_hash": "bd3c8e360b21189c4b27258981766a35319640b43d510e19c4cbc6350b3eb4a9", + "body_text": "#[must_use]\npub fn get_current_index(&self) -> usize {\n self.current_index\n }", + "body_hash": "97a3cb080bee48b83541abddd4c6300ff60dbf96fecacf5abeab1dac68492964" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::get_data_len": { + "kind": "Function", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::get_data_len", + "comment_kind": "OuterDoc", + "comment_text": " Returns the length of the data based on which `TxTab` is selected\n", + "comment_hash": "33c682ff98c417095886076248568364cdac76c2afadadfa8d246b6ec78f2f38", + "body_text": "fn get_data_len(&self, current_tab: &TxTab) -> usize {\n match current_tab {\n TxTab::Date => self.date.len(),\n TxTab::Details => self.details.len(),\n TxTab::FromMethod => self.from_method.len(),\n TxTab::ToMethod => self.to_method.len(),\n TxTab::Amount => self.amount.len(),\n TxTab::TxType => self.tx_type.len(),\n TxTab::Tags => self.tags.len(),\n TxTab::Frequency => self.frequency.len(),\n TxTab::RecurInterval => self.recur_interval.len(),\n TxTab::RecurValue => self.recur_value.len(),\n TxTab::RecurMonth => self.recur_month.len(),\n TxTab::EndDate => self.end_date.len(),\n TxTab::Nothing => 0,\n }\n }", + "body_hash": "3e07adb8323320682e32ef13ba157ff28397befc869baafaa0b847e531b2bd03" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::get_frequency": { + "kind": "Function", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::get_frequency", + "comment_kind": "OuterDoc", + "comment_text": " Same safe-default idea as `get_tx_type`: never panics on an empty/partial frequency\n\n field, defaulting to Daily.\n", + "comment_hash": "63783cf848eb9d37171ea1c4f88e224a82da664da593911b14e9f6518b3de60c", + "body_text": "#[must_use]\npub fn get_frequency(&self) -> RecurrenceFrequency {\n if let Some(first_letter) = self.frequency.chars().next() {\n match first_letter.to_ascii_lowercase() {\n 'd' => return RecurrenceFrequency::Daily,\n 'w' => return RecurrenceFrequency::Weekly,\n 'm' => return RecurrenceFrequency::Monthly,\n 'y' => return RecurrenceFrequency::Yearly,\n _ => {}\n }\n }\n RecurrenceFrequency::Daily\n }", + "body_hash": "b24a300338935b5c19aec2e38d6db15d368a83fa87526d180842767ec74ff687" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::get_recurring_texts": { + "kind": "Function", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::get_recurring_texts", + "comment_kind": "OuterDoc", + "comment_text": " Returns the Recurring page's own fields: frequency, recur interval, recur value,\n\n recur month and end date, in that order.\n", + "comment_hash": "1822b6f66909acb57e1de431b7b9af39fbaf2f71f8988ae5556671dc6471eb5f", + "body_text": "#[must_use]\npub fn get_recurring_texts(&self) -> Vec<&str> {\n vec![\n &self.frequency,\n &self.recur_interval,\n &self.recur_value,\n &self.recur_month,\n &self.end_date,\n ]\n }", + "body_hash": "6f6956eb3796c1f73eac9ceb96dacd7fd6d5c4c181bf7d52b8e9f81cf712fa3b" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::go_current_index": { + "kind": "Function", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::go_current_index", + "comment_kind": "OuterDoc", + "comment_text": " Set current index to max point based on `TxTab`\n", + "comment_hash": "9158456a685a168f2b0fbe1eaa7ddc1e54555ee0715944f74654c8a019c9cb65", + "body_text": "pub fn go_current_index(&mut self, current_tab: &TxTab) {\n self.current_index = self.get_data_len(current_tab);\n }", + "body_hash": "3e86c3f1faf40410b3b987a1456d4882052d7c6e3ac6ac2192c1db8093c79a09" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::move_index_left": { + "kind": "Function", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::move_index_left", + "comment_kind": "OuterDoc", + "comment_text": " Moves index by one value to left\n", + "comment_hash": "7bfe95e559843705937c14e6e38384aca1e97353ad11f9354c7a24c9c080eabf", + "body_text": "pub fn move_index_left(&mut self, current_tab: &TxTab) {\n let data_len = self.get_data_len(current_tab);\n\n if self.current_index > data_len {\n self.current_index = data_len;\n } else if self.current_index > 0 {\n self.current_index -= 1;\n }\n }", + "body_hash": "28dbc24130296d3c3473e9c1e87c0641ae31e13d003a20b95d5cac3f6788bd9b" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::move_index_right": { + "kind": "Function", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::move_index_right", + "comment_kind": "OuterDoc", + "comment_text": " Moves index by one value to right\n", + "comment_hash": "7ecb7588f9ee711d2908c2a4bffaa4b1c0fce0591eb281eeb38098cb2609ac43", + "body_text": "pub fn move_index_right(&mut self, current_tab: &TxTab) {\n let data_len = self.get_data_len(current_tab);\n\n match data_len.cmp(&self.current_index) {\n Ordering::Less => self.current_index = data_len,\n Ordering::Greater => self.current_index += 1,\n Ordering::Equal => {}\n }\n }", + "body_hash": "9fd0c4b4a4e578d9d29f93e878573e33518e7856204657a4fab117f8d2e5356d" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::new": { + "kind": "Function", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::new", + "comment_kind": "OuterDoc", + "comment_text": " All fields start out empty, except the date which is prefilled with the\n\n device's current local date\n", + "comment_hash": "ebbfb07ec5445d506064933e7dae24fcdf83d8c2db3f8fc47611cbdf84812fb0", + "body_text": "#[must_use]\npub fn new() -> Self {\n let current_date = Local::now().to_string();\n let formatted_current_date = ¤t_date[0..10];\n TxData {\n date: formatted_current_date.to_string(),\n details: String::new(),\n from_method: String::new(),\n to_method: String::new(),\n amount: String::new(),\n tx_type: String::new(),\n tags: String::new(),\n frequency: String::new(),\n recur_interval: String::new(),\n recur_value: String::new(),\n recur_month: String::new(),\n end_date: String::new(),\n tx_status: Vec::new(),\n editing_tx: false,\n id_num: 0,\n current_index: 0,\n autofill: String::new(),\n from_search: false,\n }\n }", + "body_hash": "ee9723a3ac45b5ff5d158747449d8cc12576a224f24d94e6378a4f4ff238d733" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::step_amount": { + "kind": "Function", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::step_amount", + "comment_kind": "OuterDoc", + "comment_text": " Steps Amount value by one\n", + "comment_hash": "6cc7740dccf0dd1619322758d521962be96f0fc8c42609e12ed21b527a2a1848", + "body_text": "pub fn step_amount(\n &mut self,\n is_search: bool,\n step_type: StepType,\n conn: &mut DbConn,\n ) -> Result<(), SteppingError> {\n if self.check_b_field(conn).is_err() {\n return Err(SteppingError::UnknownBValue);\n }\n\n let mut comparison_symbol = None;\n\n let mut user_amount = self.amount.clone();\n\n if is_search {\n match check_comparison(&user_amount) {\n ComparisonType::Equal => comparison_symbol = None,\n ComparisonType::BiggerThan => comparison_symbol = Some(\">\"),\n ComparisonType::SmallerThan => comparison_symbol = Some(\"<\"),\n ComparisonType::EqualOrBigger => comparison_symbol = Some(\">=\"),\n ComparisonType::EqualOrSmaller => comparison_symbol = Some(\"<=\"),\n }\n }\n\n if let Some(symbol) = comparison_symbol {\n user_amount = user_amount.replace(symbol, \"\");\n }\n\n let step_status = conn.step().amount(&mut user_amount, step_type);\n\n if let Some(symbol) = comparison_symbol {\n user_amount = format!(\"{symbol}{user_amount}\");\n }\n\n self.amount = user_amount;\n\n \n self.go_current_index(&TxTab::Amount);\n step_status\n }", + "body_hash": "6faf602506079ecf407e4d2793f6144beeaf9b70d0810ed6391e1fa39183088d" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::step_amount#free:c344aa877dbf": { + "kind": "Free", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::step_amount#free:c344aa877dbf", + "comment_kind": "Line", + "comment_text": "// Reload index to the final point as some data just got added/changed", + "comment_hash": "c344aa877dbf812b6e7e4d0274a75c708b504f2450a68aea7cf01616f23b0aae", + "body_text": "self.go_current_index(&TxTab::Amount);\nstep_status\n}", + "body_hash": "f704a359ea2a8a2005c8cb89ec27f21b4ee3298f5deed083363efce2f28b7794" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::step_date": { + "kind": "Function", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::step_date", + "comment_kind": "OuterDoc", + "comment_text": " Steps Date value by one\n", + "comment_hash": "8375fca899780cdc475ac5343dbbe379cb4fb96870fff81f4b0be498cb321151", + "body_text": "pub fn step_date(\n &mut self,\n date_type: DateType,\n step_type: StepType,\n conn: &mut DbConn,\n ) -> Result<(), SteppingError> {\n let mut user_date = self.date.clone();\n\n let step_status = conn.step().date(&mut user_date, step_type, date_type);\n self.date = user_date;\n\n \n self.go_current_index(&TxTab::Date);\n step_status\n }", + "body_hash": "debe5173b79ffa029647e6b2560d69b1432fe1ea8cf0ca8a454d6c6573378ee0" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::step_date#free:c344aa877dbf": { + "kind": "Free", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::step_date#free:c344aa877dbf", + "comment_kind": "Line", + "comment_text": "// Reload index to the final point as some data just got added/changed", + "comment_hash": "c344aa877dbf812b6e7e4d0274a75c708b504f2450a68aea7cf01616f23b0aae", + "body_text": "self.go_current_index(&TxTab::Date);\nstep_status\n}", + "body_hash": "e4defdb82bef35c357be813e800f41f9b44a5506986ef4eb166a4829a4992f42" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::step_end_date": { + "kind": "Function", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::step_end_date", + "comment_kind": "OuterDoc", + "comment_text": " Steps End Date value by one\n", + "comment_hash": "8668eaf18afde7f8fc525f1c6f439e4b7c97ec4590872dc3291865e56240a667", + "body_text": "pub fn step_end_date(\n &mut self,\n step_type: StepType,\n conn: &mut DbConn,\n ) -> Result<(), SteppingError> {\n let mut end_date = self.end_date.clone();\n\n let step_status = conn.step().date(&mut end_date, step_type, DateType::Exact);\n self.end_date = end_date;\n\n self.go_current_index(&TxTab::EndDate);\n step_status\n }", + "body_hash": "2d68712ef9c462e8f0483206eda02db105791aa1054074e99cfc1b3295c6be12" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::step_frequency": { + "kind": "Function", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::step_frequency", + "comment_kind": "OuterDoc", + "comment_text": " Steps Frequency value by one\n", + "comment_hash": "35f97dc3c4f3c00d2505c0b9784241d53f950588605d1a9e1c5c2e88979afeab", + "body_text": "pub fn step_frequency(\n &mut self,\n step_type: StepType,\n conn: &mut DbConn,\n ) -> Result<(), SteppingError> {\n let mut frequency = self.frequency.clone();\n\n let step_status = conn.step().frequency(&mut frequency, step_type);\n self.frequency = frequency;\n\n self.go_current_index(&TxTab::Frequency);\n step_status\n }", + "body_hash": "83069bb161cf3b5f180ad9327868b773e95291a0afa079e9942b6e949b770798" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::step_from_method": { + "kind": "Function", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::step_from_method", + "comment_kind": "OuterDoc", + "comment_text": " Steps From Method value by one\n", + "comment_hash": "d43cae6dc0722df0d069ade9da469de8d5b5c1dce2f34690d41fa407d5fd06be", + "body_text": "pub fn step_from_method(\n &mut self,\n step_type: StepType,\n conn: &mut DbConn,\n ) -> Result<(), SteppingError> {\n let mut user_method = self.from_method.clone();\n\n let step_status = conn.step().tx_method(&mut user_method, step_type);\n self.from_method = user_method;\n\n \n self.go_current_index(&TxTab::FromMethod);\n step_status\n }", + "body_hash": "e208b824f54ec72bf6a079aaee0278d6cba2b20776837ba27f889bab6bb77f27" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::step_from_method#free:c344aa877dbf": { + "kind": "Free", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::step_from_method#free:c344aa877dbf", + "comment_kind": "Line", + "comment_text": "// Reload index to the final point as some data just got added/changed", + "comment_hash": "c344aa877dbf812b6e7e4d0274a75c708b504f2450a68aea7cf01616f23b0aae", + "body_text": "self.go_current_index(&TxTab::FromMethod);\nstep_status\n}", + "body_hash": "6a0116d2c17aecfb1f43e749e671725def5cd605a07dcf573618ce8c97dcb071" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::step_recur_interval": { + "kind": "Function", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::step_recur_interval", + "comment_kind": "OuterDoc", + "comment_text": " Steps Recur Interval value by one\n", + "comment_hash": "4fc7ba49ccd885cad84271d5e3090e759ac142ea31da05e3ab447913030d340f", + "body_text": "pub fn step_recur_interval(\n &mut self,\n step_type: StepType,\n conn: &mut DbConn,\n ) -> Result<(), SteppingError> {\n let mut recur_interval = self.recur_interval.clone();\n\n let step_status = conn.step().recur_interval(&mut recur_interval, step_type);\n self.recur_interval = recur_interval;\n\n self.go_current_index(&TxTab::RecurInterval);\n step_status\n }", + "body_hash": "c000b7d0012057b1956f203be6b64e28ce642a548db74ffe427aea44ed00cadd" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::step_recur_month": { + "kind": "Function", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::step_recur_month", + "comment_kind": "OuterDoc", + "comment_text": " Steps Recur Month value by one\n", + "comment_hash": "3302a56f535fc14d30921e28a55fc4d037e76e6301487fa4297b55551c52ec7f", + "body_text": "pub fn step_recur_month(\n &mut self,\n step_type: StepType,\n conn: &mut DbConn,\n ) -> Result<(), SteppingError> {\n let mut recur_month = self.recur_month.clone();\n\n let step_status = conn.step().recur_month(&mut recur_month, step_type);\n self.recur_month = recur_month;\n\n self.go_current_index(&TxTab::RecurMonth);\n step_status\n }", + "body_hash": "b0f19f1d8405cd53a4fb314e1d177524779817cf91ae220d1c5f3eff768e6212" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::step_recur_value": { + "kind": "Function", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::step_recur_value", + "comment_kind": "OuterDoc", + "comment_text": " Steps Recur Value value by one\n", + "comment_hash": "2731f96efe6406f7ed11f86a8dd6d9fdd612be0a41e163c8f301dbd4fadf9d6b", + "body_text": "pub fn step_recur_value(\n &mut self,\n step_type: StepType,\n conn: &mut DbConn,\n ) -> Result<(), SteppingError> {\n let mut recur_value = self.recur_value.clone();\n let frequency = self.get_frequency();\n\n let step_status = conn\n .step()\n .recur_value(&mut recur_value, frequency, step_type);\n self.recur_value = recur_value;\n\n self.go_current_index(&TxTab::RecurValue);\n step_status\n }", + "body_hash": "35b56f249ae7ecd344795c911dd695cb286c4dc54d5b432bb2d72d1ff874033b" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::step_tags": { + "kind": "Function", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::step_tags", + "comment_kind": "OuterDoc", + "comment_text": " Steps up Tags value by one\n", + "comment_hash": "2cf35ede009132435e73709767a1699deca358dae6969514bab23ae0ce748b35", + "body_text": "pub fn step_tags(\n &mut self,\n step_type: StepType,\n conn: &mut DbConn,\n ) -> Result<(), SteppingError> {\n let mut user_tag = self.tags.clone();\n\n let status = conn.step().tag(&mut user_tag, step_type);\n self.tags = user_tag;\n\n \n self.go_current_index(&TxTab::Tags);\n status\n }", + "body_hash": "5ad219a32599bc8080c3ce91fb5219341f23b62b93491e20ed3376e6a050e7d1" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::step_tags#free:c344aa877dbf": { + "kind": "Free", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::step_tags#free:c344aa877dbf", + "comment_kind": "Line", + "comment_text": "// Reload index to the final point as some data just got added/changed", + "comment_hash": "c344aa877dbf812b6e7e4d0274a75c708b504f2450a68aea7cf01616f23b0aae", + "body_text": "self.go_current_index(&TxTab::Tags);\nstatus\n}", + "body_hash": "36e3cee7ccde642b64a5689e9479aa46ca37901c9ad4814498f16f7464d5fbda" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::step_to_method": { + "kind": "Function", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::step_to_method", + "comment_kind": "OuterDoc", + "comment_text": " Steps the To Method value by one\n", + "comment_hash": "d1e360f88e1fef79a0d50ab9f74e560ac683baadf84495e042a885b803735075", + "body_text": "pub fn step_to_method(\n &mut self,\n step_type: StepType,\n conn: &mut DbConn,\n ) -> Result<(), SteppingError> {\n let mut user_method = self.to_method.clone();\n\n let step_status = conn.step().tx_method(&mut user_method, step_type);\n self.to_method = user_method;\n\n \n self.go_current_index(&TxTab::ToMethod);\n step_status\n }", + "body_hash": "9be30737ebb8e33ae89632187cca6e5e5fd491c7bf3d405ee7ff893fbdc8f06b" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::step_to_method#free:c344aa877dbf": { + "kind": "Free", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::step_to_method#free:c344aa877dbf", + "comment_kind": "Line", + "comment_text": "// Reload index to the final point as some data just got added/changed", + "comment_hash": "c344aa877dbf812b6e7e4d0274a75c708b504f2450a68aea7cf01616f23b0aae", + "body_text": "self.go_current_index(&TxTab::ToMethod);\nstep_status\n}", + "body_hash": "ee373fbe2fe5770091b9342d2359e2a414e780631e47abda2cc6226c75cbfbbd" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::step_tx_type": { + "kind": "Function", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::step_tx_type", + "comment_kind": "OuterDoc", + "comment_text": " Steps Tx Type value by one\n", + "comment_hash": "ee92c2565fa699b7bbd09f376718d15b9c3695810b783e824d92dd79dfc1232e", + "body_text": "pub fn step_tx_type(\n &mut self,\n step_type: StepType,\n conn: &mut DbConn,\n ) -> Result<(), SteppingError> {\n let mut user_type = self.tx_type.clone();\n\n let step_status = conn.step().tx_type(&mut user_type, step_type);\n self.tx_type = user_type;\n\n \n self.go_current_index(&TxTab::TxType);\n step_status\n }", + "body_hash": "0e1eaa97563add66304335301b428b84a68ffc400d17f72ea010331603959add" + }, + "tui/src/tx_handler/tx_data.rs|crate::impl TxData::step_tx_type#free:c344aa877dbf": { + "kind": "Free", + "file": "tui/src/tx_handler/tx_data.rs", + "item_path": "crate::impl TxData::step_tx_type#free:c344aa877dbf", + "comment_kind": "Line", + "comment_text": "// Reload index to the final point as some data just got added/changed", + "comment_hash": "c344aa877dbf812b6e7e4d0274a75c708b504f2450a68aea7cf01616f23b0aae", + "body_text": "self.go_current_index(&TxTab::TxType);\nstep_status\n}", + "body_hash": "fa9c033c932a0e533568c8eb9545bc56bdeb793aedbe0b235b967d7fe9c823ac" + }, + "tui/src/utility/lerp.rs|crate::impl LerpState::lerp#free:025d5c2693f8": { + "kind": "Free", + "file": "tui/src/utility/lerp.rs", + "item_path": "crate::impl LerpState::lerp#free:025d5c2693f8", + "comment_kind": "Line", + "comment_text": "// Check if lerp has completed", + "comment_hash": "025d5c2693f83c0f92ea84efd7a2d9d15051848b928face0c044349e7238d793", + "body_text": "if clamped_t >= 1.0 && entry.active {}\nentry.current\n}", + "body_hash": "34df279feeeafd06de934b701c9686c85bddd0669821028bae9c473f3678bf70" + }, + "tui/src/utility/lerp.rs|crate::impl LerpState::lerp#free:3f9a017b4139": { + "kind": "Free", + "file": "tui/src/utility/lerp.rs", + "item_path": "crate::impl LerpState::lerp#free:3f9a017b4139", + "comment_kind": "Line", + "comment_text": "// Start a new lerp if the target has changed", + "comment_hash": "3f9a017b41398b78ef8f9f4d7e74d01db830b348baae3decaa83d6e121c036c0", + "body_text": "if (entry.target - new_target).abs() > f64::EPSILON {}\nlet t = (now - entry.start_time).as_secs_f64() / duration.unwrap_or(self.duration_secs);\nlet clamped_t = t.min(1.0);\nentry.current = entry.start + (entry.target - entry.start) * clamped_t;", + "body_hash": "0589616a7d52d71f9454e3ada89f4a71848100c3d81d9403e4141274b80c2f33" + }, + "tui/src/utility/sub_func.rs|crate::start_terminal": { + "kind": "Function", + "file": "tui/src/utility/sub_func.rs", + "item_path": "crate::start_terminal", + "comment_kind": "OuterDoc", + "comment_text": " Tries to open terminal/cmd and run this app\n\n Currently supports windows cmd, konsole, gnome-terminal, kgx (also known as gnome-console)\n", + "comment_hash": "095d894031afec0cea2af2f54fb064fc26b1411479b0d436d253cc1f57f2874d", + "body_text": "pub fn start_terminal(original_dir: &str) -> bool {\n let mut all_terminals = HashMap::new();\n\n if cfg!(target_os = \"windows\") {\n all_terminals.insert(\"cmd.exe\", vec![\"start\".to_string(), \"rex\".to_string()]);\n } else {\n let gnome_dir = format!(\"--working-directory={original_dir}\");\n\n all_terminals.insert(\n \"konsole\",\n vec![\n \"--new-tab\".to_string(),\n \"--workdir\".to_string(),\n original_dir.to_string(),\n \"-e\".to_string(),\n \"./rex\".to_string(),\n ],\n );\n\n all_terminals.insert(\n \"gnome-terminal\",\n vec![\n gnome_dir.clone(),\n \"--maximize\".to_string(),\n \"--\".to_string(),\n \"./rex\".to_string(),\n ],\n );\n\n all_terminals.insert(\n \"kgx\",\n vec![gnome_dir, \"-e\".to_string(), \"./rex\".to_string()],\n );\n }\n\n let mut terminal_opened = false;\n\n for (key, value) in all_terminals {\n let status = Command::new(key).args(value).output();\n if let Ok(out) = status\n && !out.stderr.len() > 2\n {\n terminal_opened = true;\n break;\n }\n }\n\n terminal_opened\n}", + "body_hash": "78a91f2f356bda9d6fd3b4051a1fc35bca2465c3ff0ed054d5f1aa507bf3344d" + }, + "tui/src/utility/utils.rs|crate::add_char_to": { + "kind": "Function", + "file": "tui/src/utility/utils.rs", + "item_path": "crate::add_char_to", + "comment_kind": "OuterDoc", + "comment_text": " Adds a char to the given index on the given string\n", + "comment_hash": "aac66d8a4293e6bd868d0c699a5faf7b190aa39d5a3b5dca5dfd62dfec29c483", + "body_text": "pub fn add_char_to(to_add: Option, current_index: &mut usize, current_data: &mut String) {\n if *current_index > current_data.len() {\n *current_index = current_data.len();\n } else {\n match to_add {\n Some(ch) => {\n current_data.insert(*current_index, ch);\n *current_index += 1;\n }\n None => {\n if !current_data.is_empty() && *current_index != 0 {\n current_data.remove(*current_index - 1);\n *current_index -= 1;\n }\n }\n }\n }\n}", + "body_hash": "0526b7327f82fe16fbb526e22274c6642cf342f2b41ec2505d2dd8871cb1e25a" + }, + "tui/src/utility/utils.rs|crate::check_comparison": { + "kind": "Function", + "file": "tui/src/utility/utils.rs", + "item_path": "crate::check_comparison", + "comment_kind": "OuterDoc", + "comment_text": " Checks if the string contains any symbol indicating comparison\n", + "comment_hash": "85c8a125dc8a8cc27e771526f77c81a84d30eaec800cea707eb495d249b3fab4", + "body_text": "#[must_use]\npub fn check_comparison(input: &str) -> ComparisonType {\n \n \n if input.starts_with(\"<=\") {\n ComparisonType::EqualOrSmaller\n } else if input.starts_with(\">=\") {\n ComparisonType::EqualOrBigger\n } else if input.starts_with('<') {\n ComparisonType::SmallerThan\n } else if input.starts_with('>') {\n ComparisonType::BiggerThan\n } else {\n ComparisonType::Equal\n }\n}", + "body_hash": "34803d775a9bef522aaeb727b474d56d796c59fa129092f14d4187905a5a0e33" + }, + "tui/src/utility/utils.rs|crate::check_comparison#free:f8b1fff1d33f": { + "kind": "Free", + "file": "tui/src/utility/utils.rs", + "item_path": "crate::check_comparison#free:f8b1fff1d33f", + "comment_kind": "Line", + "comment_text": "// The 2 character symbols have to be checked first, otherwise `>=` would\n// match as `>`", + "comment_hash": "f8b1fff1d33fff94c15f08c58104c159c8237c7231e4749a72acc88de39bb94a", + "body_text": "if input.starts_with(\"<=\") {} else if input.starts_with(\">=\") {} else if input.starts_with('<') {} else if input.starts_with('>') {} else {}\n}", + "body_hash": "9615f9b878fef62f6f2c62904879e87801b52f17749d7b0a3eb8fcbb0d8bd4a5" + }, + "tui/src/utility/utils.rs|crate::create_bolded_text": { + "kind": "Function", + "file": "tui/src/utility/utils.rs", + "item_path": "crate::create_bolded_text", + "comment_kind": "OuterDoc", + "comment_text": " Bolds everything before the first `:` of each line, for rendering\n", + "comment_hash": "3d47de63cbf344f5d20babf4247ad740bf9a770677129fe959695986c15d7c5f", + "body_text": "#[must_use]\npub fn create_bolded_text(text: &str) -> Vec> {\n let mut text_data = Vec::new();\n\n for line in text.split('\\n') {\n let split_text = line.split_once(':');\n if let Some((first_part, rest)) = split_text {\n let first_data =\n Span::styled(first_part, Style::default().add_modifier(Modifier::BOLD));\n let rest_data = Span::from(format!(\":{rest}\"));\n text_data.push(Line::from(vec![first_data, rest_data]));\n } else {\n text_data.push(Line::from(vec![Span::from(line)]));\n }\n }\n\n text_data\n}", + "body_hash": "3c98bd55975e13e6e631198aea5184b438fa7c546d8758c1f103f30d8556df55" + }, + "tui/src/utility/utils.rs|crate::create_tab": { + "kind": "Function", + "file": "tui/src/utility/utils.rs", + "item_path": "crate::create_tab", + "comment_kind": "OuterDoc", + "comment_text": " Builds a tab widget out of the given indexed data for the UI\n", + "comment_hash": "ac62a6c3bf026d9aa469a4afe3f9a83b0df0e5041ff6231627e61b941ff2c96b", + "body_text": "#[must_use]\npub fn create_tab<'a>(data: &'a IndexedData, name: &'a str, theme: &'a Theme) -> Tabs<'a> {\n let titles: Vec = data\n .titles\n .iter()\n .map(|t| Line::from(vec![Span::styled(t, Style::default().fg(theme.text()))]))\n .collect();\n\n Tabs::new(titles)\n .block(styled_block(name, theme))\n .select(data.index)\n .style(Style::default().fg(theme.border()))\n .highlight_style(\n Style::default()\n .add_modifier(Modifier::BOLD)\n .bg(theme.selectable()),\n )\n}", + "body_hash": "08d52f373df207d716af4c717e7f3f7932a9f3555465bbbfe7d171723c850ea9" + }, + "tui/src/utility/utils.rs|crate::create_tab_activation": { + "kind": "Function", + "file": "tui/src/utility/utils.rs", + "item_path": "crate::create_tab_activation", + "comment_kind": "OuterDoc", + "comment_text": " Same as [`create_tab`], except every title is colored by the bool the\n\n provided `HashMap` holds for it\n", + "comment_hash": "09fe8fb92c41fcda226eb3485efb1489bd47104a59f4c55138a1c64b2fd1a54f", + "body_text": "#[must_use]\npub fn create_tab_activation<'a>(\n data: &'a IndexedData,\n name: &'a str,\n activation: &HashMap,\n theme: &'a Theme,\n) -> Tabs<'a> {\n let titles: Vec = data\n .titles\n .iter()\n .map(|t| {\n if activation[t] {\n Line::from(vec![Span::styled(t, Style::default().fg(theme.text()))])\n } else {\n Line::from(vec![Span::styled(t, Style::default().fg(theme.negative()))])\n }\n })\n .collect();\n\n Tabs::new(titles)\n .block(styled_block(name, theme))\n .select(data.index)\n .style(Style::default().fg(theme.border()))\n .highlight_style(Style::default())\n}", + "body_hash": "4bbe422c8469ac88678f8e1b5e113c173123e827409081bd49b98178cc58dabb" + }, + "tui/src/utility/utils.rs|crate::enter_tui_interface": { + "kind": "Function", + "file": "tui/src/utility/utils.rs", + "item_path": "crate::enter_tui_interface", + "comment_kind": "OuterDoc", + "comment_text": " Enters raw mode so the TUI can render properly\n", + "comment_hash": "f06d81669a4fe5880929521488f08dff1f9af22b1707b841425e20ff7e32a1a0", + "body_text": "pub fn enter_tui_interface() -> Result>> {\n enable_raw_mode()?;\n let mut stdout = stdout();\n execute!(stdout, EnterAlternateScreen)?;\n let backend = CrosstermBackend::new(stdout);\n let terminal = Terminal::new(backend)?;\n Ok(terminal)\n}", + "body_hash": "6c578fb07bebadf10802dc9d654a12d311f529b7009087121ed7974b53496674" + }, + "tui/src/utility/utils.rs|crate::exit_tui_interface": { + "kind": "Function", + "file": "tui/src/utility/utils.rs", + "item_path": "crate::exit_tui_interface", + "comment_kind": "OuterDoc", + "comment_text": " Exits raw mode so the terminal starts working normally\n", + "comment_hash": "c7fdae2110f5f1c84f2aa375a343dae1d08dbfab7b02b717b1b16b27159271ce", + "body_text": "pub fn exit_tui_interface() -> Result<()> {\n let stdout = stdout();\n let backend = CrosstermBackend::new(stdout);\n let mut terminal = Terminal::new(backend)?;\n\n execute!(terminal.backend_mut(), LeaveAlternateScreen)?;\n terminal.show_cursor()?;\n disable_raw_mode()?;\n Ok(())\n}", + "body_hash": "0b59110cc82706c424b8a6e64a5b2dcf3843ca51b0de8baa73f9cd2767afec7b" + }, + "tui/src/utility/utils.rs|crate::parse_github_body": { + "kind": "Function", + "file": "tui/src/utility/utils.rs", + "item_path": "crate::parse_github_body", + "comment_kind": "OuterDoc", + "comment_text": " Parse GitHub release information for popup menu\n", + "comment_hash": "470e558453807edb95a99e97a55303ca3b0abaf9fc3cf74be214a0fa5f46448c", + "body_text": "#[must_use]\npub fn parse_github_body(body: &str) -> String {\n let body = body.replace(\"## Updates\", \"\");\n let body = body.replace('*', \"•\");\n let body = body.replace('\\r', \"\");\n let end_point = body.find(\"## Changes\").unwrap();\n format!(\"\\n{}\\n\", body[..end_point].trim())\n}", + "body_hash": "8712a07c1862724fd8f40b64ee36d943c78da64073a8e07b880a8a3edb6bf03f" + }, + "tui/src/utility/utils.rs|crate::sort_table_data": { + "kind": "Function", + "file": "tui/src/utility/utils.rs", + "item_path": "crate::sort_table_data", + "comment_kind": "OuterDoc", + "comment_text": " Used for sorting summary table data\n", + "comment_hash": "0e16b280bac9bf9b7e91a855d2a52d3556688ad06c71d4a8064f7c9384756cad", + "body_text": "#[must_use]\npub fn sort_table_data(mut data: Vec>, sort_type: &SortingType) -> Vec> {\n match sort_type {\n SortingType::Tags => data.sort(),\n SortingType::Income => data.sort_by(|a, b| {\n let val_a: f64 = a[1].parse().unwrap();\n let val_b: f64 = b[1].parse().unwrap();\n val_b.partial_cmp(&val_a).unwrap()\n }),\n SortingType::Expense => {\n data.sort_by(|a, b| {\n let val_a: f64 = a[2].parse().unwrap();\n let val_b: f64 = b[2].parse().unwrap();\n val_b.partial_cmp(&val_a).unwrap()\n });\n }\n }\n\n data\n}", + "body_hash": "27b49ee1a3748b452ee5b765dce79c7cc27ffad560ab27055187a01f7f14a034" + }, + "tui/src/utility/utils.rs|crate::styled_block": { + "kind": "Function", + "file": "tui/src/utility/utils.rs", + "item_path": "crate::styled_block", + "comment_kind": "OuterDoc", + "comment_text": " Returns a styled block for UI to use\n", + "comment_hash": "30d3179002be06f8b858153f1512b5a616af7deac5013d3c0f4c86cbf1ed6e38", + "body_text": "#[must_use]\npub fn styled_block<'a>(title: &'a str, theme: &'a Theme) -> Block<'a> {\n Block::default()\n .borders(Borders::ALL)\n .border_type(BorderType::Rounded)\n .style(Style::default().bg(theme.background()).fg(theme.border()))\n .title(Span::styled(\n title,\n Style::default().add_modifier(Modifier::BOLD),\n ))\n}", + "body_hash": "c490531ec5392c7af4c988037c2172994a2dab8bd080787868f88ac333e583a2" + }, + "tui/src/utility/version_checker.rs|crate::check_version": { + "kind": "Function", + "file": "tui/src/utility/version_checker.rs", + "item_path": "crate::check_version", + "comment_kind": "OuterDoc", + "comment_text": " Uses GitHub API to get the latest release version number to check if the current version matches with it.\n", + "comment_hash": "5e569c588cfebc62655e6afa5d3ba17d6965734053dd7210d6e195d96da72bfb", + "body_text": "pub fn check_version() -> Result>> {\n let current_version = Version::parse(env!(\"CARGO_PKG_VERSION\"))?;\n\n let client = reqwest::blocking::Client::builder()\n .user_agent(\"Rex\")\n .connect_timeout(Duration::from_secs(1))\n .timeout(Duration::from_secs(2))\n .build()?;\n\n let caller: GithubRelease = client\n .get(\"https://api.github.com/repos/TheRustyPickle/Rex/releases/latest\")\n .send()?\n .json()?;\n\n let github_version = Version::parse(&caller.name.replace('v', \"\"))?;\n\n if github_version > current_version {\n let updates = parse_github_body(&caller.body);\n Ok(Some(vec![caller.name, updates]))\n } else {\n Ok(None)\n }\n}", + "body_hash": "60b7a0af3c27a4bfb0d0555f2dfe70024666e134019bfeaff445512adc51afe4" + } + } +} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 9b9ddf2a..1bf8174f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1395,9 +1395,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" [[package]] name = "lru" @@ -2184,6 +2184,7 @@ dependencies = [ "anyhow", "chrono", "diesel", + "log", "rex-db", "rex-shared", "strsim", @@ -2222,6 +2223,7 @@ dependencies = [ "chrono", "crossterm", "dirs", + "log", "open", "ratatui", "reqwest", @@ -2231,6 +2233,7 @@ dependencies = [ "semver", "serde", "serde_json", + "simplelog", "strum", "strum_macros", "thousands", @@ -2561,6 +2564,17 @@ dependencies = [ "libc", ] +[[package]] +name = "simplelog" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16257adbfaef1ee58b1363bdc0664c9b8e1e30aed86049635fb5f147d065a9c0" +dependencies = [ + "log", + "termcolor", + "time", +] + [[package]] name = "siphasher" version = "1.0.2" @@ -2709,6 +2723,15 @@ dependencies = [ "libc", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "termina" version = "0.3.3" diff --git a/README.md b/README.md index fc68fc3e..a6e970ec 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -

Rex

+ +

Rex

wakatime @@ -15,12 +16,15 @@ Rex is a terminal user interface app for managing incomes, expenses, and transac

Key Features

* Easily view, add, edit, and delete transactions. -* Navigate through transactions and instantly observe balance changes after each transaction. +* Navigate through transactions and instantly observe balance changes after +each transaction. * Chart for visualizing balance changes over a specific month, year, or all transactions. -* Access a summary with key insights and information on income, expense, and percentage distribution. +* Access a summary with key insights and information on income, expense, and +percentage distribution. * Built using SQLite database and keeps everything local. * Find transactions quickly using partial or specific information. * Organize transactions with custom tags for easy filtering. +* Add recurring transactions with flexible interval rules. * Works fully offline.

Installation

@@ -39,10 +43,11 @@ cargo run --release **2. Run the Latest Release:** * Download the latest executable from [Release](https://github.com/TheRustyPickle/Rex/releases/latest). - * Open terminal/CMD and start the program by the command `./rex` or `rex` depending on the OS at the correct directory. - + * Open terminal/CMD and start the program by the command `./rex` or `rex` + depending on the OS at the correct directory. + or - + * Double-click the executable which will try to open a terminal/CMD by itself. **3. Install from Cargo:** @@ -52,7 +57,8 @@ cargo run --release **4. Install using a package manager:** -* On NetBSD a package is available from the [official repositories](https://pkgsrc.se/finance/rex). To install it simply run: +* On NetBSD a package is available from the +[official repositories](https://pkgsrc.se/finance/rex). To install it simply run: ```sh pkgin install rex @@ -60,7 +66,9 @@ pkgin install rex

App Data Location

-See [here](https://docs.rs/dirs/latest/dirs/fn.data_local_dir.html) to learn about the initial directory where Rex data gets saved which is determined based on the OS. +See [here](https://docs.rs/dirs/latest/dirs/fn.data_local_dir.html) to learn +about the initial directory where Rex data gets saved which is determined based +on the OS.

Feedback & Bug Reports

diff --git a/app/Cargo.toml b/app/Cargo.toml index 13ba2bf9..32306aad 100644 --- a/app/Cargo.toml +++ b/app/Cargo.toml @@ -13,6 +13,7 @@ license = "MIT" anyhow.workspace = true chrono.workspace = true diesel.workspace = true +log = "0.4.34" rex-db.workspace = true rex-shared.workspace = true strsim = "0.11.1" @@ -20,5 +21,6 @@ strum.workspace = true strum_macros.workspace = true thiserror = "2.0.17" + [lints.clippy] too_many_arguments = { level = "allow" } diff --git a/app/src/conn.rs b/app/src/conn.rs index 8860b6b6..d71e43cf 100644 --- a/app/src/conn.rs +++ b/app/src/conn.rs @@ -1,14 +1,15 @@ use anyhow::{Error, Result}; -use chrono::NaiveDate; +use chrono::{Local, NaiveDate}; use diesel::{Connection, SqliteConnection}; -pub use rex_db::models::FetchNature; -use rex_db::models::{Balance, FullTx, NewSearch, NewTx, Tag, Tx, TxMethod}; +use rex_db::models::{Balance, FullTx, NewSearch, NewTx, RecurringTx, Tag, Tx, TxMethod}; +pub use rex_db::models::{FetchNature, FullRecurringTx, NewRecurringTx, RecurrenceFrequency}; use rex_db::{Cache, ConnCache, get_connection, get_connection_no_migrations}; use std::collections::{HashMap, HashSet}; use crate::modifier::{ activity_delete_tx, activity_edit_tx, activity_new_tx, activity_search_tx, - activity_swap_position, add_new_tx, add_new_tx_methods, delete_tx, + activity_swap_position, add_new_recurring_tx, add_new_tx, add_new_tx_methods, advance_date, + delete_recurring_tx, delete_tx, edit_recurring_tx, process_one_due_tx, }; use crate::ui_helper::{Autofiller, Stepper, Verifier}; use crate::utils::parse_month_year; @@ -181,6 +182,139 @@ impl DbConn { Ok(()) } + pub fn add_recurring_tx(&mut self, new: NewRecurringTx, tags: &str) -> Result<()> { + self.conn.transaction::<_, Error, _>(|conn| { + let mut db_conn = MutDbConn::new(conn, &self.cache); + + let new_tags = add_new_recurring_tx(new, tags, &mut db_conn)?; + + self.cache.new_tags(new_tags); + + Ok(()) + })?; + + self.process_due_recurring_txs()?; + + Ok(()) + } + + pub fn edit_recurring_tx(&mut self, id: i32, new: NewRecurringTx, tags: &str) -> Result<()> { + self.conn.transaction::<_, Error, _>(|conn| { + let mut db_conn = MutDbConn::new(conn, &self.cache); + + let new_tags = edit_recurring_tx(id, new, tags, &mut db_conn)?; + + self.cache.new_tags(new_tags); + + Ok(()) + })?; + + self.process_due_recurring_txs()?; + + Ok(()) + } + + pub fn delete_recurring_tx(&mut self, id: i32) -> Result<()> { + self.conn.transaction::<_, Error, _>(|conn| { + let mut db_conn = MutDbConn::new(conn, &self.cache); + + delete_recurring_tx(id, &mut db_conn)?; + + Ok(()) + })?; + + Ok(()) + } + + pub fn set_recurring_paused(&mut self, id: i32, paused: bool) -> Result<()> { + self.conn.transaction::<_, Error, _>(|conn| { + let mut db_conn = MutDbConn::new(conn, &self.cache); + + RecurringTx::set_paused(id, paused, &mut db_conn)?; + + Ok(()) + })?; + + Ok(()) + } + + pub fn get_recurring_txs(&mut self) -> Result> { + Ok(FullRecurringTx::get_all(self)?) + } + + pub fn process_due_recurring_txs(&mut self) -> Result { + const MAX_OCCURRENCE: usize = 100_000; + + let today = Local::now().date_naive(); + + let due_ids: Vec = RecurringTx::get_due(today, self)? + .into_iter() + .map(|r| r.id) + .collect(); + + let mut total_created = 0; + + for id in due_ids { + let rule = FullRecurringTx::get_by_id(id, self)?; + + if rule.is_paused { + continue; + } + + let (created, new_tags) = self.conn.transaction::<_, Error, _>(|conn| { + let mut db_conn = MutDbConn::new(conn, &self.cache); + + let mut current_date = rule.next_recurring_date; + let mut last_date = rule.last_recurred_date; + let mut created = 0; + let mut new_tags = Vec::new(); + + for _ in 0..MAX_OCCURRENCE { + if current_date > today { + break; + } + + if let Some(end) = rule.end_date + && current_date > end + { + break; + } + + new_tags.extend(process_one_due_tx(&rule, current_date, &mut db_conn)?); + + last_date = Some(current_date); + + current_date = advance_date( + current_date, + rule.frequency, + rule.recur_interval, + rule.recur_value, + rule.recur_month, + ); + + created += 1; + } + + if created > 0 { + RecurringTx::update_schedule(rule.id, last_date, current_date, &mut db_conn)?; + } + + Ok((created, new_tags)) + })?; + + if created > 0 + && let Some(details) = &rule.details + { + self.cache.new_details(details.clone()); + } + + self.cache.new_tags(new_tags); + total_created += created; + } + + Ok(total_created) + } + pub fn add_new_methods(&mut self, method_list: &[String]) -> Result<()> { self.conn.transaction::<_, Error, _>(|conn| { let mut db_conn = MutDbConn::new(conn, &self.cache); diff --git a/app/src/lib.rs b/app/src/lib.rs index a720c577..c8cef773 100644 --- a/app/src/lib.rs +++ b/app/src/lib.rs @@ -2,5 +2,5 @@ pub mod conn; pub mod migration; pub mod modifier; pub mod ui_helper; -mod utils; +pub mod utils; pub mod views; diff --git a/app/src/modifier/mod.rs b/app/src/modifier/mod.rs index a1986ec5..026fcc45 100644 --- a/app/src/modifier/mod.rs +++ b/app/src/modifier/mod.rs @@ -2,10 +2,12 @@ mod delete_tx; mod new_activity; mod new_method; mod new_tx; +mod recurring_tx; mod shared; pub(crate) use delete_tx::*; pub(crate) use new_activity::*; pub(crate) use new_method::*; pub(crate) use new_tx::*; +pub(crate) use recurring_tx::*; pub use shared::*; diff --git a/app/src/modifier/new_activity.rs b/app/src/modifier/new_activity.rs index 3e903c12..a5215b95 100644 --- a/app/src/modifier/new_activity.rs +++ b/app/src/modifier/new_activity.rs @@ -67,7 +67,7 @@ pub(crate) fn activity_edit_tx( let new_activity = NewActivity::new(activity_type).insert(conn)?; - // New one always first! + // The new tx row is inserted first so the activity view lists it above the old one let new_tx_activity = NewActivityTx::new_from_new_tx(new_tx, new_activity.id).insert(conn)?; let old_tx_activity = NewActivityTx::new_from_full_tx(old_tx, false, new_activity.id).insert(conn)?; diff --git a/app/src/modifier/recurring_tx.rs b/app/src/modifier/recurring_tx.rs new file mode 100644 index 00000000..b2cb31e0 --- /dev/null +++ b/app/src/modifier/recurring_tx.rs @@ -0,0 +1,371 @@ +use anyhow::{Context, Result}; +use chrono::{Datelike, Days, Local, Months, NaiveDate, NaiveTime}; +use rex_db::ConnCache; +use rex_db::models::{ + FullRecurringTx, NewRecurringTx, NewTag, NewTx, RecurrenceFrequency, RecurringTx, + RecurringTxTag, Tag, +}; + +use crate::modifier::{activity_new_tx, add_new_tx}; +use crate::utils::split_tags; + +pub(crate) fn add_new_recurring_tx( + new: NewRecurringTx, + tags: &str, + db_conn: &mut impl ConnCache, +) -> Result> { + let mut tag_list = split_tags(tags); + + if tag_list.is_empty() { + tag_list.push("Unknown".to_string()); + } + + let added = new.insert(db_conn).context("Failed on new recurring tx")?; + + let mut new_tags = Vec::new(); + let mut recurring_tx_tags = Vec::new(); + + for (index, tag) in tag_list.into_iter().enumerate() { + if let Ok(tag_id) = db_conn.cache().get_tag_id(&tag) { + recurring_tx_tags.push(RecurringTxTag::new(added.id, tag_id, index == 0)); + continue; + } + + let tag_data = NewTag::new(&tag) + .insert(db_conn) + .context("Failed on new tag")?; + + new_tags.push(tag_data.clone()); + recurring_tx_tags.push(RecurringTxTag::new(added.id, tag_data.id, index == 0)); + } + + RecurringTxTag::insert_batch(recurring_tx_tags, db_conn)?; + + Ok(new_tags) +} + +pub(crate) fn edit_recurring_tx( + id: i32, + new: NewRecurringTx, + tags: &str, + db_conn: &mut impl ConnCache, +) -> Result> { + RecurringTx::update(id, &new, db_conn).context("Failed to update recurring tx")?; + + RecurringTxTag::delete_by_recurring_tx_id(id, db_conn)?; + + let mut tag_list = split_tags(tags); + + if tag_list.is_empty() { + tag_list.push("Unknown".to_string()); + } + + let mut new_tags = Vec::new(); + let mut recurring_tx_tags = Vec::new(); + + for (index, tag) in tag_list.into_iter().enumerate() { + if let Ok(tag_id) = db_conn.cache().get_tag_id(&tag) { + recurring_tx_tags.push(RecurringTxTag::new(id, tag_id, index == 0)); + continue; + } + + let tag_data = NewTag::new(&tag) + .insert(db_conn) + .context("Failed on new tag")?; + + new_tags.push(tag_data.clone()); + recurring_tx_tags.push(RecurringTxTag::new(id, tag_data.id, index == 0)); + } + + RecurringTxTag::insert_batch(recurring_tx_tags, db_conn)?; + + Ok(new_tags) +} + +pub(crate) fn delete_recurring_tx(id: i32, db_conn: &mut impl ConnCache) -> Result<()> { + RecurringTx::delete_by_id(id, db_conn)?; + + Ok(()) +} + +pub(crate) fn process_one_due_tx( + rule: &FullRecurringTx, + occurrence_date: NaiveDate, + db_conn: &mut impl ConnCache, +) -> Result> { + let local_now = Local::now().naive_local(); + + let date = if occurrence_date == local_now.date() { + local_now + } else { + occurrence_date.and_time(NaiveTime::MIN) + }; + + let tx_type_str = rule.tx_type.to_string(); + + let tags_str = rule + .tags + .iter() + .map(|t| t.name.as_str()) + .collect::>() + .join(", "); + + let new_tx = NewTx::new( + date, + rule.details.as_deref(), + rule.from_method.id, + rule.to_method.as_ref().map(|m| m.id), + rule.amount.value(), + &tx_type_str, + ); + + let new_tags = add_new_tx(new_tx.clone(), &tags_str, None, db_conn) + .context("Failed to create recurring tx")?; + + activity_new_tx(&new_tx, &tags_str, db_conn)?; + + Ok(new_tags) +} + +/// Returns the last valid day of `year`-`month`, so a stored day like 31 can be clamped +/// down for months that don't have it (e.g. February). +fn last_day_of_month(year: i32, month: u32) -> u32 { + let next_month_first = if month == 12 { + NaiveDate::from_ymd_opt(year + 1, 1, 1) + } else { + NaiveDate::from_ymd_opt(year, month + 1, 1) + } + .unwrap(); + + (next_month_first - Days::new(1)).day() +} + +// If day is set to 31 but the month has 28 or 30 days, work with the minimum of the two so it +// doesn't start pointing to an invalid day of the month. +fn clamp_day(year: i32, month: u32, day: i32) -> NaiveDate { + let clamped_day = (day as u32).min(last_day_of_month(year, month)); + + NaiveDate::from_ymd_opt(year, month, clamped_day).unwrap() +} + +fn step_month(year: i32, month: u32, months_to_add: i32) -> (i32, u32) { + let start = NaiveDate::from_ymd_opt(year, month, 1).unwrap(); + + let stepped = start + .checked_add_months(Months::new(months_to_add as u32)) + .unwrap(); + + (stepped.year(), stepped.month()) +} + +/// Advances one interval forward from an already-valid occurrence date. `recur_value` must +/// be `Some` for Weekly/Monthly/Yearly and `recur_month` must be `Some` for Yearly +pub(crate) fn advance_date( + current: NaiveDate, + frequency: RecurrenceFrequency, + interval: i32, + recur_value: Option, + recur_month: Option, +) -> NaiveDate { + match frequency { + RecurrenceFrequency::Daily => current + Days::new(interval as u64), + RecurrenceFrequency::Weekly => current + Days::new(7 * interval as u64), + RecurrenceFrequency::Monthly => { + let (year, month) = step_month(current.year(), current.month(), interval); + clamp_day(year, month, recur_value.unwrap()) + } + RecurrenceFrequency::Yearly => clamp_day( + current.year() + interval, + recur_month.unwrap() as u32, + recur_value.unwrap(), + ), + } +} + +pub(crate) fn first_occurrence_on_or_after( + start: NaiveDate, + frequency: RecurrenceFrequency, + recur_value: Option, + recur_month: Option, +) -> NaiveDate { + match frequency { + RecurrenceFrequency::Daily => start, + RecurrenceFrequency::Weekly => { + let target_weekday = recur_value.unwrap(); + let start_weekday = start.weekday().num_days_from_sunday() as i32; + let days_to_add = (target_weekday - start_weekday).rem_euclid(7); + + start + Days::new(days_to_add as u64) + } + RecurrenceFrequency::Monthly => { + let target_this_month = clamp_day(start.year(), start.month(), recur_value.unwrap()); + + if target_this_month >= start { + target_this_month + } else { + let (year, month) = step_month(start.year(), start.month(), 1); + clamp_day(year, month, recur_value.unwrap()) + } + } + RecurrenceFrequency::Yearly => { + let month = recur_month.unwrap() as u32; + let target_this_year = clamp_day(start.year(), month, recur_value.unwrap()); + + if target_this_year >= start { + target_this_year + } else { + clamp_day(start.year() + 1, month, recur_value.unwrap()) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn date(year: i32, month: u32, day: u32) -> NaiveDate { + NaiveDate::from_ymd_opt(year, month, day).unwrap() + } + + #[test] + fn clamp_day_keeps_valid_day() { + assert_eq!(clamp_day(2026, 1, 15), date(2026, 1, 15)); + } + + #[test] + fn clamp_day_clamps_to_february_in_non_leap_year() { + assert_eq!(clamp_day(2026, 2, 31), date(2026, 2, 28)); + } + + #[test] + fn clamp_day_clamps_to_february_in_leap_year() { + assert_eq!(clamp_day(2024, 2, 31), date(2024, 2, 29)); + } + + #[test] + fn advance_date_daily_steps_by_interval() { + let next = advance_date(date(2026, 1, 30), RecurrenceFrequency::Daily, 5, None, None); + assert_eq!(next, date(2026, 2, 4)); + } + + #[test] + fn advance_date_weekly_preserves_weekday_across_month_boundary() { + // 2026-01-28 is a Wednesday, every-2-weeks should land on the next Wednesday twice over + let start = date(2026, 1, 28); + let next = advance_date( + start, + RecurrenceFrequency::Weekly, + 2, + Some(start.weekday().num_days_from_sunday() as i32), + None, + ); + assert_eq!(next, date(2026, 2, 11)); + assert_eq!(next.weekday(), start.weekday()); + } + + #[test] + fn advance_date_monthly_does_not_drift_after_clamping() { + // 31st of every month: Jan 31 -> Feb 28 (clamped) -> back to Mar 31 (no drift) + let jan_31 = date(2026, 1, 31); + let feb = advance_date(jan_31, RecurrenceFrequency::Monthly, 1, Some(31), None); + assert_eq!(feb, date(2026, 2, 28)); + + let mar = advance_date(feb, RecurrenceFrequency::Monthly, 1, Some(31), None); + assert_eq!(mar, date(2026, 3, 31)); + } + + #[test] + fn advance_date_monthly_interval_crosses_year_boundary() { + // Quarterly (every 3 months) starting in November + let nov = date(2026, 11, 15); + let next = advance_date(nov, RecurrenceFrequency::Monthly, 3, Some(15), None); + assert_eq!(next, date(2027, 2, 15)); + } + + #[test] + fn advance_date_yearly_clamps_leap_day() { + let leap_day = date(2024, 2, 29); + let next = advance_date(leap_day, RecurrenceFrequency::Yearly, 1, Some(29), Some(2)); + assert_eq!(next, date(2025, 2, 28)); + } + + #[test] + fn first_occurrence_weekly_start_already_matches() { + let start = date(2026, 3, 16); + let target_weekday = start.weekday().num_days_from_sunday() as i32; + + let first = first_occurrence_on_or_after( + start, + RecurrenceFrequency::Weekly, + Some(target_weekday), + None, + ); + assert_eq!(first, start); + } + + #[test] + fn first_occurrence_weekly_rolls_forward_to_chosen_weekday() { + // The start date's own weekday doesn't constrain which day can be chosen - it's + // just an anchor. Picking a day 2 days ahead in the week should roll forward, not error. + let start = date(2026, 3, 14); + let start_weekday = start.weekday().num_days_from_sunday() as i32; + let target_weekday = (start_weekday + 2).rem_euclid(7); + + let first = first_occurrence_on_or_after( + start, + RecurrenceFrequency::Weekly, + Some(target_weekday), + None, + ); + assert_eq!(first, start + Days::new(2)); + } + + #[test] + fn first_occurrence_weekly_wraps_to_next_week_when_day_already_passed() { + let start = date(2026, 3, 14); + let start_weekday = start.weekday().num_days_from_sunday() as i32; + // A day that's "behind" in the week relative to start wraps all the way to next week + let target_weekday = (start_weekday + 6).rem_euclid(7); + + let first = first_occurrence_on_or_after( + start, + RecurrenceFrequency::Weekly, + Some(target_weekday), + None, + ); + assert_eq!(first, start + Days::new(6)); + } + + #[test] + fn first_occurrence_monthly_start_already_matches() { + let start = date(2026, 3, 15); + let first = + first_occurrence_on_or_after(start, RecurrenceFrequency::Monthly, Some(15), None); + assert_eq!(first, start); + } + + #[test] + fn first_occurrence_monthly_target_still_ahead_this_month() { + let start = date(2026, 3, 10); + let first = + first_occurrence_on_or_after(start, RecurrenceFrequency::Monthly, Some(20), None); + assert_eq!(first, date(2026, 3, 20)); + } + + #[test] + fn first_occurrence_monthly_target_already_passed_rolls_to_next_month() { + let start = date(2026, 3, 25); + let first = + first_occurrence_on_or_after(start, RecurrenceFrequency::Monthly, Some(5), None); + assert_eq!(first, date(2026, 4, 5)); + } + + #[test] + fn first_occurrence_yearly_rolls_to_next_year() { + let start = date(2026, 6, 1); + let first = + first_occurrence_on_or_after(start, RecurrenceFrequency::Yearly, Some(1), Some(1)); + assert_eq!(first, date(2027, 1, 1)); + } +} diff --git a/app/src/modifier/shared.rs b/app/src/modifier/shared.rs index 661f3199..8c3fb544 100644 --- a/app/src/modifier/shared.rs +++ b/app/src/modifier/shared.rs @@ -1,10 +1,14 @@ use anyhow::{Result, anyhow}; use chrono::{Days, Local, Months, NaiveDate, NaiveTime}; use rex_db::ConnCache; -use rex_db::models::{Balance, DateNature, FetchNature, NewSearch, NewTx, Tx, TxType}; +use rex_db::models::{ + Balance, DateNature, FetchNature, NewRecurringTx, NewSearch, NewTx, RecurrenceFrequency, Tx, + TxType, +}; use rex_shared::models::{Dollar, LAST_POSSIBLE_TIME}; -use crate::utils::parse_amount_nature_cent; +use crate::modifier::first_occurrence_on_or_after; +use crate::utils::{month_name_to_num, parse_amount_nature_cent, weekday_name_to_num}; pub(crate) fn tidy_balances(date: NaiveDate, db_conn: &mut impl ConnCache) -> Result<()> { let tx = Balance::get_highest_date(db_conn)?; @@ -13,8 +17,10 @@ pub(crate) fn tidy_balances(date: NaiveDate, db_conn: &mut impl ConnCache) -> Re tidy_recursive(max_date, date, db_conn)?; - // From bad delete txs, final balance got corrupted but monthly balances are fine. - // If does not match, trust the monthly balances + // A bad delete can corrupt the final balance while the monthly ones stay + // correct, so whenever the two disagree, trust the latest monthly balance. + // This issue is from an older version which has since been fixed and should not occur again. + // This is just as a safety net. let mut final_balance = Balance::get_final_balance(db_conn)?; let balance_highest_date = Balance::get_balance_highest_date(db_conn)?; @@ -127,6 +133,82 @@ pub fn parse_tx_fields<'a>( Ok(new_tx) } +#[allow(clippy::too_many_arguments)] +pub fn parse_recurring_tx_fields<'a>( + date: &'a str, + details: &'a str, + from_method: &'a str, + to_method: &'a str, + amount: &'a str, + tx_type: &'a str, + frequency: &'a str, + recur_interval: &'a str, + recur_value: &'a str, + recur_month: &'a str, + end_date: &'a str, + db_conn: &impl ConnCache, +) -> Result> { + let start_date = date.parse::()?; + + let details = if details.is_empty() { + None + } else { + Some(details) + }; + + let amount = Dollar::new(amount.parse()?).cent().value(); + + let from_method = db_conn.cache().get_method_id(from_method)?; + let to_method = if to_method.is_empty() { + None + } else { + Some(db_conn.cache().get_method_id(to_method)?) + }; + + let frequency_value: RecurrenceFrequency = frequency.into(); + let interval = recur_interval.parse::()?; + + let (value, month) = match frequency_value { + RecurrenceFrequency::Daily => (None, None), + RecurrenceFrequency::Weekly => { + let weekday = weekday_name_to_num(recur_value)?; + + (Some(weekday), None) + } + RecurrenceFrequency::Monthly => (Some(recur_value.parse::()?), None), + RecurrenceFrequency::Yearly => ( + Some(recur_value.parse::()?), + Some(month_name_to_num(recur_month)? as i32), + ), + }; + + let next_recurring_date = + first_occurrence_on_or_after(start_date, frequency_value, value, month); + + let end_date = if end_date.is_empty() { + None + } else { + Some(end_date.parse::()?) + }; + + let created_at = Local::now().naive_local(); + + Ok(NewRecurringTx::new( + created_at, + details, + from_method, + to_method, + amount, + tx_type, + frequency, + interval, + value, + month, + next_recurring_date, + end_date, + )) +} + pub fn parse_search_fields<'a>( date: &'a str, details: &'a str, diff --git a/app/src/ui_helper/autofiller.rs b/app/src/ui_helper/autofiller.rs index 6a6ee1ad..0bf74bf9 100644 --- a/app/src/ui_helper/autofiller.rs +++ b/app/src/ui_helper/autofiller.rs @@ -1,9 +1,35 @@ use rex_db::ConnCache; -use rex_db::models::TxType; +use rex_db::models::{RecurrenceFrequency, TxType}; use strum::IntoEnumIterator; use crate::conn::MutDbConn; use crate::ui_helper::get_best_match; +use crate::utils::{MONTH_NAMES, WEEKDAY_NAMES}; + +fn suggest_from_options(user_input: &str, options: &[String]) -> String { + let trimmed_input = user_input.trim(); + + if trimmed_input.is_empty() || options.is_empty() { + return String::new(); + } + + let lowercase = trimmed_input.to_lowercase(); + + let mut prefix_matches = options + .iter() + .filter(|o| o.to_lowercase().starts_with(&lowercase)); + + let suggestion = match (prefix_matches.next(), prefix_matches.next()) { + (Some(only_match), None) => only_match.clone(), + _ => get_best_match(user_input, options), + }; + + if suggestion == trimmed_input { + String::new() + } else { + suggestion + } +} pub struct Autofiller<'a> { conn: MutDbConn<'a>, @@ -24,76 +50,60 @@ impl<'a> Autofiller<'a> { .map(|m| m.name.clone()) .collect::>(); - let trimmed_input = user_input.trim(); - - if trimmed_input.is_empty() || methods.is_empty() { - return String::new(); - } - - let best_match = get_best_match(user_input, &methods); - - if best_match == trimmed_input { - String::new() - } else { - best_match - } + suggest_from_options(user_input, &methods) } #[must_use] pub fn tx_type(&self, user_input: &str) -> String { let trimmed_input = user_input.trim(); - let lowercase = trimmed_input.to_lowercase(); - if trimmed_input.is_empty() { return String::new(); } - let return_best_match = || { - let tx_types = TxType::iter() - .map(|s| s.to_string()) - .collect::>(); - - let best_match = get_best_match(user_input, &tx_types); - - if best_match == trimmed_input { - String::new() - } else { - best_match - } - }; + let lowercase = trimmed_input.to_lowercase(); - let best_match = if lowercase.len() <= 2 { - let tx_type = if lowercase.starts_with('t') { - TxType::Transfer.to_string() + let shortcut = if lowercase.len() <= 2 { + if lowercase.starts_with('t') { + Some(TxType::Transfer) } else if lowercase.starts_with('e') { - TxType::Expense.to_string() + Some(TxType::Expense) } else if lowercase.starts_with('i') { - TxType::Income.to_string() + Some(TxType::Income) } else if lowercase.starts_with("br") { - TxType::BorrowRepay.to_string() + Some(TxType::BorrowRepay) } else if lowercase.starts_with("lr") { - TxType::LendRepay.to_string() + Some(TxType::LendRepay) } else if lowercase.starts_with('b') { - TxType::Borrow.to_string() + Some(TxType::Borrow) } else if lowercase.starts_with('l') { - TxType::Lend.to_string() + Some(TxType::Lend) } else { - return_best_match() - }; - - tx_type.clone() + None + } } else { - return_best_match() + None }; - if best_match == trimmed_input { - String::new() - } else { - best_match + if let Some(tx_type) = shortcut { + let tx_type = tx_type.to_string(); + + return if tx_type == trimmed_input { + String::new() + } else { + tx_type + }; } + + let tx_types = TxType::iter() + .map(|s| s.to_string()) + .collect::>(); + + suggest_from_options(user_input, &tx_types) } + /// Tags are comma separated, so only the tag currently being typed (the last one) is completed + #[must_use] pub fn tags(&self, user_input: &str) -> String { let tags = self .conn @@ -103,27 +113,9 @@ impl<'a> Autofiller<'a> { .map(|m| m.name.clone()) .collect::>(); - let trimmed_input = user_input.trim(); - - if trimmed_input.is_empty() || tags.is_empty() { - return String::new(); - } - - let split_data = user_input.split(',').map(str::trim).collect::>(); - - let last_value = split_data.last().unwrap().trim(); + let last_value = user_input.rsplit(',').next().unwrap_or_default().trim(); - if last_value.is_empty() { - return String::new(); - } - - let best_match = get_best_match(last_value, &tags); - - if best_match == last_value { - String::new() - } else { - best_match - } + suggest_from_options(last_value, &tags) } #[must_use] @@ -136,18 +128,41 @@ impl<'a> Autofiller<'a> { .map(std::string::ToString::to_string) .collect::>(); - let trimmed_input = user_input.trim(); + suggest_from_options(user_input, &details) + } - if trimmed_input.is_empty() || details.is_empty() { + #[must_use] + pub fn frequency(&self, user_input: &str) -> String { + let frequencies = RecurrenceFrequency::iter() + .map(|f| f.to_string()) + .collect::>(); + + suggest_from_options(user_input, &frequencies) + } + + /// Only Weekly has a name to complete (the day of the week). Monthly/Yearly take a plain + /// day-of-month number and Daily takes nothing, so those never get a suggestion. + #[must_use] + pub fn recur_value(&self, user_input: &str, frequency: RecurrenceFrequency) -> String { + if frequency != RecurrenceFrequency::Weekly { return String::new(); } - let best_match = get_best_match(user_input, &details); + let weekdays = WEEKDAY_NAMES + .iter() + .map(std::string::ToString::to_string) + .collect::>(); - if best_match == trimmed_input { - String::new() - } else { - best_match - } + suggest_from_options(user_input, &weekdays) + } + + #[must_use] + pub fn recur_month(&self, user_input: &str) -> String { + let months = MONTH_NAMES + .iter() + .map(std::string::ToString::to_string) + .collect::>(); + + suggest_from_options(user_input, &months) } } diff --git a/app/src/ui_helper/outputs.rs b/app/src/ui_helper/outputs.rs index 11e7b6b3..f4430a98 100644 --- a/app/src/ui_helper/outputs.rs +++ b/app/src/ui_helper/outputs.rs @@ -27,6 +27,14 @@ pub enum Field { TxType, #[error("Tags")] Tags, + #[error("Frequency")] + Frequency, + #[error("Recur Interval")] + RecurInterval, + #[error("Recur Value")] + RecurValue, + #[error("Recur Month")] + RecurMonth, } #[derive(Debug, Error)] @@ -59,6 +67,16 @@ pub enum VerifierError { InvalidBValue, #[error("Tags: Non-existing tags cannot be accepted")] NonExistingTag, + #[error("Frequency: Value not acceptable. Values: Daily/Weekly/Monthly/Yearly")] + InvalidFrequency, + #[error("Recur Interval: Value must be a whole number of at least 1")] + InvalidRecurInterval, + #[error("Recur Value: Day of month must be between 1-31")] + InvalidRecurValueMonthly, + #[error("Recur Value: Day of week not acceptable. Values: Sunday-Saturday")] + InvalidRecurValueWeekly, + #[error("Recur Month: Value not acceptable. Values: January-December")] + InvalidRecurMonth, #[error("Others: Something went wrong while verifying input. Error: {0}")] Others(String), } @@ -77,6 +95,14 @@ pub enum SteppingError { InvalidTags, #[error("Amount: Failed to step value. Value of B cannot be determined")] UnknownBValue, + #[error("Frequency: Failed to step as the frequency is invalid")] + InvalidFrequency, + #[error("Recur Interval: Failed to step due to invalid interval format")] + InvalidRecurInterval, + #[error("Recur Value: Failed to step as the value does not exist for the current frequency")] + InvalidRecurValue, + #[error("Recur Month: Failed to step as the month does not exists")] + InvalidRecurMonth, #[error("{0}: Error acquired while validating input")] ParsingError(Field), } diff --git a/app/src/ui_helper/shared.rs b/app/src/ui_helper/shared.rs index ba5a7cef..89a6d38b 100644 --- a/app/src/ui_helper/shared.rs +++ b/app/src/ui_helper/shared.rs @@ -1,6 +1,7 @@ use strsim::normalized_levenshtein; -/// Uses Levenshtein algorithm to get the best match of a string in a vec of strings +/// Uses the normalized Levenshtein distance to find the closest match for +/// `data` among `matching_set` #[must_use] pub(crate) fn get_best_match(data: &str, matching_set: &[String]) -> String { let mut best_match = &matching_set[0]; diff --git a/app/src/ui_helper/stepper.rs b/app/src/ui_helper/stepper.rs index 5c33bd2d..8daf7e3c 100644 --- a/app/src/ui_helper/stepper.rs +++ b/app/src/ui_helper/stepper.rs @@ -1,12 +1,13 @@ -use chrono::{Duration, Months, NaiveDate}; +use chrono::{Duration, Local, Months, NaiveDate}; use rex_db::ConnCache; -use rex_db::models::TxType; +use rex_db::models::{RecurrenceFrequency, TxType}; use strum::IntoEnumIterator; use crate::conn::MutDbConn; use crate::ui_helper::{ DateType, Field, Output, StepType, SteppingError, VerifierError, get_best_match, }; +use crate::utils::{MONTH_NAMES, WEEKDAY_NAMES}; pub struct Stepper<'a> { conn: MutDbConn<'a>, @@ -27,11 +28,15 @@ impl<'a> Stepper<'a> { match verify_status { Ok(data) => match data { - Output::Nothing(_) => match date_type { - DateType::Exact => *user_date = String::from("2022-01-01"), - DateType::Monthly => *user_date = String::from("2022-01"), - DateType::Yearly => *user_date = String::from("2022"), - }, + Output::Nothing(_) => { + let current_date = Local::now().naive_local(); + + match date_type { + DateType::Exact => *user_date = current_date.format("%Y-%m-%d").to_string(), + DateType::Monthly => *user_date = current_date.format("%Y-%m").to_string(), + DateType::Yearly => *user_date = current_date.format("%Y").to_string(), + } + } Output::Accepted(_) => match date_type { DateType::Exact => { let mut current_date = @@ -214,6 +219,189 @@ impl<'a> Stepper<'a> { Ok(()) } + pub fn frequency( + mut self, + user_freq: &mut String, + step_type: StepType, + ) -> Result<(), SteppingError> { + let verify_status = self.conn.verify().frequency(user_freq); + + match verify_status { + Ok(data) => match data { + Output::Accepted(_) => { + let frequencies: Vec = + RecurrenceFrequency::iter().map(|f| f.to_string()).collect(); + + let mut current_index = + frequencies.iter().position(|f| f == user_freq).unwrap(); + + match step_type { + StepType::StepUp => { + current_index = (current_index + 1) % frequencies.len(); + } + StepType::StepDown => { + current_index = + (current_index + frequencies.len() - 1) % frequencies.len(); + } + } + + *user_freq = frequencies[current_index].clone(); + } + Output::Nothing(_) => { + *user_freq = RecurrenceFrequency::Daily.to_string(); + } + }, + Err(_) => { + return Err(SteppingError::InvalidFrequency); + } + } + + Ok(()) + } + + pub fn recur_interval( + mut self, + user_value: &mut String, + step_type: StepType, + ) -> Result<(), SteppingError> { + let verify_status = self.conn.verify().recur_interval(user_value); + + match verify_status { + Ok(data) => match data { + Output::Accepted(_) => { + let mut current = user_value + .parse::() + .map_err(|_| SteppingError::ParsingError(Field::RecurInterval))?; + + match step_type { + StepType::StepUp => { + if current < 9999 { + current += 1; + } + } + StepType::StepDown => { + if current > 1 { + current -= 1; + } + } + } + + *user_value = current.to_string(); + } + Output::Nothing(_) => { + *user_value = "1".to_string(); + } + }, + Err(_) => { + return Err(SteppingError::InvalidRecurInterval); + } + } + + Ok(()) + } + + pub fn recur_value( + mut self, + user_value: &mut String, + frequency: RecurrenceFrequency, + step_type: StepType, + ) -> Result<(), SteppingError> { + if let RecurrenceFrequency::Daily = frequency { + return Ok(()); + } + + let verify_status = self.conn.verify().recur_value(user_value, frequency); + + if let RecurrenceFrequency::Weekly = frequency { + return match verify_status { + Ok(data) => match data { + Output::Accepted(_) => { + let mut current_index = + WEEKDAY_NAMES.iter().position(|d| *d == user_value).unwrap(); + + match step_type { + StepType::StepUp => { + current_index = (current_index + 1) % WEEKDAY_NAMES.len(); + } + StepType::StepDown => { + current_index = + (current_index + WEEKDAY_NAMES.len() - 1) % WEEKDAY_NAMES.len(); + } + } + + *user_value = WEEKDAY_NAMES[current_index].to_string(); + Ok(()) + } + Output::Nothing(_) => { + *user_value = WEEKDAY_NAMES[0].to_string(); + Ok(()) + } + }, + Err(_) => Err(SteppingError::InvalidRecurValue), + }; + } + + match verify_status { + Ok(data) => match data { + Output::Accepted(_) => { + let mut current = user_value + .parse::() + .map_err(|_| SteppingError::ParsingError(Field::RecurValue))?; + + match step_type { + StepType::StepUp => current = if current >= 31 { 1 } else { current + 1 }, + StepType::StepDown => current = if current <= 1 { 31 } else { current - 1 }, + } + + *user_value = current.to_string(); + } + Output::Nothing(_) => { + *user_value = "1".to_string(); + } + }, + Err(_) => { + return Err(SteppingError::InvalidRecurValue); + } + } + + Ok(()) + } + + pub fn recur_month( + mut self, + user_value: &mut String, + step_type: StepType, + ) -> Result<(), SteppingError> { + let verify_status = self.conn.verify().recur_month(user_value); + + match verify_status { + Ok(data) => match data { + Output::Accepted(_) => { + let mut current_index = + MONTH_NAMES.iter().position(|m| *m == user_value).unwrap(); + + match step_type { + StepType::StepUp => current_index = (current_index + 1) % MONTH_NAMES.len(), + StepType::StepDown => { + current_index = + (current_index + MONTH_NAMES.len() - 1) % MONTH_NAMES.len(); + } + } + + *user_value = MONTH_NAMES[current_index].to_string(); + } + Output::Nothing(_) => { + *user_value = MONTH_NAMES[0].to_string(); + } + }, + Err(_) => { + return Err(SteppingError::InvalidRecurMonth); + } + } + + Ok(()) + } + pub fn tag(&self, user_tag: &mut String, step_type: StepType) -> Result<(), SteppingError> { let all_tags = self .conn diff --git a/app/src/ui_helper/verifier.rs b/app/src/ui_helper/verifier.rs index baadd3e4..34149df3 100644 --- a/app/src/ui_helper/verifier.rs +++ b/app/src/ui_helper/verifier.rs @@ -3,11 +3,12 @@ use std::collections::HashSet; use chrono::NaiveDate; use rex_db::ConnCache; -use rex_db::models::TxType; +use rex_db::models::{RecurrenceFrequency, TxType}; use strum::IntoEnumIterator; use crate::conn::MutDbConn; use crate::ui_helper::{DateType, Field, Output, VerifierError, get_best_match}; +use crate::utils::{MONTH_NAMES, WEEKDAY_NAMES}; pub struct Verifier<'a> { conn: MutDbConn<'a>, @@ -18,21 +19,19 @@ impl<'a> Verifier<'a> { Self { conn } } - /// Checks if: + /// Validates and normalizes the user's date input for the given `DateType`: /// - /// - The inputted year is between 2022 to 2037. - /// - The inputted month is between 01 to 12. - /// - The inputted date is between 01 to 31. - /// - The inputted date is empty. - /// - Contains any extra spaces. - /// - The date actually exists. - /// - Removes any extra spaces and non-numeric characters. - /// - Ensures proper char length for each part of the date. + /// - an empty input is left alone + /// - every character except digits and `-` is dropped, so extra spaces and + /// other symbols disappear + /// - the year must be 4 characters long, the month and the day 2 + /// - the month must be within 01-12 and the day within 01-31 + /// - for an exact date, the parts must form a date that actually exists /// - /// Finally, tries to correct the date if it was not accepted by - /// adding 0 if the beginning if the length is smaller than necessary - /// or restores to the smallest or the largest date if date is beyond the - /// accepted value. + /// A rejected input is repaired in place before the error is returned: a + /// short year becomes 2022, a long one is cut down to 4 characters, single + /// digit months and days get a leading 0 and out of range months and days + /// are clamped to 12 and 31. pub fn date( self, user_date: &mut String, @@ -52,7 +51,8 @@ impl<'a> Verifier<'a> { .map(ToString::to_string) .collect::>(); - // If one part of the date is missing/extra, return unknown date + // The part count has to match the date type, so fall back to the + // earliest date this type accepts match date_type { DateType::Exact => { if split_date.len() != 3 { @@ -96,8 +96,8 @@ impl<'a> Verifier<'a> { DateType::Yearly => (None, None), }; - // Checks if the year part length is 4. If not 4, turn the year to 2022 + the other character entered by the user - // and return the new date + // The year has to be 4 characters long. A shorter one becomes 2022, a + // longer one is cut down to its first 4 characters if split_date[0].len() != 4 { match split_date[0].len().cmp(&4) { Ordering::Less => match date_type { @@ -126,8 +126,8 @@ impl<'a> Verifier<'a> { return Err(VerifierError::InvalidYear); } - // Checks if the month part length is 2. If not 2, turn the month to 0 + whatever month was entered + the other character entered by the user - // and return the new date + // The month has to be 2 characters long. A single digit month gets a + // leading 0, anything above 12 is clamped to 12 match date_type { DateType::Exact => { if split_date[1].len() != 2 { @@ -157,8 +157,8 @@ impl<'a> Verifier<'a> { DateType::Yearly => {} } - // Checks if the day part length is 2. If not 2, turn the day to 0 + whatever day was entered + the other character entered by the user - // and return the new date + // The day has to be 2 characters long. A single digit day gets a + // leading 0, anything above 31 is clamped to 31 if let DateType::Exact = date_type { let unwrapped_day = int_day.unwrap(); if split_date[2].len() != 2 { @@ -172,7 +172,7 @@ impl<'a> Verifier<'a> { } } - // Checks if the month value is between 1 and 12 + // Clamp the month into 01-12 match date_type { DateType::Exact => { let unwrapped_month = int_month.unwrap(); @@ -201,7 +201,7 @@ impl<'a> Verifier<'a> { DateType::Yearly => {} } - // Checks if the day value is between 1 and 31 + // Clamp the day into 01-31 if let DateType::Exact = date_type { let unwrapped_day = int_day.unwrap(); if !(1..=31).contains(&unwrapped_day) { @@ -215,8 +215,8 @@ impl<'a> Verifier<'a> { } } - // We will check if the date actually exists otherwise return error - // Some months have more or less days than 31 so the date needs to be validated + // Months have fewer than 31 days at times, so the day still has to be + // validated against the calendar if let DateType::Exact = date_type { NaiveDate::parse_from_str(user_date, "%Y-%m-%d") .map_err(|_| VerifierError::NonExistingDate)?; @@ -225,17 +225,15 @@ impl<'a> Verifier<'a> { Ok(Output::Accepted(Field::Date)) } - /// Checks if: + /// Validates and normalizes the user's amount input: /// - /// - Amount is empty - /// - Amount is zero or below - /// - Amount text contains a calculation symbol - /// - contains any extra spaces - /// - removes any extra spaces and non-numeric characters - /// - /// If the value is not float, tries to make it float ending with double zero + /// - an empty input is left alone + /// - every character except digits, `.` and the calculation symbols is dropped + /// - a calculation such as `1+5*10` is evaluated, `*` and `/` first + /// - the value is normalized to exactly 2 decimal places + /// - a zero or negative value is turned positive and rejected pub fn amount(&self, user_amount: &mut String) -> Result { - // Cancel all verification if the amount is empty + // Nothing to verify while the field is empty if user_amount.is_empty() { return Ok(Output::Nothing(Field::Amount)); } @@ -247,42 +245,38 @@ impl<'a> Verifier<'a> { .filter(|c| c.is_numeric() || *c == '.' || calc_symbols.contains(c)) .collect(); - // Already checked if the initial amount is empty. - // If it becomes empty after the filtering was done, there no number inside so return error + // The field was non empty before filtering, so it can only be empty now + // if nothing usable was left in it if user_amount.is_empty() { return Err(VerifierError::ParsingError(Field::Amount)); } // Check if any of the symbols are present if calc_symbols.iter().any(|s| user_amount.contains(*s)) { - // How it works: - // The calc_symbol intentionally starts with * and / so these calculations are done first. - // Start a main loop which will only run for the amount of times anyone of them from calc_symbols is present. - // Loop over the symbols and check if the symbol is present in the string - // find the index of where the symbol is then take the number values from both side of the symbol. - // Example: 1+5*10. We start with *, we initially, we will work with 5*10. - // Isolate the numbers => do the calculation => replace the part of the string we are working with, with the result which is 50 - // result: 1+50 => break the symbol checking loop and continue the main loop again so we start working with 1+50. - - // Get the amount of time the symbols were found in the amount string + // `*` and `/` come first in `calc_symbols` so they are resolved before + // `+` and `-`. One substitution is made per symbol found, so an + // expression like `1+5*10` collapses `5*10` into 50 first and then + // works on the remaining `1+50`. + + // One substitution per symbol found, so the loop below runs that many times let count = user_amount .chars() .filter(|c| calc_symbols.contains(c)) .count(); - // Remove all spaces for easier indexing + // Spaces are already gone thanks to the filter above, so indexing is safe let mut working_value = user_amount.to_owned(); for _i in 0..count { for symbol in &calc_symbols { if let Some(location) = working_value.find(*symbol) { - // If a symbol is found, we want to store the values to its side to these variables. - // Example: 1+5 first_value = 1 last_value = 5 + // Grab the numbers on both sides of the symbol, e.g. `1+5` + // gives first_value = 1 and last_value = 5 let mut first_value = String::new(); let mut last_value = String::new(); - // Skip to symbol location + 1 index value and start taking chars from here until the end - // of the string or until another cal symbol is encountered + // Read right of the symbol, up to the end of the string or + // the next calculation symbol for char in working_value.chars().skip(location + 1) { if calc_symbols.contains(&char) { break; @@ -290,7 +284,7 @@ impl<'a> Verifier<'a> { last_value.push(char); } - // Do the same thing as before but this time, reverse the string + // Same as above, but walking the string backwards to get the left operand for char in working_value .chars() .rev() @@ -304,7 +298,8 @@ impl<'a> Verifier<'a> { // Un-reverse the string first_value = first_value.chars().rev().collect(); - // If either of them is empty, the one that is not empty is the value we want to use for using in replacement + // One side is missing, as in `-5`, so there is nothing to + // calculate and the other side is kept as is let final_value = if first_value.is_empty() || last_value.is_empty() { if first_value.is_empty() { last_value.clone() @@ -312,7 +307,7 @@ impl<'a> Verifier<'a> { first_value.clone() } } else { - // If both value is intact, do the calculation and the result is for replacement + // Both operands are there, so this symbol's result is ready for replacement let first_num: f64 = match first_value.parse() { Ok(v) => v, Err(_) => { @@ -336,9 +331,9 @@ impl<'a> Verifier<'a> { } }; - // Example: 1+5*10 - // if everything goes alright, first_value is 5, last_value is 10 and the symbol is * - // replace 5*10 with the earlier result we got which is 50. Continue with 1+50 in the next loop + // e.g. `1+5*10` gives first_value = 5, last_value = 10 and + // symbol = `*`, so `5*10` becomes 50 and the next loop works + // on `1+50` working_value = working_value .replace(&format!("{first_value}{symbol}{last_value}"), &final_value); @@ -349,8 +344,8 @@ impl<'a> Verifier<'a> { *user_amount = working_value; } - // If dot is present but nothing after that, add 2 zero - // if no dot, add dot + 2 zero + // Make sure a `.` exists and that something follows it, otherwise the + // value cannot be parsed as a float if user_amount.contains('.') { let state = user_amount.split('.').collect::>(); if state[1].is_empty() { @@ -369,7 +364,7 @@ impl<'a> Verifier<'a> { return Err(VerifierError::AmountBelowZero); } - // Checks if there is 2 number after the dot else add zero/s + // Pad or cut the decimals down to exactly 2 digits if user_amount.contains('.') { let split_amount = user_amount.split('.').collect::>(); @@ -382,11 +377,10 @@ impl<'a> Verifier<'a> { } } - // We can safely split now as previously we just added a dot + 2 numbers with the amount - // and create the final value for the amount + // Safe to split now, the amount always has a `.` followed by 2 digits let split_amount = user_amount.split('.').collect::>(); - // limit max character to 10 + // The integer part is capped at 10 digits if split_amount[0].len() > 10 { *user_amount = format!("{}.{}", &split_amount[0][..10], split_amount[1]); } @@ -394,20 +388,18 @@ impl<'a> Verifier<'a> { Ok(Output::Accepted(Field::Amount)) } - /// Checks if: + /// Validates the user's transaction method input: /// - /// - The Transaction method exists on the database. - /// - The Transaction method is empty - /// - contains any extra spaces + /// - an empty input is left alone + /// - surrounding spaces are trimmed + /// - the name has to match an existing method, ignoring case /// - /// If the Transaction is not found, matches each character with the available - /// Transaction Methods and corrects to the best matching one. + /// Anything else is fuzzy corrected to the closest existing method name and + /// rejected. pub fn tx_method(&self, user_method: &mut String) -> Result { - // Get all currently added tx methods - *user_method = user_method.trim().to_string(); - // Cancel all verification if the text is empty + // Nothing to verify while the text is empty if user_method.is_empty() { return Ok(Output::Nothing(Field::TxMethod)); } @@ -435,11 +427,14 @@ impl<'a> Verifier<'a> { Err(VerifierError::InvalidTxMethod) } - /// Checks if: + /// Validates the user's transaction type input: /// - /// - The transaction method starts with E, I, or T + /// - a 1 or 2 character input is expanded by its first letters, so `e` becomes + /// Expense, `i` Income, `t` Transfer, `b` Borrow, `l` Lend and the `br` / `lr` + /// variants their repay types + /// - anything longer has to be a full type name /// - /// Auto expands E to Expense, I to Income and T to transfer. + /// Unrecognised input is fuzzy corrected to the closest type and rejected. pub fn tx_type(&self, user_type: &mut String) -> Result { let trimmed_input = user_type.trim(); @@ -493,9 +488,144 @@ impl<'a> Verifier<'a> { Ok(Output::Accepted(Field::TxType)) } - /// Checks if: - /// - /// - All tags inserted is unique and is properly separated by commas + /// Checks if the inputted recurrence frequency (Daily/Weekly/Monthly/Yearly) is valid, + /// accepting the same short mnemonics (d/w/m/y) + pub fn frequency(&self, user_freq: &mut String) -> Result { + let trimmed_input = user_freq.trim(); + + if user_freq.is_empty() { + return Ok(Output::Nothing(Field::Frequency)); + } + + let frequencies = RecurrenceFrequency::iter() + .map(|f| f.to_string()) + .collect::>(); + + let return_best_match = || { + let best_match = get_best_match(user_freq, &frequencies); + + if best_match == trimmed_input { + String::new() + } else { + best_match + } + }; + + let lowercase = user_freq.to_lowercase(); + + if lowercase.len() <= 2 { + if lowercase.starts_with('d') { + *user_freq = RecurrenceFrequency::Daily.to_string(); + } else if lowercase.starts_with('w') { + *user_freq = RecurrenceFrequency::Weekly.to_string(); + } else if lowercase.starts_with('m') { + *user_freq = RecurrenceFrequency::Monthly.to_string(); + } else if lowercase.starts_with('y') { + *user_freq = RecurrenceFrequency::Yearly.to_string(); + } else { + *user_freq = return_best_match(); + return Err(VerifierError::InvalidFrequency); + } + } else { + if frequencies.contains(user_freq) { + return Ok(Output::Accepted(Field::Frequency)); + } + *user_freq = return_best_match(); + return Err(VerifierError::InvalidFrequency); + } + + Ok(Output::Accepted(Field::Frequency)) + } + + /// Checks if the inputted "every N units" interval is a whole number of at least 1. + pub fn recur_interval(&self, user_value: &mut String) -> Result { + if user_value.is_empty() { + return Ok(Output::Nothing(Field::RecurInterval)); + } + + *user_value = user_value.chars().filter(char::is_ascii_digit).collect(); + + if user_value.is_empty() { + return Err(VerifierError::ParsingError(Field::RecurInterval)); + } + + let parsed = user_value + .parse::() + .map_err(|_| VerifierError::ParsingError(Field::RecurInterval))?; + + if parsed < 1 { + *user_value = "1".to_string(); + return Err(VerifierError::InvalidRecurInterval); + } + + Ok(Output::Accepted(Field::RecurInterval)) + } + + pub fn recur_value( + &self, + user_value: &mut String, + frequency: RecurrenceFrequency, + ) -> Result { + if user_value.is_empty() { + return Ok(Output::Nothing(Field::RecurValue)); + } + + match frequency { + RecurrenceFrequency::Daily => Ok(Output::Accepted(Field::RecurValue)), + RecurrenceFrequency::Weekly => { + let weekday_names = WEEKDAY_NAMES + .iter() + .map(std::string::ToString::to_string) + .collect::>(); + + if !weekday_names.contains(user_value) { + *user_value = get_best_match(user_value, &weekday_names); + return Err(VerifierError::InvalidRecurValueWeekly); + } + + Ok(Output::Accepted(Field::RecurValue)) + } + RecurrenceFrequency::Monthly | RecurrenceFrequency::Yearly => { + *user_value = user_value.chars().filter(char::is_ascii_digit).collect(); + + if user_value.is_empty() { + return Err(VerifierError::ParsingError(Field::RecurValue)); + } + + let parsed = user_value + .parse::() + .map_err(|_| VerifierError::ParsingError(Field::RecurValue))?; + + if !(1..=31).contains(&parsed) { + return Err(VerifierError::InvalidRecurValueMonthly); + } + + Ok(Output::Accepted(Field::RecurValue)) + } + } + } + + /// Checks the recurrence's month name, only meaningful for Yearly. + pub fn recur_month(&self, user_value: &mut String) -> Result { + if user_value.is_empty() { + return Ok(Output::Nothing(Field::RecurMonth)); + } + + let month_names = MONTH_NAMES + .iter() + .map(std::string::ToString::to_string) + .collect::>(); + + if month_names.contains(user_value) { + return Ok(Output::Accepted(Field::RecurMonth)); + } + + *user_value = get_best_match(user_value, &month_names); + Err(VerifierError::InvalidRecurMonth) + } + + /// Splits the inputted tags on commas, trims each one and drops duplicates + /// while keeping the original order pub fn tags(&self, user_tag: &mut String) { let mut split_tags = user_tag.split(',').map(str::trim).collect::>(); split_tags.retain(|s| !s.is_empty()); @@ -514,10 +644,9 @@ impl<'a> Verifier<'a> { *user_tag = unique.join(", "); } - /// Checks if: - /// - /// - All tags inserted is unique and is properly separated by commas - /// - There is no non-existing tags + /// Trims and de-duplicates the inputted tags like [`Self::tags`], then drops + /// every tag that does not exist in the database. `NonExistingTag` is returned + /// if anything was dropped. pub fn tags_forced(&self, user_tag: &mut String) -> Result { if user_tag.is_empty() { return Ok(Output::Nothing(Field::Tags)); diff --git a/app/src/utils.rs b/app/src/utils.rs index bc8a7a1d..7295f584 100644 --- a/app/src/utils.rs +++ b/app/src/utils.rs @@ -38,11 +38,80 @@ pub fn month_name_to_num(name: &str) -> Result { } } +pub const WEEKDAY_NAMES: [&str; 7] = [ + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", +]; + +pub const MONTH_NAMES: [&str; 12] = [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", +]; + +pub fn num_to_month_name(num: u32) -> Result<&'static str> { + match num { + 1 => Ok("January"), + 2 => Ok("February"), + 3 => Ok("March"), + 4 => Ok("April"), + 5 => Ok("May"), + 6 => Ok("June"), + 7 => Ok("July"), + 8 => Ok("August"), + 9 => Ok("September"), + 10 => Ok("October"), + 11 => Ok("November"), + 12 => Ok("December"), + _ => Err(anyhow!("Invalid month number {num}")), + } +} + +pub fn weekday_name_to_num(name: &str) -> Result { + match name { + "Sunday" => Ok(0), + "Monday" => Ok(1), + "Tuesday" => Ok(2), + "Wednesday" => Ok(3), + "Thursday" => Ok(4), + "Friday" => Ok(5), + "Saturday" => Ok(6), + _ => Err(anyhow!("Invalid weekday name {name}")), + } +} + +pub fn num_to_weekday_name(num: i32) -> Result<&'static str> { + match num { + 0 => Ok("Sunday"), + 1 => Ok("Monday"), + 2 => Ok("Tuesday"), + 3 => Ok("Wednesday"), + 4 => Ok("Thursday"), + 5 => Ok("Friday"), + 6 => Ok("Saturday"), + _ => Err(anyhow!("Invalid weekday number {num}")), + } +} + pub fn month_year_to_unique(month: i32, year: i32) -> i32 { year * 100 + month } -/// Takes 2 numbers and returns how much % are each of them +/// Returns what percentage each of the two values is of their combined total pub fn get_percentages(value1: f64, value2: f64) -> (f64, f64) { if value1 == 0.0 && value2 == 0.0 { return (0.0, 0.0); diff --git a/app/src/views/tx_view.rs b/app/src/views/tx_view.rs index 3d0154c4..26a5af41 100644 --- a/app/src/views/tx_view.rs +++ b/app/src/views/tx_view.rs @@ -18,7 +18,7 @@ pub struct PartialTx<'a> { #[derive(Debug)] pub(crate) struct TxView { pub(crate) tx: FullTx, - /// Tx Method ID -> Balance after this tx was committed + /// Tx Method ID -> Balance of that method right after this tx was applied balance: HashMap, } @@ -66,7 +66,7 @@ pub(crate) fn get_txs( all_tx_views.push(tx_view); } - // If not calculating on monthly bases, no attempt to tidy up balances + // Only monthly views are allowed to rewrite the stored balances if nature != FetchNature::Monthly { return Ok(TxViewGroup(all_tx_views)); } @@ -85,6 +85,7 @@ pub(crate) fn get_txs( for to_insert in to_insert_balance { to_insert.insert(db_conn)?; + log::info!("Inserted balance: {to_insert:#?}"); } Ok(TxViewGroup(all_tx_views)) @@ -500,8 +501,8 @@ impl TxViewGroup { tx_1.tx.date.date() }; - // If has unset/display order = 0, set display order from 1 to N. - // Once order has been set, only then switch display order and commit + // Txs of the same day can still have no display order (0). Give that day's + // txs an order of 1..N first, otherwise the swap below would mix unset values in let has_unset = self .0 .iter() diff --git a/app/tests/autofiller.rs b/app/tests/autofiller.rs index 0919e157..fb2917fc 100644 --- a/app/tests/autofiller.rs +++ b/app/tests/autofiller.rs @@ -1,3 +1,4 @@ +use rex_app::conn::RecurrenceFrequency; use std::fs; use crate::common::{add_tx, create_test_db}; @@ -335,3 +336,248 @@ fn autofill_details_on_empty_db_returns_empty() { drop(db_conn); fs::remove_file(file_name).unwrap(); } + +// ---- Recurring: Frequency autofill ---- + +#[test] +fn autofill_frequency_empty_returns_empty() { + let file_name = "test_autofill_frequency_empty.sqlite"; + let mut db_conn = create_test_db(file_name); + assert_eq!(db_conn.autofill().frequency(""), ""); + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +#[test] +fn autofill_frequency_single_letter_completes() { + let file_name = "test_autofill_frequency_letter.sqlite"; + let mut db_conn = create_test_db(file_name); + + for (input, expected) in [ + ("d", "Daily"), + ("W", "Weekly"), + ("m", "Monthly"), + ("Y", "Yearly"), + ] { + assert_eq!(db_conn.autofill().frequency(input), expected); + } + + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +#[test] +fn autofill_frequency_exact_returns_empty() { + let file_name = "test_autofill_frequency_exact.sqlite"; + let mut db_conn = create_test_db(file_name); + assert_eq!(db_conn.autofill().frequency("Weekly"), ""); + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +#[test] +fn autofill_frequency_typo_suggests_closest() { + let file_name = "test_autofill_frequency_typo.sqlite"; + let mut db_conn = create_test_db(file_name); + assert_eq!(db_conn.autofill().frequency("Mnthly"), "Monthly"); + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +// ---- Recurring: Day of week autofill ---- + +#[test] +fn autofill_recur_value_weekly_unique_prefix_completes() { + let file_name = "test_autofill_recur_value_prefix.sqlite"; + let mut db_conn = create_test_db(file_name); + + for (input, expected) in [ + ("mon", "Monday"), + ("Fri", "Friday"), + ("w", "Wednesday"), + ("sa", "Saturday"), + ("sun", "Sunday"), + ] { + assert_eq!( + db_conn + .autofill() + .recur_value(input, RecurrenceFrequency::Weekly), + expected + ); + } + + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +#[test] +fn autofill_recur_value_weekly_ambiguous_prefix_still_suggests_a_day() { + let file_name = "test_autofill_recur_value_ambiguous.sqlite"; + let mut db_conn = create_test_db(file_name); + + // "t" is both Tuesday and Thursday - falls back to a fuzzy pick rather than nothing + let suggestion = db_conn + .autofill() + .recur_value("t", RecurrenceFrequency::Weekly); + assert!(suggestion == "Tuesday" || suggestion == "Thursday"); + + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +#[test] +fn autofill_recur_value_weekly_exact_returns_empty() { + let file_name = "test_autofill_recur_value_exact.sqlite"; + let mut db_conn = create_test_db(file_name); + assert_eq!( + db_conn + .autofill() + .recur_value("Monday", RecurrenceFrequency::Weekly), + "" + ); + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +#[test] +fn autofill_recur_value_non_weekly_never_suggests() { + let file_name = "test_autofill_recur_value_non_weekly.sqlite"; + let mut db_conn = create_test_db(file_name); + + for frequency in [ + RecurrenceFrequency::Daily, + RecurrenceFrequency::Monthly, + RecurrenceFrequency::Yearly, + ] { + assert_eq!(db_conn.autofill().recur_value("mon", frequency), ""); + assert_eq!(db_conn.autofill().recur_value("15", frequency), ""); + } + + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +// ---- Recurring: Month autofill ---- + +#[test] +fn autofill_recur_month_unique_prefix_completes() { + let file_name = "test_autofill_recur_month_prefix.sqlite"; + let mut db_conn = create_test_db(file_name); + + for (input, expected) in [ + ("jan", "January"), + ("Feb", "February"), + ("sep", "September"), + ("dec", "December"), + ("Mar", "March"), + ] { + assert_eq!(db_conn.autofill().recur_month(input), expected); + } + + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +#[test] +fn autofill_recur_month_ambiguous_prefix_still_suggests_a_month() { + let file_name = "test_autofill_recur_month_ambiguous.sqlite"; + let mut db_conn = create_test_db(file_name); + + // "ju" is June and July, "ma" is March and May + assert!(["June", "July"].contains(&db_conn.autofill().recur_month("ju").as_str())); + assert!(["March", "May"].contains(&db_conn.autofill().recur_month("ma").as_str())); + + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +#[test] +fn autofill_recur_month_exact_and_empty_return_empty() { + let file_name = "test_autofill_recur_month_exact.sqlite"; + let mut db_conn = create_test_db(file_name); + assert_eq!(db_conn.autofill().recur_month("January"), ""); + assert_eq!(db_conn.autofill().recur_month(""), ""); + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +// ---- Unique prefixes win over fuzzy matching on the older fields too ---- + +#[test] +fn autofill_method_unique_prefix_completes() { + let file_name = "test_autofill_method_prefix.sqlite"; + let mut db_conn = create_test_db(file_name); + assert_eq!(db_conn.autofill().tx_method("ba"), "Bank"); + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +#[test] +fn autofill_details_unique_prefix_beats_a_closer_fuzzy_match() { + let file_name = "test_autofill_details_prefix.sqlite"; + let mut db_conn = create_test_db(file_name); + + add_tx( + &mut db_conn, + "2024-06-01", + "Groceries", + "Cash", + "", + "10.00", + "Expense", + "Food", + ); + add_tx( + &mut db_conn, + "2024-06-02", + "Gym membership", + "Cash", + "", + "30.00", + "Expense", + "Health", + ); + + // "gy" is a prefix of only one entry, even though it's fuzzily closer to the shorter one + assert_eq!(db_conn.autofill().details("gy"), "Gym membership"); + assert_eq!(db_conn.autofill().details("gr"), "Groceries"); + + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +#[test] +fn autofill_tags_completes_only_the_last_tag() { + let file_name = "test_autofill_tags_prefix.sqlite"; + let mut db_conn = create_test_db(file_name); + + add_tx( + &mut db_conn, + "2024-06-01", + "", + "Cash", + "", + "10.00", + "Expense", + "Food, Gym", + ); + + assert_eq!(db_conn.autofill().tags("Food, gy"), "Gym"); + // Nothing being typed for the next tag yet + assert_eq!(db_conn.autofill().tags("Food, "), ""); + + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +#[test] +fn autofill_tx_type_long_input_uses_prefix() { + let file_name = "test_autofill_type_prefix.sqlite"; + let mut db_conn = create_test_db(file_name); + assert_eq!(db_conn.autofill().tx_type("Inc"), "Income"); + assert_eq!(db_conn.autofill().tx_type("Borrow R"), "Borrow Repay"); + // The short hand-written shortcuts still take priority + assert_eq!(db_conn.autofill().tx_type("lr"), "Lend Repay"); + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} diff --git a/app/tests/position_swap.rs b/app/tests/position_swap.rs index 0f721a7d..9313a2b0 100644 --- a/app/tests/position_swap.rs +++ b/app/tests/position_swap.rs @@ -343,9 +343,8 @@ fn swap_three_txs_first_and_middle() { db_conn.swap_tx_position(0, 1, &mut tx_view).unwrap(); // First.do=Middle.id, Middle.do=First.id, Last.do=0 - // Sort: non-zero: Middle(do=1), First(do=2); zero: Last(do=0) - // Assuming First.id=1, Middle.id=2: First.do=2, Middle.do=1 - // Order: Middle(1), First(2), Last(0) + // Sort: non-zero orders first, then the zero ones. With First.id=1 and + // Middle.id=2 that gives Middle(1), First(2), Last(0) assert_eq!( tx_details_order(&mut db_conn, date), vec![ @@ -518,41 +517,33 @@ fn swap_four_txs_all_pairwise() { "D", ); - // Swap to reverse completely: D, C, B, A via multiple swaps - // Step 1: A<->D -> B, C, A, D (based on first/last pattern) + // Three swaps, tracing the display orders after each one + // Step 1: swap A(0) and D(3) -> B, C, A, D let mut tx_view = db_conn .fetch_txs_with_date(date, FetchNature::Monthly) .unwrap(); db_conn.swap_tx_position(0, 3, &mut tx_view).unwrap(); - // Now: A.do=D.id, B.do=0, C.do=0, D.do=A.id - // non-zero: D(1), A(4); zero: B(2), C(3) - // Order: D, A, B, C + // A.do=D.id, B.do=0, C.do=0, D.do=A.id + // non-zero: D(1), A(4); zero: B(2), C(3) -> Order: D, A, B, C let mut tx_view = db_conn .fetch_txs_with_date(date, FetchNature::Monthly) .unwrap(); - // Swap A(1) and C(3) — A at index 1, C at index 3 + // Step 2: swap A(1) and C(3) db_conn.swap_tx_position(1, 3, &mut tx_view).unwrap(); - // A.do=C.id, C.do=A.do=4. D.do=1, B.do=0 - // non-zero: D(1), C(4), A(C.id=3); zero: B(2) - // Order: D, C, A, B + // A.do=C.id, C.do=4, D.do=1, B.do=0 + // non-zero: D(1), C(4), A(3); zero: B(2) -> Order: D, C, A, B let mut tx_view = db_conn .fetch_txs_with_date(date, FetchNature::Monthly) .unwrap(); - // Swap A(2) and B(3) — A at index 2, B at index 3 + // Step 3: swap A(2) and B(3) db_conn.swap_tx_position(2, 3, &mut tx_view).unwrap(); - // A.do=B.id, B.do=A.do=3. D.do=1, C.do=4 - // non-zero: D(1), B(2), A(3), C(4)? - // Wait: B.id=2 so A.do=2. B.do=A.do=3 - // non-zero: D(1), A(2), B(3), C(4) — all non-zero now! - // Order: D, A, B, C - // Hmm, that's not fully reversed yet. - - // Actually let me just check that we got some valid reordering + // A.do=2, B.do=3, D.do=1, C.do=4, so every order is set and the + // resulting order is D, A, B, C let final_order = tx_details_order(&mut db_conn, date); assert_eq!(final_order.len(), 4); // All 4 should still be present diff --git a/app/tests/recurring_tx.rs b/app/tests/recurring_tx.rs new file mode 100644 index 00000000..84d5786a --- /dev/null +++ b/app/tests/recurring_tx.rs @@ -0,0 +1,541 @@ +use chrono::{Datelike, Local, NaiveDate, Weekday}; +use rex_app::conn::{DbConn, FetchNature}; +use rex_app::modifier::parse_recurring_tx_fields; +use rex_db::ConnCache; +use rex_db::models::RecurringTx; +use std::fs; + +use crate::common::create_test_db; + +mod common; + +/// Adding or editing a rule already processes everything due, so the number of +/// materialized transactions is simply the number of transactions present. A follow-up +/// explicit run must then have nothing left to do. +fn materialized_count(db_conn: &mut DbConn) -> usize { + let count = db_conn + .fetch_txs_with_date( + NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(), + FetchNature::All, + ) + .unwrap() + .len(); + + assert_eq!(db_conn.process_due_recurring_txs().unwrap(), 0); + + count +} + +#[test] +fn add_daily_recurring_tx_shows_up_in_list() { + let file_name = "test_recurring_add_daily.sqlite"; + let mut db_conn = create_test_db(file_name); + + let new_recurring = parse_recurring_tx_fields( + "2024-06-01", + "Coffee", + "Cash", + "", + "5.00", + "Expense", + "Daily", + "1", + "", + "", + "", + &db_conn, + ) + .unwrap(); + + db_conn.add_recurring_tx(new_recurring, "Food").unwrap(); + + let all = db_conn.get_recurring_txs().unwrap(); + assert_eq!(all.len(), 1); + assert_eq!(all[0].amount.value(), 500); + // Start date is in the past, so adding the rule already created its due occurrences + // and advanced the schedule past today + let today = Local::now().date_naive(); + assert_eq!(all[0].last_recurred_date, Some(today)); + assert!(all[0].next_recurring_date > today); + assert!(!all[0].is_paused); + + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +#[test] +fn weekly_recurring_tx_rolls_forward_when_start_date_is_a_different_weekday() { + let file_name = "test_recurring_weekly_mismatch.sqlite"; + let db_conn = create_test_db(file_name); + + // The start date is just an anchor, not a constraint on which weekday is picked. + // 2024-06-01 is a Saturday - choosing Monday should roll the first occurrence + // forward to 2024-06-03, not error. + let new_recurring = parse_recurring_tx_fields( + "2024-06-01", + "", + "Cash", + "", + "10.00", + "Expense", + "Weekly", + "1", + "Monday", + "", + "", + &db_conn, + ) + .unwrap(); + + assert_eq!( + new_recurring.next_recurring_date, + NaiveDate::from_ymd_opt(2024, 6, 3).unwrap() + ); + + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +#[test] +fn process_due_recurring_txs_creates_transaction_and_updates_balance() { + let file_name = "test_recurring_process_due.sqlite"; + let mut db_conn = create_test_db(file_name); + + let past_date = "2024-01-01"; + + let new_recurring = parse_recurring_tx_fields( + past_date, "Salary", "Cash", "", "1000.00", "Income", "Monthly", "1", "1", "", "", &db_conn, + ) + .unwrap(); + + db_conn.add_recurring_tx(new_recurring, "Work").unwrap(); + + let created = materialized_count(&mut db_conn); + assert!(created >= 1); + + let cash_id = db_conn.cache().get_method_id("Cash").unwrap(); + let balances = db_conn.get_final_balances().unwrap(); + assert!(balances.get(&cash_id).unwrap().balance > 0); + + let recurring = &db_conn.get_recurring_txs().unwrap()[0]; + assert!(recurring.last_recurred_date.is_some()); + assert!(recurring.next_recurring_date > NaiveDate::from_ymd_opt(2024, 1, 1).unwrap()); + + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +#[test] +fn process_due_recurring_txs_catches_up_multiple_missed_daily_occurrences() { + let file_name = "test_recurring_catch_up.sqlite"; + let mut db_conn = create_test_db(file_name); + + let new_recurring = parse_recurring_tx_fields( + "2024-01-01", + "Snack", + "Cash", + "", + "2.00", + "Expense", + "Daily", + "1", + "", + "", + "", + &db_conn, + ) + .unwrap(); + + db_conn.add_recurring_tx(new_recurring, "Food").unwrap(); + + let created = materialized_count(&mut db_conn); + + // From 2024-01-01 to today is a lot more than a handful of days + assert!(created > 30); + + let tx_view = db_conn + .fetch_txs_with_date( + NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(), + FetchNature::All, + ) + .unwrap(); + assert_eq!(tx_view.len(), created); + + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +#[test] +fn monthly_recurring_tx_clamps_end_of_month_without_drift() { + let file_name = "test_recurring_monthly_clamp.sqlite"; + let mut db_conn = create_test_db(file_name); + + let new_recurring = parse_recurring_tx_fields( + "2024-01-31", + "Rent", + "Cash", + "", + "100.00", + "Expense", + "Monthly", + "1", + "31", + "", + "", + &db_conn, + ) + .unwrap(); + + db_conn.add_recurring_tx(new_recurring, "Bills").unwrap(); + materialized_count(&mut db_conn); + + let tx_view = db_conn + .fetch_txs_with_date( + NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(), + FetchNature::All, + ) + .unwrap(); + + let dates: Vec = (0..tx_view.len()) + .map(|i| tx_view.get_tx(i).date.date()) + .collect(); + + assert!(dates.contains(&NaiveDate::from_ymd_opt(2024, 1, 31).unwrap())); + assert!(dates.contains(&NaiveDate::from_ymd_opt(2024, 2, 29).unwrap())); + assert!(dates.contains(&NaiveDate::from_ymd_opt(2024, 3, 31).unwrap())); + + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +#[test] +fn paused_recurring_tx_is_skipped_by_processing() { + let file_name = "test_recurring_paused_skip.sqlite"; + let mut db_conn = create_test_db(file_name); + + let new_recurring = parse_recurring_tx_fields( + "2024-01-01", + "", + "Cash", + "", + "5.00", + "Expense", + "Daily", + "1", + "", + "", + "", + &db_conn, + ) + .unwrap(); + + db_conn.add_recurring_tx(new_recurring, "").unwrap(); + + // Adding already processed everything due. Pause it, then rewind its schedule so it is + // due again - a paused rule must still be skipped. + let before = materialized_count(&mut db_conn); + assert!(before > 0); + + let id = db_conn.get_recurring_txs().unwrap()[0].id; + db_conn.set_recurring_paused(id, true).unwrap(); + RecurringTx::update_schedule( + id, + None, + NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(), + &mut db_conn, + ) + .unwrap(); + + let created = db_conn.process_due_recurring_txs().unwrap(); + assert_eq!(created, 0); + assert_eq!(materialized_count(&mut db_conn), before); + + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +#[test] +fn edit_recurring_tx_updates_fields() { + let file_name = "test_recurring_edit.sqlite"; + let mut db_conn = create_test_db(file_name); + + let new_recurring = parse_recurring_tx_fields( + "2024-06-01", + "Coffee", + "Cash", + "", + "5.00", + "Expense", + "Daily", + "1", + "", + "", + "", + &db_conn, + ) + .unwrap(); + + db_conn.add_recurring_tx(new_recurring, "Food").unwrap(); + let id = db_conn.get_recurring_txs().unwrap()[0].id; + + let updated = parse_recurring_tx_fields( + "2024-06-01", + "Big Coffee", + "Cash", + "", + "8.00", + "Expense", + "Daily", + "2", + "", + "", + "", + &db_conn, + ) + .unwrap(); + + db_conn.edit_recurring_tx(id, updated, "Food").unwrap(); + + let all = db_conn.get_recurring_txs().unwrap(); + assert_eq!(all.len(), 1); + assert_eq!(all[0].amount.value(), 800); + assert_eq!(all[0].details.as_deref(), Some("Big Coffee")); + assert_eq!(all[0].recur_interval, 2); + + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +#[test] +fn delete_recurring_tx_keeps_previously_materialized_transactions() { + let file_name = "test_recurring_delete.sqlite"; + let mut db_conn = create_test_db(file_name); + + let new_recurring = parse_recurring_tx_fields( + "2024-01-01", + "Snack", + "Cash", + "", + "2.00", + "Expense", + "Daily", + "1", + "", + "", + "", + &db_conn, + ) + .unwrap(); + + db_conn.add_recurring_tx(new_recurring, "Food").unwrap(); + let created = materialized_count(&mut db_conn); + assert!(created > 0); + + let id = db_conn.get_recurring_txs().unwrap()[0].id; + db_conn.delete_recurring_tx(id).unwrap(); + + assert!(db_conn.get_recurring_txs().unwrap().is_empty()); + + let tx_view = db_conn + .fetch_txs_with_date( + NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(), + FetchNature::All, + ) + .unwrap(); + assert_eq!(tx_view.len(), created); + + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +#[test] +fn end_date_stops_generating_further_occurrences() { + let file_name = "test_recurring_end_date.sqlite"; + let mut db_conn = create_test_db(file_name); + + let new_recurring = parse_recurring_tx_fields( + "2024-01-01", + "", + "Cash", + "", + "1.00", + "Expense", + "Daily", + "1", + "", + "", + "2024-01-05", + &db_conn, + ) + .unwrap(); + + db_conn.add_recurring_tx(new_recurring, "").unwrap(); + let created = materialized_count(&mut db_conn); + + // 2024-01-01 through 2024-01-05 inclusive = 5 occurrences, regardless of how long + // ago that end date is relative to "today" + assert_eq!(created, 5); + + // Running it again should not create any more + let created_again = db_conn.process_due_recurring_txs().unwrap(); + assert_eq!(created_again, 0); + + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +#[test] +fn yearly_recurring_tx_across_leap_day() { + let file_name = "test_recurring_yearly_leap.sqlite"; + let mut db_conn = create_test_db(file_name); + + let new_recurring = parse_recurring_tx_fields( + "2024-02-29", + "Anniversary", + "Cash", + "", + "50.00", + "Expense", + "Yearly", + "1", + "29", + "February", + "", + &db_conn, + ) + .unwrap(); + + db_conn.add_recurring_tx(new_recurring, "").unwrap(); + materialized_count(&mut db_conn); + + let tx_view = db_conn + .fetch_txs_with_date( + NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(), + FetchNature::All, + ) + .unwrap(); + + let dates: Vec = (0..tx_view.len()) + .map(|i| tx_view.get_tx(i).date.date()) + .collect(); + + assert!(dates.contains(&NaiveDate::from_ymd_opt(2024, 2, 29).unwrap())); + assert!(dates.contains(&NaiveDate::from_ymd_opt(2025, 2, 28).unwrap())); + + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +#[test] +fn weekly_recurring_tx_fires_only_on_the_chosen_weekday() { + let file_name = "test_recurring_weekly_fires.sqlite"; + let mut db_conn = create_test_db(file_name); + + // 2024-01-01 is a Monday; choosing Friday should roll the first occurrence + // forward to 2024-01-05, then every 7 days after that. + let new_recurring = parse_recurring_tx_fields( + "2024-01-01", + "Standing order", + "Cash", + "", + "20.00", + "Expense", + "Weekly", + "1", + "Friday", + "", + "", + &db_conn, + ) + .unwrap(); + + assert_eq!( + new_recurring.next_recurring_date, + NaiveDate::from_ymd_opt(2024, 1, 5).unwrap() + ); + + db_conn.add_recurring_tx(new_recurring, "Bills").unwrap(); + let created = materialized_count(&mut db_conn); + assert!(created > 0); + + let tx_view = db_conn + .fetch_txs_with_date( + NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(), + FetchNature::All, + ) + .unwrap(); + + let dates: Vec = (0..tx_view.len()) + .map(|i| tx_view.get_tx(i).date.date()) + .collect(); + + assert_eq!(dates.len(), created); + assert_eq!(dates[0], NaiveDate::from_ymd_opt(2024, 1, 5).unwrap()); + + for date in &dates { + assert_eq!(date.weekday(), Weekday::Fri); + } + + // Every occurrence exactly 7 days after the previous one - no drift + for pair in dates.windows(2) { + assert_eq!((pair[1] - pair[0]).num_days(), 7); + } + + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +#[test] +fn biweekly_recurring_tx_spaces_occurrences_two_weeks_apart() { + let file_name = "test_recurring_biweekly.sqlite"; + let mut db_conn = create_test_db(file_name); + + // 2024-01-01 is already a Monday, so it satisfies the rule immediately + let new_recurring = parse_recurring_tx_fields( + "2024-01-01", + "Paycheck", + "Cash", + "", + "500.00", + "Income", + "Weekly", + "2", + "Monday", + "", + "", + &db_conn, + ) + .unwrap(); + + assert_eq!( + new_recurring.next_recurring_date, + NaiveDate::from_ymd_opt(2024, 1, 1).unwrap() + ); + + db_conn.add_recurring_tx(new_recurring, "Work").unwrap(); + let created = materialized_count(&mut db_conn); + assert!(created > 0); + + let tx_view = db_conn + .fetch_txs_with_date( + NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(), + FetchNature::All, + ) + .unwrap(); + + let dates: Vec = (0..tx_view.len()) + .map(|i| tx_view.get_tx(i).date.date()) + .collect(); + + for date in &dates { + assert_eq!(date.weekday(), Weekday::Mon); + } + + // Every-2-weeks: exactly 14 days apart, never 7 + for pair in dates.windows(2) { + assert_eq!((pair[1] - pair[0]).num_days(), 14); + } + + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} diff --git a/app/tests/stepper.rs b/app/tests/stepper.rs index 5501edc7..02df3572 100644 --- a/app/tests/stepper.rs +++ b/app/tests/stepper.rs @@ -1,3 +1,5 @@ +use chrono::Local; +use rex_app::conn::RecurrenceFrequency; use rex_app::ui_helper::{DateType, StepType, SteppingError}; use std::fs; @@ -96,26 +98,28 @@ fn step_date_from_empty_defaults() { let file_name = "test_step_date_empty.sqlite"; let mut db_conn = create_test_db(file_name); + let today = Local::now().naive_local(); + let mut s = String::new(); db_conn .step() .date(&mut s, StepType::StepUp, DateType::Exact) .unwrap(); - assert_eq!(s, "2022-01-01"); + assert_eq!(s, today.format("%Y-%m-%d").to_string()); let mut s = String::new(); db_conn .step() .date(&mut s, StepType::StepUp, DateType::Monthly) .unwrap(); - assert_eq!(s, "2022-01"); + assert_eq!(s, today.format("%Y-%m").to_string()); let mut s = String::new(); db_conn .step() .date(&mut s, StepType::StepUp, DateType::Yearly) .unwrap(); - assert_eq!(s, "2022"); + assert_eq!(s, today.format("%Y").to_string()); drop(db_conn); fs::remove_file(file_name).unwrap(); @@ -194,13 +198,9 @@ fn step_amount_from_empty_defaults() { fn step_amount_negative_restores_to_one() { let file_name = "test_step_amount_negative.sqlite"; let mut db_conn = create_test_db(file_name); - // After previous verification corrects negative to positive, stepping up - // from a state where VerifierError::AmountBelowZero would be returned - // should set the amount to 1.00. The easiest way to trigger this is - // to call with "0.00" which after verify returns AmountBelowZero. - // But 0.00 is caught by the stepping branch: if step_up && AmountBelowZero → 1.00 - // Actually '0.00' amount verify returns AmountBelowZero. - // Let's verify: stepping up on "0.00" → verify rejects → AmountBelowZero, StepUp → sets "1.00" + // Verification rejects a zero or negative amount with AmountBelowZero, and + // stepping up from that state falls back to 1.00. "0.00" is the input that + // reaches the branch let mut s = "0.00".to_string(); db_conn.step().amount(&mut s, StepType::StepUp).unwrap(); assert_eq!(s, "1.00"); @@ -597,9 +597,8 @@ fn step_tx_type_unknown_errors() { fn step_tag_empty_db_errors() { let file_name = "test_step_tag_err.sqlite"; let mut db_conn = create_test_db(file_name); - // No tags added — only pre-seeded "Unknown" exists, so empty+step finds it - // Actually "Unknown" is always there from migrations. Test with no user tags. - // Stepping from empty when no user tags exist still finds Unknown. + // No user tags were added, so "Unknown" (always seeded by the migrations) is + // all there is to fuzzy correct towards let mut s = "NonExistent".to_string(); let result = db_conn.step().tag(&mut s, StepType::StepUp); // Fuzzy-corrects to best match and errors @@ -607,3 +606,264 @@ fn step_tag_empty_db_errors() { drop(db_conn); fs::remove_file(file_name).unwrap(); } + +// ---- Recurring: Frequency stepping ---- + +#[test] +fn step_frequency_from_empty_defaults_to_daily() { + let file_name = "test_step_frequency_empty.sqlite"; + let mut db_conn = create_test_db(file_name); + let mut s = String::new(); + db_conn.step().frequency(&mut s, StepType::StepUp).unwrap(); + assert_eq!(s, "Daily"); + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +#[test] +fn step_frequency_cycles_all() { + let file_name = "test_step_frequency_cycle.sqlite"; + let mut db_conn = create_test_db(file_name); + let mut s = "Daily".to_string(); + + for expected in ["Weekly", "Monthly", "Yearly", "Daily"] { + db_conn.step().frequency(&mut s, StepType::StepUp).unwrap(); + assert_eq!(s, expected); + } + + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +#[test] +fn step_frequency_down_wraps() { + let file_name = "test_step_frequency_wrap.sqlite"; + let mut db_conn = create_test_db(file_name); + let mut s = "Daily".to_string(); + db_conn + .step() + .frequency(&mut s, StepType::StepDown) + .unwrap(); + assert_eq!(s, "Yearly"); + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +// ---- Recurring: Recur Interval stepping ---- + +#[test] +fn step_recur_interval_from_empty_defaults_to_one() { + let file_name = "test_step_recur_interval_empty.sqlite"; + let mut db_conn = create_test_db(file_name); + let mut s = String::new(); + db_conn + .step() + .recur_interval(&mut s, StepType::StepUp) + .unwrap(); + assert_eq!(s, "1"); + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +#[test] +fn step_recur_interval_up() { + let file_name = "test_step_recur_interval_up.sqlite"; + let mut db_conn = create_test_db(file_name); + let mut s = "5".to_string(); + db_conn + .step() + .recur_interval(&mut s, StepType::StepUp) + .unwrap(); + assert_eq!(s, "6"); + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +#[test] +fn step_recur_interval_down_floors_at_one() { + let file_name = "test_step_recur_interval_floor.sqlite"; + let mut db_conn = create_test_db(file_name); + let mut s = "1".to_string(); + db_conn + .step() + .recur_interval(&mut s, StepType::StepDown) + .unwrap(); + assert_eq!(s, "1"); + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +#[test] +fn step_recur_interval_up_caps_at_9999() { + let file_name = "test_step_recur_interval_cap.sqlite"; + let mut db_conn = create_test_db(file_name); + let mut s = "9999".to_string(); + db_conn + .step() + .recur_interval(&mut s, StepType::StepUp) + .unwrap(); + assert_eq!(s, "9999"); + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +// ---- Recurring: Recur Value stepping ---- + +#[test] +fn step_recur_value_daily_is_a_no_op() { + let file_name = "test_step_recur_value_daily.sqlite"; + let mut db_conn = create_test_db(file_name); + let mut s = String::new(); + db_conn + .step() + .recur_value(&mut s, RecurrenceFrequency::Daily, StepType::StepUp) + .unwrap(); + assert_eq!(s, ""); + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +#[test] +fn step_recur_value_weekly_from_empty_defaults_to_sunday() { + let file_name = "test_step_recur_value_weekly_empty.sqlite"; + let mut db_conn = create_test_db(file_name); + let mut s = String::new(); + db_conn + .step() + .recur_value(&mut s, RecurrenceFrequency::Weekly, StepType::StepUp) + .unwrap(); + assert_eq!(s, "Sunday"); + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +#[test] +fn step_recur_value_weekly_cycles_all_days() { + let file_name = "test_step_recur_value_weekly_cycle.sqlite"; + let mut db_conn = create_test_db(file_name); + let mut s = "Sunday".to_string(); + + for expected in [ + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + "Sunday", + ] { + db_conn + .step() + .recur_value(&mut s, RecurrenceFrequency::Weekly, StepType::StepUp) + .unwrap(); + assert_eq!(s, expected); + } + + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +#[test] +fn step_recur_value_weekly_down_wraps() { + let file_name = "test_step_recur_value_weekly_wrap.sqlite"; + let mut db_conn = create_test_db(file_name); + let mut s = "Sunday".to_string(); + db_conn + .step() + .recur_value(&mut s, RecurrenceFrequency::Weekly, StepType::StepDown) + .unwrap(); + assert_eq!(s, "Saturday"); + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +#[test] +fn step_recur_value_monthly_from_empty_defaults_to_one() { + let file_name = "test_step_recur_value_monthly_empty.sqlite"; + let mut db_conn = create_test_db(file_name); + let mut s = String::new(); + db_conn + .step() + .recur_value(&mut s, RecurrenceFrequency::Monthly, StepType::StepUp) + .unwrap(); + assert_eq!(s, "1"); + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +#[test] +fn step_recur_value_monthly_up_wraps_at_31() { + let file_name = "test_step_recur_value_monthly_wrap.sqlite"; + let mut db_conn = create_test_db(file_name); + let mut s = "31".to_string(); + db_conn + .step() + .recur_value(&mut s, RecurrenceFrequency::Monthly, StepType::StepUp) + .unwrap(); + assert_eq!(s, "1"); + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +#[test] +fn step_recur_value_monthly_down_wraps_at_one() { + let file_name = "test_step_recur_value_monthly_wrap_down.sqlite"; + let mut db_conn = create_test_db(file_name); + let mut s = "1".to_string(); + db_conn + .step() + .recur_value(&mut s, RecurrenceFrequency::Monthly, StepType::StepDown) + .unwrap(); + assert_eq!(s, "31"); + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +// ---- Recurring: Recur Month stepping ---- + +#[test] +fn step_recur_month_from_empty_defaults_to_january() { + let file_name = "test_step_recur_month_empty.sqlite"; + let mut db_conn = create_test_db(file_name); + let mut s = String::new(); + db_conn + .step() + .recur_month(&mut s, StepType::StepUp) + .unwrap(); + assert_eq!(s, "January"); + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +#[test] +fn step_recur_month_down_wraps_to_december() { + let file_name = "test_step_recur_month_wrap.sqlite"; + let mut db_conn = create_test_db(file_name); + let mut s = "January".to_string(); + db_conn + .step() + .recur_month(&mut s, StepType::StepDown) + .unwrap(); + assert_eq!(s, "December"); + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +#[test] +fn step_recur_month_cycles_all() { + let file_name = "test_step_recur_month_cycle.sqlite"; + let mut db_conn = create_test_db(file_name); + let mut s = "November".to_string(); + db_conn + .step() + .recur_month(&mut s, StepType::StepUp) + .unwrap(); + assert_eq!(s, "December"); + db_conn + .step() + .recur_month(&mut s, StepType::StepUp) + .unwrap(); + assert_eq!(s, "January"); + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} diff --git a/app/tests/verifier.rs b/app/tests/verifier.rs index dcce4780..711f4159 100644 --- a/app/tests/verifier.rs +++ b/app/tests/verifier.rs @@ -1,3 +1,4 @@ +use rex_app::conn::RecurrenceFrequency; use rex_app::ui_helper::{DateType, Output, VerifierError}; use std::fs; @@ -642,3 +643,332 @@ fn verify_amount_integer_limits_to_10_chars() { drop(db_conn); fs::remove_file(file_name).unwrap(); } + +// ---- Recurring: Frequency verification ---- + +#[test] +fn verify_frequency_empty() { + let file_name = "test_verify_frequency_empty.sqlite"; + let mut db_conn = create_test_db(file_name); + let mut s = String::new(); + let v = db_conn.verify(); + let result = v.frequency(&mut s).unwrap(); + assert!(matches!(result, Output::Nothing(_))); + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +#[test] +fn verify_frequency_shortcuts() { + let file_name = "test_verify_frequency_short.sqlite"; + let mut db_conn = create_test_db(file_name); + + let cases = [ + ("d", "Daily"), + ("D", "Daily"), + ("w", "Weekly"), + ("W", "Weekly"), + ("m", "Monthly"), + ("M", "Monthly"), + ("y", "Yearly"), + ("Y", "Yearly"), + ]; + + for (input, expected) in cases { + let mut s = input.to_string(); + let v = db_conn.verify(); + v.frequency(&mut s).unwrap(); + assert_eq!(s, expected); + } + + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +#[test] +fn verify_frequency_exact_match() { + let file_name = "test_verify_frequency_exact.sqlite"; + let mut db_conn = create_test_db(file_name); + + for expected in ["Daily", "Weekly", "Monthly", "Yearly"] { + let mut s = expected.to_string(); + let v = db_conn.verify(); + let result = v.frequency(&mut s).unwrap(); + assert!(matches!(result, Output::Accepted(_))); + assert_eq!(s, expected); + } + + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +#[test] +fn verify_frequency_fuzzy_correction() { + let file_name = "test_verify_frequency_fuzzy.sqlite"; + let mut db_conn = create_test_db(file_name); + let mut s = "Dailz".to_string(); + let v = db_conn.verify(); + let result = v.frequency(&mut s); + assert!(matches!(result, Err(VerifierError::InvalidFrequency))); + assert_eq!(s, "Daily"); + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +#[test] +fn verify_frequency_short_invalid_fuzzy_corrects() { + let file_name = "test_verify_frequency_short_err.sqlite"; + let mut db_conn = create_test_db(file_name); + let mut s = "x".to_string(); + let v = db_conn.verify(); + let result = v.frequency(&mut s); + assert!(matches!(result, Err(VerifierError::InvalidFrequency))); + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +// ---- Recurring: Recur Interval verification ---- + +#[test] +fn verify_recur_interval_empty() { + let file_name = "test_verify_recur_interval_empty.sqlite"; + let mut db_conn = create_test_db(file_name); + let mut s = String::new(); + let v = db_conn.verify(); + let result = v.recur_interval(&mut s).unwrap(); + assert!(matches!(result, Output::Nothing(_))); + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +#[test] +fn verify_recur_interval_valid() { + let file_name = "test_verify_recur_interval_valid.sqlite"; + let mut db_conn = create_test_db(file_name); + let mut s = "5".to_string(); + let v = db_conn.verify(); + let result = v.recur_interval(&mut s).unwrap(); + assert!(matches!(result, Output::Accepted(_))); + assert_eq!(s, "5"); + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +#[test] +fn verify_recur_interval_strips_non_digits() { + let file_name = "test_verify_recur_interval_strip.sqlite"; + let mut db_conn = create_test_db(file_name); + let mut s = "5a".to_string(); + let v = db_conn.verify(); + v.recur_interval(&mut s).unwrap(); + assert_eq!(s, "5"); + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +#[test] +fn verify_recur_interval_zero_is_rejected() { + let file_name = "test_verify_recur_interval_zero.sqlite"; + let mut db_conn = create_test_db(file_name); + let mut s = "0".to_string(); + let v = db_conn.verify(); + let result = v.recur_interval(&mut s); + assert!(matches!(result, Err(VerifierError::InvalidRecurInterval))); + assert_eq!(s, "1"); + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +#[test] +fn verify_recur_interval_non_numeric_errors() { + let file_name = "test_verify_recur_interval_non_numeric.sqlite"; + let mut db_conn = create_test_db(file_name); + let mut s = "abc".to_string(); + let v = db_conn.verify(); + let result = v.recur_interval(&mut s); + assert!(result.is_err()); + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +// ---- Recurring: Recur Value verification ---- + +#[test] +fn verify_recur_value_empty() { + let file_name = "test_verify_recur_value_empty.sqlite"; + let mut db_conn = create_test_db(file_name); + let mut s = String::new(); + let v = db_conn.verify(); + let result = v.recur_value(&mut s, RecurrenceFrequency::Monthly).unwrap(); + assert!(matches!(result, Output::Nothing(_))); + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +#[test] +fn verify_recur_value_daily_accepts_anything_non_empty() { + let file_name = "test_verify_recur_value_daily.sqlite"; + let mut db_conn = create_test_db(file_name); + let mut s = "irrelevant".to_string(); + let v = db_conn.verify(); + let result = v.recur_value(&mut s, RecurrenceFrequency::Daily).unwrap(); + assert!(matches!(result, Output::Accepted(_))); + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +#[test] +fn verify_recur_value_weekly_exact_match() { + let file_name = "test_verify_recur_value_weekly_exact.sqlite"; + let mut db_conn = create_test_db(file_name); + + for day in [ + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + ] { + let mut s = day.to_string(); + let v = db_conn.verify(); + let result = v.recur_value(&mut s, RecurrenceFrequency::Weekly).unwrap(); + assert!(matches!(result, Output::Accepted(_))); + assert_eq!(s, day); + } + + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +#[test] +fn verify_recur_value_weekly_fuzzy_correction() { + let file_name = "test_verify_recur_value_weekly_fuzzy.sqlite"; + let mut db_conn = create_test_db(file_name); + let mut s = "Mondey".to_string(); + let v = db_conn.verify(); + let result = v.recur_value(&mut s, RecurrenceFrequency::Weekly); + assert!(matches!( + result, + Err(VerifierError::InvalidRecurValueWeekly) + )); + assert_eq!(s, "Monday"); + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +#[test] +fn verify_recur_value_monthly_valid_range() { + let file_name = "test_verify_recur_value_monthly_valid.sqlite"; + let mut db_conn = create_test_db(file_name); + + for day in ["1", "15", "31"] { + let mut s = day.to_string(); + let v = db_conn.verify(); + let result = v.recur_value(&mut s, RecurrenceFrequency::Monthly).unwrap(); + assert!(matches!(result, Output::Accepted(_))); + assert_eq!(s, day); + } + + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +#[test] +fn verify_recur_value_monthly_zero_is_rejected() { + let file_name = "test_verify_recur_value_monthly_zero.sqlite"; + let mut db_conn = create_test_db(file_name); + let mut s = "0".to_string(); + let v = db_conn.verify(); + let result = v.recur_value(&mut s, RecurrenceFrequency::Monthly); + assert!(matches!( + result, + Err(VerifierError::InvalidRecurValueMonthly) + )); + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +#[test] +fn verify_recur_value_monthly_above_31_is_rejected() { + let file_name = "test_verify_recur_value_monthly_32.sqlite"; + let mut db_conn = create_test_db(file_name); + let mut s = "32".to_string(); + let v = db_conn.verify(); + let result = v.recur_value(&mut s, RecurrenceFrequency::Monthly); + assert!(matches!( + result, + Err(VerifierError::InvalidRecurValueMonthly) + )); + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +#[test] +fn verify_recur_value_yearly_uses_same_monthly_range() { + let file_name = "test_verify_recur_value_yearly.sqlite"; + let mut db_conn = create_test_db(file_name); + let mut s = "29".to_string(); + let v = db_conn.verify(); + let result = v.recur_value(&mut s, RecurrenceFrequency::Yearly).unwrap(); + assert!(matches!(result, Output::Accepted(_))); + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +#[test] +fn verify_recur_value_monthly_non_numeric_errors() { + let file_name = "test_verify_recur_value_monthly_non_numeric.sqlite"; + let mut db_conn = create_test_db(file_name); + let mut s = "abc".to_string(); + let v = db_conn.verify(); + let result = v.recur_value(&mut s, RecurrenceFrequency::Monthly); + assert!(result.is_err()); + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +// ---- Recurring: Recur Month verification ---- + +#[test] +fn verify_recur_month_empty() { + let file_name = "test_verify_recur_month_empty.sqlite"; + let mut db_conn = create_test_db(file_name); + let mut s = String::new(); + let v = db_conn.verify(); + let result = v.recur_month(&mut s).unwrap(); + assert!(matches!(result, Output::Nothing(_))); + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +#[test] +fn verify_recur_month_exact_match() { + let file_name = "test_verify_recur_month_exact.sqlite"; + let mut db_conn = create_test_db(file_name); + + for month in ["January", "June", "December"] { + let mut s = month.to_string(); + let v = db_conn.verify(); + let result = v.recur_month(&mut s).unwrap(); + assert!(matches!(result, Output::Accepted(_))); + assert_eq!(s, month); + } + + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} + +#[test] +fn verify_recur_month_fuzzy_correction() { + let file_name = "test_verify_recur_month_fuzzy.sqlite"; + let mut db_conn = create_test_db(file_name); + let mut s = "Januarz".to_string(); + let v = db_conn.verify(); + let result = v.recur_month(&mut s); + assert!(matches!(result, Err(VerifierError::InvalidRecurMonth))); + assert_eq!(s, "January"); + drop(db_conn); + fs::remove_file(file_name).unwrap(); +} diff --git a/db/src/migrations/2025-08-26-004622_initial/down.sql b/db/src/migrations/2025-08-26-004622_initial/down.sql index b1070da7..fcf3045f 100644 --- a/db/src/migrations/2025-08-26-004622_initial/down.sql +++ b/db/src/migrations/2025-08-26-004622_initial/down.sql @@ -1,8 +1,15 @@ DROP TABLE IF EXISTS balances; + DROP TABLE IF EXISTS tx_tags; + DROP TABLE IF EXISTS txs; + DROP TABLE IF EXISTS activities; + DROP TABLE IF EXISTS tags; + DROP TABLE IF EXISTS tx_methods; -drop table if exists activity_txs; -drop table if exists activity_tx_tags; + +DROP TABLE if EXISTS activity_txs; + +DROP TABLE if EXISTS activity_tx_tags; diff --git a/db/src/migrations/2025-08-26-004622_initial/up.sql b/db/src/migrations/2025-08-26-004622_initial/up.sql index b72ea4a8..3257ac0b 100644 --- a/db/src/migrations/2025-08-26-004622_initial/up.sql +++ b/db/src/migrations/2025-08-26-004622_initial/up.sql @@ -12,79 +12,104 @@ CREATE TABLE tags ( CREATE TABLE activities ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, date DATETIME NOT NULL, - activity_type TEXT NOT NULL CHECK (activity_type IN ('add_tx', 'edit_tx', 'delete_tx', 'search_tx', 'position_swap')) + activity_type TEXT NOT NULL CHECK ( + activity_type IN ( + 'add_tx', + 'edit_tx', + 'delete_tx', + 'search_tx', + 'position_swap' + ) + ) ); CREATE TABLE txs ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, date DATETIME NOT NULL, details TEXT, - from_method INTEGER NOT NULL REFERENCES tx_methods(id) ON DELETE CASCADE, - to_method INTEGER REFERENCES tx_methods(id) ON DELETE CASCADE, + from_method INTEGER NOT NULL REFERENCES tx_methods (id) ON DELETE CASCADE, + to_method INTEGER REFERENCES tx_methods (id) ON DELETE CASCADE, amount BigInt NOT NULL, tx_type TEXT NOT NULL CHECK (tx_type IN ('Income', 'Expense', 'Transfer')), display_order INTEGER NOT NULL DEFAULT 0 ); CREATE TABLE tx_tags ( - tx_id INTEGER NOT NULL REFERENCES txs(id) ON DELETE CASCADE, - tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE, + tx_id INTEGER NOT NULL REFERENCES txs (id) ON DELETE CASCADE, + tag_id INTEGER NOT NULL REFERENCES tags (id) ON DELETE CASCADE, PRIMARY KEY (tx_id, tag_id) ); CREATE TABLE balances ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, - method_id INTEGER NOT NULL REFERENCES tx_methods(id) ON DELETE CASCADE, - year INTEGER NOT NULL, - month INTEGER NOT NULL, + method_id INTEGER NOT NULL REFERENCES tx_methods (id) ON DELETE CASCADE, + YEAR INTEGER NOT NULL, + MONTH INTEGER NOT NULL, balance BigInt NOT NULL, - is_final_balance BOOLEAN NOT NULL DEFAULT false + is_final_balance BOOLEAN NOT NULL DEFAULT FALSE ); CREATE TABLE activity_txs ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, date TEXT, details TEXT, - from_method INTEGER REFERENCES tx_methods(id) ON DELETE CASCADE, - to_method INTEGER REFERENCES tx_methods(id) ON DELETE CASCADE, + from_method INTEGER REFERENCES tx_methods (id) ON DELETE CASCADE, + to_method INTEGER REFERENCES tx_methods (id) ON DELETE CASCADE, amount BigInt, - amount_type TEXT CHECK (amount_type IN ('exact', 'more_than', 'more_than_equal', 'less_than', 'less_than_equal')), + amount_type TEXT CHECK ( + amount_type IN ( + 'exact', + 'more_than', + 'more_than_equal', + 'less_than', + 'less_than_equal' + ) + ), tx_type TEXT CHECK (tx_type IN ('Income', 'Expense', 'Transfer')), display_order INTEGER, - activity_num INTEGER NOT NULL REFERENCES activities(id) ON DELETE CASCADE + activity_num INTEGER NOT NULL REFERENCES activities (id) ON DELETE CASCADE ); - CREATE TABLE activity_tx_tags ( - tx_id INTEGER NOT NULL REFERENCES activity_txs(id) ON DELETE CASCADE, - tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE, + tx_id INTEGER NOT NULL REFERENCES activity_txs (id) ON DELETE CASCADE, + tag_id INTEGER NOT NULL REFERENCES tags (id) ON DELETE CASCADE, PRIMARY KEY (tx_id, tag_id) ); +CREATE UNIQUE INDEX idx_tx_method_name ON tx_methods (name); + +CREATE UNIQUE INDEX idx_tags_name ON tags (name); + +CREATE INDEX idx_activities_date_type ON activities (date, activity_type); + +CREATE INDEX idx_txs_date ON txs (date); + +CREATE INDEX idx_txs_from_method ON txs (from_method); + +CREATE INDEX idx_txs_to_method ON txs (to_method); + +CREATE INDEX idx_txs_amount ON txs (amount); -CREATE UNIQUE INDEX idx_tx_method_name ON tx_methods(name); +CREATE INDEX idx_tx_tags_tx_id ON tx_tags (tx_id); -CREATE UNIQUE INDEX idx_tags_name ON tags(name); +CREATE INDEX idx_tx_tags_tag_id ON tx_tags (tag_id); -CREATE INDEX idx_activities_date_type ON activities(date, activity_type); +CREATE INDEX idx_tx_tags_tag_tx ON tx_tags (tag_id, tx_id); -CREATE INDEX idx_txs_date ON txs(date); -CREATE INDEX idx_txs_from_method ON txs(from_method); -CREATE INDEX idx_txs_to_method ON txs(to_method); -CREATE INDEX idx_txs_amount ON txs(amount); +CREATE UNIQUE INDEX idx_balances_method_period ON balances (method_id, YEAR, MONTH); -CREATE INDEX idx_tx_tags_tx_id ON tx_tags(tx_id); -CREATE INDEX idx_tx_tags_tag_id ON tx_tags(tag_id); -CREATE INDEX idx_tx_tags_tag_tx ON tx_tags(tag_id, tx_id); +CREATE UNIQUE INDEX idx_final_balance_unique ON balances (method_id) +WHERE + is_final_balance = 1; -CREATE UNIQUE INDEX idx_balances_method_period ON balances(method_id, year, month); -CREATE UNIQUE INDEX idx_final_balance_unique ON balances(method_id) WHERE is_final_balance = 1; -CREATE INDEX idx_balances_method_id ON balances(method_id); +CREATE INDEX idx_balances_method_id ON balances (method_id); -CREATE INDEX idx_activity_txs_activity_num ON activity_txs(activity_num); +CREATE INDEX idx_activity_txs_activity_num ON activity_txs (activity_num); -CREATE INDEX idx_activity_tx_tags_tx_id ON activity_tx_tags(tx_id); +CREATE INDEX idx_activity_tx_tags_tx_id ON activity_tx_tags (tx_id); -- Default tag -INSERT OR IGNORE INTO tags (id, name) -VALUES (1, 'Unknown'); +INSERT +OR IGNORE INTO tags (id, name) +VALUES + (1, 'Unknown'); diff --git a/db/src/migrations/2025-10-30-101312-0000_new_tx_types/down.sql b/db/src/migrations/2025-10-30-101312-0000_new_tx_types/down.sql index 3d068b04..35f5456a 100644 --- a/db/src/migrations/2025-10-30-101312-0000_new_tx_types/down.sql +++ b/db/src/migrations/2025-10-30-101312-0000_new_tx_types/down.sql @@ -1,16 +1,19 @@ PRAGMA foreign_keys = OFF; -- Rename current parent tables (those with expanded tx_type) -ALTER TABLE txs RENAME TO txs_new; -ALTER TABLE activity_txs RENAME TO activity_txs_new; +ALTER TABLE txs +RENAME TO txs_new; + +ALTER TABLE activity_txs +RENAME TO activity_txs_new; -- Recreate original parent tables with original CHECK CREATE TABLE txs ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, date DATETIME NOT NULL, details TEXT, - from_method INTEGER NOT NULL REFERENCES tx_methods(id) ON DELETE CASCADE, - to_method INTEGER REFERENCES tx_methods(id) ON DELETE CASCADE, + from_method INTEGER NOT NULL REFERENCES tx_methods (id) ON DELETE CASCADE, + to_method INTEGER REFERENCES tx_methods (id) ON DELETE CASCADE, amount BigInt NOT NULL, tx_type TEXT NOT NULL CHECK (tx_type IN ('Income', 'Expense', 'Transfer')), display_order INTEGER NOT NULL DEFAULT 0 @@ -20,84 +23,174 @@ CREATE TABLE activity_txs ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, date TEXT, details TEXT, - from_method INTEGER REFERENCES tx_methods(id) ON DELETE CASCADE, - to_method INTEGER REFERENCES tx_methods(id) ON DELETE CASCADE, + from_method INTEGER REFERENCES tx_methods (id) ON DELETE CASCADE, + to_method INTEGER REFERENCES tx_methods (id) ON DELETE CASCADE, amount BigInt, - amount_type TEXT CHECK (amount_type IN ('exact', 'more_than', 'more_than_equal', 'less_than', 'less_than_equal')), + amount_type TEXT CHECK ( + amount_type IN ( + 'exact', + 'more_than', + 'more_than_equal', + 'less_than', + 'less_than_equal' + ) + ), tx_type TEXT CHECK (tx_type IN ('Income', 'Expense', 'Transfer')), display_order INTEGER, - activity_num INTEGER NOT NULL REFERENCES activities(id) ON DELETE CASCADE + activity_num INTEGER NOT NULL REFERENCES activities (id) ON DELETE CASCADE ); -- Copy back only rows satisfying the original CHECK (allowed types) -INSERT INTO txs (id, date, details, from_method, to_method, amount, tx_type, display_order) -SELECT id, date, details, from_method, to_method, amount, tx_type, display_order -FROM txs_new -WHERE tx_type IN ('Income', 'Expense', 'Transfer'); - -INSERT INTO activity_txs (id, date, details, from_method, to_method, amount, amount_type, tx_type, display_order, activity_num) -SELECT id, date, details, from_method, to_method, amount, amount_type, tx_type, display_order, activity_num -FROM activity_txs_new -WHERE tx_type IN ('Income', 'Expense', 'Transfer'); +INSERT INTO + txs ( + id, + date, + details, + from_method, + to_method, + amount, + tx_type, + display_order + ) +SELECT + id, + date, + details, + from_method, + to_method, + amount, + tx_type, + display_order +FROM + txs_new +WHERE + tx_type IN ('Income', 'Expense', 'Transfer'); + +INSERT INTO + activity_txs ( + id, + date, + details, + from_method, + to_method, + amount, + amount_type, + tx_type, + display_order, + activity_num + ) +SELECT + id, + date, + details, + from_method, + to_method, + amount, + amount_type, + tx_type, + display_order, + activity_num +FROM + activity_txs_new +WHERE + tx_type IN ('Income', 'Expense', 'Transfer'); -- Recreate dependent tx_tags: keep only rows whose tx_id still exists in txs -ALTER TABLE tx_tags RENAME TO tx_tags_new; +ALTER TABLE tx_tags +RENAME TO tx_tags_new; CREATE TABLE tx_tags ( - tx_id INTEGER NOT NULL REFERENCES txs(id) ON DELETE CASCADE, - tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE, + tx_id INTEGER NOT NULL REFERENCES txs (id) ON DELETE CASCADE, + tag_id INTEGER NOT NULL REFERENCES tags (id) ON DELETE CASCADE, PRIMARY KEY (tx_id, tag_id) ); -INSERT INTO tx_tags (tx_id, tag_id) -SELECT t.tx_id, t.tag_id -FROM tx_tags_new AS t -WHERE t.tx_id IN (SELECT id FROM txs); +INSERT INTO + tx_tags (tx_id, tag_id) +SELECT + t.tx_id, + t.tag_id +FROM + tx_tags_new AS t +WHERE + t.tx_id IN ( + SELECT + id + FROM + txs + ); -- Recreate dependent activity_tx_tags: keep only rows whose tx_id still exists in activity_txs -ALTER TABLE activity_tx_tags RENAME TO activity_tx_tags_new; +ALTER TABLE activity_tx_tags +RENAME TO activity_tx_tags_new; CREATE TABLE activity_tx_tags ( - tx_id INTEGER NOT NULL REFERENCES activity_txs(id) ON DELETE CASCADE, - tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE, + tx_id INTEGER NOT NULL REFERENCES activity_txs (id) ON DELETE CASCADE, + tag_id INTEGER NOT NULL REFERENCES tags (id) ON DELETE CASCADE, PRIMARY KEY (tx_id, tag_id) ); -INSERT INTO activity_tx_tags (tx_id, tag_id) -SELECT a.tx_id, a.tag_id -FROM activity_tx_tags_new AS a -WHERE a.tx_id IN (SELECT id FROM activity_txs); +INSERT INTO + activity_tx_tags (tx_id, tag_id) +SELECT + a.tx_id, + a.tag_id +FROM + activity_tx_tags_new AS a +WHERE + a.tx_id IN ( + SELECT + id + FROM + activity_txs + ); -- Drop temporary old tables DROP TABLE tx_tags_new; + DROP TABLE activity_tx_tags_new; + DROP TABLE txs_new; + DROP TABLE activity_txs_new; -- Recreate indexes exactly as in original schema -CREATE UNIQUE INDEX IF NOT EXISTS idx_tx_method_name ON tx_methods(name); -CREATE UNIQUE INDEX IF NOT EXISTS idx_tags_name ON tags(name); -CREATE UNIQUE INDEX IF NOT EXISTS idx_balances_method_period ON balances(method_id, year, month); -CREATE UNIQUE INDEX IF NOT EXISTS idx_final_balance_unique ON balances(method_id) WHERE is_final_balance = 1; +CREATE UNIQUE INDEX IF NOT EXISTS idx_tx_method_name ON tx_methods (name); + +CREATE UNIQUE INDEX IF NOT EXISTS idx_tags_name ON tags (name); + +CREATE UNIQUE INDEX IF NOT EXISTS idx_balances_method_period ON balances (method_id, YEAR, MONTH); + +CREATE UNIQUE INDEX IF NOT EXISTS idx_final_balance_unique ON balances (method_id) +WHERE + is_final_balance = 1; + +CREATE INDEX IF NOT EXISTS idx_activities_date_type ON activities (date, activity_type); + +CREATE INDEX IF NOT EXISTS idx_txs_date ON txs (date); + +CREATE INDEX IF NOT EXISTS idx_txs_from_method ON txs (from_method); + +CREATE INDEX IF NOT EXISTS idx_txs_to_method ON txs (to_method); + +CREATE INDEX IF NOT EXISTS idx_txs_amount ON txs (amount); -CREATE INDEX IF NOT EXISTS idx_activities_date_type ON activities(date, activity_type); +CREATE INDEX IF NOT EXISTS idx_tx_tags_tx_id ON tx_tags (tx_id); -CREATE INDEX IF NOT EXISTS idx_txs_date ON txs(date); -CREATE INDEX IF NOT EXISTS idx_txs_from_method ON txs(from_method); -CREATE INDEX IF NOT EXISTS idx_txs_to_method ON txs(to_method); -CREATE INDEX IF NOT EXISTS idx_txs_amount ON txs(amount); +CREATE INDEX IF NOT EXISTS idx_tx_tags_tag_id ON tx_tags (tag_id); -CREATE INDEX IF NOT EXISTS idx_tx_tags_tx_id ON tx_tags(tx_id); -CREATE INDEX IF NOT EXISTS idx_tx_tags_tag_id ON tx_tags(tag_id); -CREATE INDEX IF NOT EXISTS idx_tx_tags_tag_tx ON tx_tags(tag_id, tx_id); +CREATE INDEX IF NOT EXISTS idx_tx_tags_tag_tx ON tx_tags (tag_id, tx_id); -CREATE INDEX IF NOT EXISTS idx_balances_method_id ON balances(method_id); +CREATE INDEX IF NOT EXISTS idx_balances_method_id ON balances (method_id); -CREATE INDEX IF NOT EXISTS idx_activity_txs_activity_num ON activity_txs(activity_num); +CREATE INDEX IF NOT EXISTS idx_activity_txs_activity_num ON activity_txs (activity_num); -CREATE INDEX IF NOT EXISTS idx_activity_tx_tags_tx_id ON activity_tx_tags(tx_id); +CREATE INDEX IF NOT EXISTS idx_activity_tx_tags_tx_id ON activity_tx_tags (tx_id); -- Default tag (ensure it still exists) -INSERT OR IGNORE INTO tags (id, name) VALUES (1, 'Unknown'); +INSERT +OR IGNORE INTO tags (id, name) +VALUES + (1, 'Unknown'); PRAGMA foreign_keys = ON; diff --git a/db/src/migrations/2025-10-30-101312-0000_new_tx_types/up.sql b/db/src/migrations/2025-10-30-101312-0000_new_tx_types/up.sql index cfd70a72..da647fef 100644 --- a/db/src/migrations/2025-10-30-101312-0000_new_tx_types/up.sql +++ b/db/src/migrations/2025-10-30-101312-0000_new_tx_types/up.sql @@ -1,14 +1,17 @@ PRAGMA foreign_keys = OFF; -ALTER TABLE txs RENAME TO txs_old; -ALTER TABLE activity_txs RENAME TO activity_txs_old; +ALTER TABLE txs +RENAME TO txs_old; + +ALTER TABLE activity_txs +RENAME TO activity_txs_old; CREATE TABLE txs ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, date DATETIME NOT NULL, details TEXT, - from_method INTEGER NOT NULL REFERENCES tx_methods(id) ON DELETE CASCADE, - to_method INTEGER REFERENCES tx_methods(id) ON DELETE CASCADE, + from_method INTEGER NOT NULL REFERENCES tx_methods (id) ON DELETE CASCADE, + to_method INTEGER REFERENCES tx_methods (id) ON DELETE CASCADE, amount BigInt NOT NULL, tx_type TEXT NOT NULL CHECK ( tx_type IN ( @@ -28,10 +31,18 @@ CREATE TABLE activity_txs ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, date TEXT, details TEXT, - from_method INTEGER REFERENCES tx_methods(id) ON DELETE CASCADE, - to_method INTEGER REFERENCES tx_methods(id) ON DELETE CASCADE, + from_method INTEGER REFERENCES tx_methods (id) ON DELETE CASCADE, + to_method INTEGER REFERENCES tx_methods (id) ON DELETE CASCADE, amount BigInt, - amount_type TEXT CHECK (amount_type IN ('exact', 'more_than', 'more_than_equal', 'less_than', 'less_than_equal')), + amount_type TEXT CHECK ( + amount_type IN ( + 'exact', + 'more_than', + 'more_than_equal', + 'less_than', + 'less_than_equal' + ) + ), tx_type TEXT CHECK ( tx_type IN ( 'Income', @@ -44,65 +55,137 @@ CREATE TABLE activity_txs ( ) ), display_order INTEGER, - activity_num INTEGER NOT NULL REFERENCES activities(id) ON DELETE CASCADE + activity_num INTEGER NOT NULL REFERENCES activities (id) ON DELETE CASCADE ); -INSERT INTO txs (id, date, details, from_method, to_method, amount, tx_type, display_order) -SELECT id, date, details, from_method, to_method, amount, tx_type, display_order FROM txs_old; - -INSERT INTO activity_txs (id, date, details, from_method, to_method, amount, amount_type, tx_type, display_order, activity_num) -SELECT id, date, details, from_method, to_method, amount, amount_type, tx_type, display_order, activity_num FROM activity_txs_old; - -ALTER TABLE tx_tags RENAME TO tx_tags_old; +INSERT INTO + txs ( + id, + date, + details, + from_method, + to_method, + amount, + tx_type, + display_order + ) +SELECT + id, + date, + details, + from_method, + to_method, + amount, + tx_type, + display_order +FROM + txs_old; + +INSERT INTO + activity_txs ( + id, + date, + details, + from_method, + to_method, + amount, + amount_type, + tx_type, + display_order, + activity_num + ) +SELECT + id, + date, + details, + from_method, + to_method, + amount, + amount_type, + tx_type, + display_order, + activity_num +FROM + activity_txs_old; + +ALTER TABLE tx_tags +RENAME TO tx_tags_old; CREATE TABLE tx_tags ( - tx_id INTEGER NOT NULL REFERENCES txs(id) ON DELETE CASCADE, - tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE, + tx_id INTEGER NOT NULL REFERENCES txs (id) ON DELETE CASCADE, + tag_id INTEGER NOT NULL REFERENCES tags (id) ON DELETE CASCADE, PRIMARY KEY (tx_id, tag_id) ); -INSERT INTO tx_tags (tx_id, tag_id) -SELECT tx_id, tag_id FROM tx_tags_old; +INSERT INTO + tx_tags (tx_id, tag_id) +SELECT + tx_id, + tag_id +FROM + tx_tags_old; -ALTER TABLE activity_tx_tags RENAME TO activity_tx_tags_old; +ALTER TABLE activity_tx_tags +RENAME TO activity_tx_tags_old; CREATE TABLE activity_tx_tags ( - tx_id INTEGER NOT NULL REFERENCES activity_txs(id) ON DELETE CASCADE, - tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE, + tx_id INTEGER NOT NULL REFERENCES activity_txs (id) ON DELETE CASCADE, + tag_id INTEGER NOT NULL REFERENCES tags (id) ON DELETE CASCADE, PRIMARY KEY (tx_id, tag_id) ); -INSERT INTO activity_tx_tags (tx_id, tag_id) -SELECT tx_id, tag_id FROM activity_tx_tags_old; +INSERT INTO + activity_tx_tags (tx_id, tag_id) +SELECT + tx_id, + tag_id +FROM + activity_tx_tags_old; DROP TABLE tx_tags_old; + DROP TABLE activity_tx_tags_old; + DROP TABLE txs_old; + DROP TABLE activity_txs_old; -CREATE UNIQUE INDEX IF NOT EXISTS idx_tx_method_name ON tx_methods(name); -CREATE UNIQUE INDEX IF NOT EXISTS idx_tags_name ON tags(name); -CREATE UNIQUE INDEX IF NOT EXISTS idx_balances_method_period ON balances(method_id, year, month); -CREATE UNIQUE INDEX IF NOT EXISTS idx_final_balance_unique ON balances(method_id) WHERE is_final_balance = 1; +CREATE UNIQUE INDEX IF NOT EXISTS idx_tx_method_name ON tx_methods (name); + +CREATE UNIQUE INDEX IF NOT EXISTS idx_tags_name ON tags (name); + +CREATE UNIQUE INDEX IF NOT EXISTS idx_balances_method_period ON balances (method_id, YEAR, MONTH); + +CREATE UNIQUE INDEX IF NOT EXISTS idx_final_balance_unique ON balances (method_id) +WHERE + is_final_balance = 1; + +CREATE INDEX IF NOT EXISTS idx_activities_date_type ON activities (date, activity_type); + +CREATE INDEX IF NOT EXISTS idx_txs_date ON txs (date); + +CREATE INDEX IF NOT EXISTS idx_txs_from_method ON txs (from_method); + +CREATE INDEX IF NOT EXISTS idx_txs_to_method ON txs (to_method); + +CREATE INDEX IF NOT EXISTS idx_txs_amount ON txs (amount); -CREATE INDEX IF NOT EXISTS idx_activities_date_type ON activities(date, activity_type); +CREATE INDEX IF NOT EXISTS idx_tx_tags_tx_id ON tx_tags (tx_id); -CREATE INDEX IF NOT EXISTS idx_txs_date ON txs(date); -CREATE INDEX IF NOT EXISTS idx_txs_from_method ON txs(from_method); -CREATE INDEX IF NOT EXISTS idx_txs_to_method ON txs(to_method); -CREATE INDEX IF NOT EXISTS idx_txs_amount ON txs(amount); +CREATE INDEX IF NOT EXISTS idx_tx_tags_tag_id ON tx_tags (tag_id); -CREATE INDEX IF NOT EXISTS idx_tx_tags_tx_id ON tx_tags(tx_id); -CREATE INDEX IF NOT EXISTS idx_tx_tags_tag_id ON tx_tags(tag_id); -CREATE INDEX IF NOT EXISTS idx_tx_tags_tag_tx ON tx_tags(tag_id, tx_id); +CREATE INDEX IF NOT EXISTS idx_tx_tags_tag_tx ON tx_tags (tag_id, tx_id); -CREATE INDEX IF NOT EXISTS idx_balances_method_id ON balances(method_id); +CREATE INDEX IF NOT EXISTS idx_balances_method_id ON balances (method_id); -CREATE INDEX IF NOT EXISTS idx_activity_txs_activity_num ON activity_txs(activity_num); +CREATE INDEX IF NOT EXISTS idx_activity_txs_activity_num ON activity_txs (activity_num); -CREATE INDEX IF NOT EXISTS idx_activity_tx_tags_tx_id ON activity_tx_tags(tx_id); +CREATE INDEX IF NOT EXISTS idx_activity_tx_tags_tx_id ON activity_tx_tags (tx_id); -- Default tag -INSERT OR IGNORE INTO tags (id, name) VALUES (1, 'Unknown'); +INSERT +OR IGNORE INTO tags (id, name) +VALUES + (1, 'Unknown'); PRAGMA foreign_keys = ON; diff --git a/db/src/migrations/2025-10-31-114912-0000_primary_tag/down.sql b/db/src/migrations/2025-10-31-114912-0000_primary_tag/down.sql index a5df6fe1..949363dc 100644 --- a/db/src/migrations/2025-10-31-114912-0000_primary_tag/down.sql +++ b/db/src/migrations/2025-10-31-114912-0000_primary_tag/down.sql @@ -1,13 +1,18 @@ CREATE TABLE tx_tags_old ( - tx_id INTEGER NOT NULL REFERENCES txs(id) ON DELETE CASCADE, - tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE, + tx_id INTEGER NOT NULL REFERENCES txs (id) ON DELETE CASCADE, + tag_id INTEGER NOT NULL REFERENCES tags (id) ON DELETE CASCADE, PRIMARY KEY (tx_id, tag_id) ); -INSERT INTO tx_tags_old (tx_id, tag_id) -SELECT tx_id, tag_id -FROM tx_tags; +INSERT INTO + tx_tags_old (tx_id, tag_id) +SELECT + tx_id, + tag_id +FROM + tx_tags; DROP TABLE tx_tags; -ALTER TABLE tx_tags_old RENAME TO tx_tags; +ALTER TABLE tx_tags_old +RENAME TO tx_tags; diff --git a/db/src/migrations/2025-10-31-114912-0000_primary_tag/up.sql b/db/src/migrations/2025-10-31-114912-0000_primary_tag/up.sql index a415c563..65a6fe09 100644 --- a/db/src/migrations/2025-10-31-114912-0000_primary_tag/up.sql +++ b/db/src/migrations/2025-10-31-114912-0000_primary_tag/up.sql @@ -1,24 +1,34 @@ CREATE TABLE tx_tags_new ( - tx_id INTEGER NOT NULL REFERENCES txs(id) ON DELETE CASCADE, - tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE, + tx_id INTEGER NOT NULL REFERENCES txs (id) ON DELETE CASCADE, + tag_id INTEGER NOT NULL REFERENCES tags (id) ON DELETE CASCADE, is_primary BOOLEAN NOT NULL DEFAULT FALSE, PRIMARY KEY (tx_id, tag_id) ); -INSERT INTO tx_tags_new (tx_id, tag_id, is_primary) +INSERT INTO + tx_tags_new (tx_id, tag_id, is_primary) SELECT tx_id, tag_id, CASE - WHEN ROW_NUMBER() OVER (PARTITION BY tx_id ORDER BY tag_id) = 1 THEN 1 + WHEN ROW_NUMBER() OVER ( + PARTITION BY + tx_id + ORDER BY + tag_id + ) = 1 THEN 1 ELSE 0 END AS is_primary -FROM tx_tags; +FROM + tx_tags; DROP TABLE tx_tags; -ALTER TABLE tx_tags_new RENAME TO tx_tags; +ALTER TABLE tx_tags_new +RENAME TO tx_tags; -CREATE INDEX IF NOT EXISTS idx_tx_tags_tx_id ON tx_tags(tx_id); -CREATE INDEX IF NOT EXISTS idx_tx_tags_tag_id ON tx_tags(tag_id); -CREATE INDEX IF NOT EXISTS idx_tx_tags_tag_tx ON tx_tags(tag_id, tx_id); +CREATE INDEX IF NOT EXISTS idx_tx_tags_tx_id ON tx_tags (tx_id); + +CREATE INDEX IF NOT EXISTS idx_tx_tags_tag_id ON tx_tags (tag_id); + +CREATE INDEX IF NOT EXISTS idx_tx_tags_tag_tx ON tx_tags (tag_id, tx_id); diff --git a/db/src/migrations/2026-09-26-101608-0000_add_recurring_txs/down.sql b/db/src/migrations/2026-09-26-101608-0000_add_recurring_txs/down.sql new file mode 100644 index 00000000..6406842e --- /dev/null +++ b/db/src/migrations/2026-09-26-101608-0000_add_recurring_txs/down.sql @@ -0,0 +1,3 @@ +DROP TABLE recurring_tx_tags; + +DROP TABLE recurring_txs; diff --git a/db/src/migrations/2026-09-26-101608-0000_add_recurring_txs/up.sql b/db/src/migrations/2026-09-26-101608-0000_add_recurring_txs/up.sql new file mode 100644 index 00000000..e1cc00d0 --- /dev/null +++ b/db/src/migrations/2026-09-26-101608-0000_add_recurring_txs/up.sql @@ -0,0 +1,68 @@ +CREATE TABLE recurring_txs ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + created_at DATETIME NOT NULL, + details TEXT, + from_method INTEGER NOT NULL REFERENCES tx_methods (id) ON DELETE CASCADE, + to_method INTEGER REFERENCES tx_methods (id) ON DELETE CASCADE, + amount BigInt NOT NULL, + tx_type TEXT NOT NULL CHECK ( + tx_type IN ( + 'Income', + 'Expense', + 'Transfer', + 'Borrow', + 'Lend', + 'Borrow Repay', + 'Lend Repay' + ) + ), + frequency TEXT NOT NULL CHECK ( + frequency IN ('Daily', 'Weekly', 'Monthly', 'Yearly') + ), + recur_interval INTEGER NOT NULL DEFAULT 1 CHECK (recur_interval >= 1), + recur_value INTEGER CHECK ( + ( + frequency = 'Daily' + AND recur_value IS NULL + ) + OR ( + frequency = 'Weekly' + AND recur_value BETWEEN 0 AND 6 + ) + OR ( + frequency IN ('Monthly', 'Yearly') + AND recur_value BETWEEN 1 AND 31 + ) + ), + recur_month INTEGER CHECK ( + ( + frequency != 'Yearly' + AND recur_month IS NULL + ) + OR ( + frequency = 'Yearly' + AND recur_month BETWEEN 1 AND 12 + ) + ), + last_recurred_date DATE, + next_recurring_date DATE NOT NULL, + end_date DATE, + is_paused BOOLEAN NOT NULL DEFAULT FALSE +); + +CREATE TABLE recurring_tx_tags ( + recurring_tx_id INTEGER NOT NULL REFERENCES recurring_txs (id) ON DELETE CASCADE, + tag_id INTEGER NOT NULL REFERENCES tags (id) ON DELETE CASCADE, + is_primary BOOLEAN NOT NULL DEFAULT FALSE, + PRIMARY KEY (recurring_tx_id, tag_id) +); + +CREATE INDEX idx_recurring_txs_next_date ON recurring_txs (next_recurring_date); + +CREATE INDEX idx_recurring_txs_from_method ON recurring_txs (from_method); + +CREATE INDEX idx_recurring_txs_to_method ON recurring_txs (to_method); + +CREATE INDEX idx_recurring_tx_tags_recurring_tx_id ON recurring_tx_tags (recurring_tx_id); + +CREATE INDEX idx_recurring_tx_tags_tag_id ON recurring_tx_tags (tag_id); diff --git a/db/src/models/balances.rs b/db/src/models/balances.rs index dcb39092..43602a98 100644 --- a/db/src/models/balances.rs +++ b/db/src/models/balances.rs @@ -162,7 +162,7 @@ impl Balance { Ok(balance_list) } - /// Get the last non-final balance of a method. Date = Current date + /// The last non-final balance of every method before the given date pub fn get_last_balance( date: NaiveDate, nature: FetchNature, @@ -180,8 +180,8 @@ impl Balance { let mut pending_balance_tx_methods = HashSet::new(); - // All means all TXs were fetched. The last balance is the balance before the first TX - // which is 0 + // "All" means every tx was fetched, so the balance before the very first + // one is 0 if let FetchNature::All = nature { let mut to_return = HashMap::new(); @@ -224,7 +224,8 @@ impl Balance { } } - // Fallback. Start from the previous month and look for the last non-final balance + // Fallback: any method the loop above could not resolve gets its most + // recent balance recorded before the previous month let date = date - Months::new(1); if !pending_balance_tx_methods.is_empty() { diff --git a/db/src/models/mod.rs b/db/src/models/mod.rs index baa56ab2..1949b9aa 100644 --- a/db/src/models/mod.rs +++ b/db/src/models/mod.rs @@ -3,6 +3,8 @@ mod activity_tx_tags; mod activity_txs; mod balances; mod others; +mod recurring_tx_tags; +mod recurring_txs; mod tags; mod tx_methods; mod tx_tags; @@ -13,6 +15,8 @@ pub use activity_tx_tags::*; pub use activity_txs::*; pub use balances::*; pub use others::*; +pub use recurring_tx_tags::*; +pub use recurring_txs::*; pub use tags::*; pub use tx_methods::*; pub use tx_tags::*; diff --git a/db/src/models/others.rs b/db/src/models/others.rs index c1dc7673..15a23dd4 100644 --- a/db/src/models/others.rs +++ b/db/src/models/others.rs @@ -21,6 +21,30 @@ pub enum TxType { LendRepay, } +#[derive(Clone, Debug, Copy, Display, EnumIter, Eq, PartialEq)] +pub enum RecurrenceFrequency { + #[strum(to_string = "Daily")] + Daily, + #[strum(to_string = "Weekly")] + Weekly, + #[strum(to_string = "Monthly")] + Monthly, + #[strum(to_string = "Yearly")] + Yearly, +} + +impl From<&str> for RecurrenceFrequency { + fn from(s: &str) -> Self { + match s { + "Daily" => RecurrenceFrequency::Daily, + "Weekly" => RecurrenceFrequency::Weekly, + "Monthly" => RecurrenceFrequency::Monthly, + "Yearly" => RecurrenceFrequency::Yearly, + other => panic!("Invalid RecurrenceFrequency string: {other}"), + } + } +} + #[derive(Clone, Debug, Copy, Eq, PartialEq)] pub enum FetchNature { Monthly, diff --git a/db/src/models/recurring_tx_tags.rs b/db/src/models/recurring_tx_tags.rs new file mode 100644 index 00000000..c565d95c --- /dev/null +++ b/db/src/models/recurring_tx_tags.rs @@ -0,0 +1,58 @@ +use diesel::prelude::*; +use diesel::result::Error; + +use crate::ConnCache; +use crate::schema::recurring_tx_tags; + +#[derive(Clone, Queryable, Insertable, Selectable)] +pub struct RecurringTxTag { + pub recurring_tx_id: i32, + pub tag_id: i32, + pub is_primary: bool, +} + +impl RecurringTxTag { + #[must_use] + pub fn new(recurring_tx_id: i32, tag_id: i32, is_primary: bool) -> Self { + RecurringTxTag { + recurring_tx_id, + tag_id, + is_primary, + } + } + + pub fn get_by_recurring_tx_ids( + recurring_tx_ids: Vec, + db_conn: &mut impl ConnCache, + ) -> Result, Error> { + use crate::schema::recurring_tx_tags::dsl::{ + is_primary, recurring_tx_id, recurring_tx_tags, + }; + + recurring_tx_tags + .filter(recurring_tx_id.eq_any(recurring_tx_ids)) + .order(is_primary.desc()) + .load(db_conn.conn()) + } + + pub fn insert_batch( + tags: Vec, + db_conn: &mut impl ConnCache, + ) -> Result { + use crate::schema::recurring_tx_tags::dsl::recurring_tx_tags; + + diesel::insert_into(recurring_tx_tags) + .values(tags) + .execute(db_conn.conn()) + } + + pub fn delete_by_recurring_tx_id( + recurring_tx_id_value: i32, + db_conn: &mut impl ConnCache, + ) -> Result { + use crate::schema::recurring_tx_tags::dsl::{recurring_tx_id, recurring_tx_tags}; + + diesel::delete(recurring_tx_tags.filter(recurring_tx_id.eq(recurring_tx_id_value))) + .execute(db_conn.conn()) + } +} diff --git a/db/src/models/recurring_txs.rs b/db/src/models/recurring_txs.rs new file mode 100644 index 00000000..718ca7a9 --- /dev/null +++ b/db/src/models/recurring_txs.rs @@ -0,0 +1,320 @@ +use chrono::{NaiveDate, NaiveDateTime}; +use diesel::prelude::*; +use diesel::result::Error; +use rex_shared::models::Cent; +use std::collections::HashMap; + +use crate::ConnCache; +use crate::models::{EMPTY, RecurrenceFrequency, RecurringTxTag, Tag, TxMethod, TxType}; +use crate::schema::recurring_txs; + +#[derive(Clone, Queryable, Selectable, Insertable)] +pub struct RecurringTx { + pub id: i32, + created_at: NaiveDateTime, + details: Option, + pub from_method: i32, + pub to_method: Option, + pub amount: i64, + pub tx_type: String, + pub frequency: String, + pub recur_interval: i32, + pub recur_value: Option, + pub recur_month: Option, + pub last_recurred_date: Option, + pub next_recurring_date: NaiveDate, + pub end_date: Option, + pub is_paused: bool, +} + +#[derive(Clone, Insertable)] +#[diesel(table_name = recurring_txs)] +pub struct NewRecurringTx<'a> { + pub created_at: NaiveDateTime, + pub details: Option<&'a str>, + pub from_method: i32, + pub to_method: Option, + pub amount: i64, + pub tx_type: &'a str, + pub frequency: &'a str, + pub recur_interval: i32, + pub recur_value: Option, + pub recur_month: Option, + pub last_recurred_date: Option, + pub next_recurring_date: NaiveDate, + pub end_date: Option, + pub is_paused: bool, +} + +impl<'a> NewRecurringTx<'a> { + #[allow(clippy::too_many_arguments)] + #[must_use] + pub fn new( + created_at: NaiveDateTime, + details: Option<&'a str>, + from_method: i32, + to_method: Option, + amount: i64, + tx_type: &'a str, + frequency: &'a str, + recur_interval: i32, + recur_value: Option, + recur_month: Option, + next_recurring_date: NaiveDate, + end_date: Option, + ) -> Self { + NewRecurringTx { + created_at, + details, + from_method, + to_method, + amount, + tx_type, + frequency, + recur_interval, + recur_value, + recur_month, + last_recurred_date: None, + next_recurring_date, + end_date, + is_paused: false, + } + } + + pub fn insert(self, db_conn: &mut impl ConnCache) -> Result { + use crate::schema::recurring_txs::dsl::recurring_txs; + + diesel::insert_into(recurring_txs) + .values(self) + .returning(RecurringTx::as_returning()) + .get_result(db_conn.conn()) + } +} + +pub struct FullRecurringTx { + pub id: i32, + pub details: Option, + pub from_method: TxMethod, + pub to_method: Option, + pub amount: Cent, + pub tx_type: TxType, + pub frequency: RecurrenceFrequency, + pub recur_interval: i32, + pub recur_value: Option, + pub recur_month: Option, + pub last_recurred_date: Option, + pub next_recurring_date: NaiveDate, + pub end_date: Option, + pub is_paused: bool, + pub tags: Vec, +} + +impl RecurringTx { + pub fn get_all(db_conn: &mut impl ConnCache) -> Result, Error> { + use crate::schema::recurring_txs::dsl::recurring_txs; + + recurring_txs + .select(RecurringTx::as_select()) + .load(db_conn.conn()) + } + + pub fn get_by_id(id_num: i32, db_conn: &mut impl ConnCache) -> Result { + use crate::schema::recurring_txs::dsl::{id, recurring_txs}; + + recurring_txs + .filter(id.eq(id_num)) + .select(Self::as_select()) + .first(db_conn.conn()) + } + + /// All non-paused rules whose next occurrence is already due + pub fn get_due(today: NaiveDate, db_conn: &mut impl ConnCache) -> Result, Error> { + use crate::schema::recurring_txs::dsl::{is_paused, next_recurring_date, recurring_txs}; + + recurring_txs + .filter(is_paused.eq(false)) + .filter(next_recurring_date.le(today)) + .select(Self::as_select()) + .load(db_conn.conn()) + } + + pub fn update_schedule( + id_num: i32, + last: Option, + next: NaiveDate, + db_conn: &mut impl ConnCache, + ) -> Result { + use crate::schema::recurring_txs::dsl::{ + id, last_recurred_date, next_recurring_date, recurring_txs, + }; + + diesel::update(recurring_txs.filter(id.eq(id_num))) + .set((last_recurred_date.eq(last), next_recurring_date.eq(next))) + .execute(db_conn.conn()) + } + + pub fn set_paused( + id_num: i32, + paused: bool, + db_conn: &mut impl ConnCache, + ) -> Result { + use crate::schema::recurring_txs::dsl::{id, is_paused, recurring_txs}; + + diesel::update(recurring_txs.filter(id.eq(id_num))) + .set(is_paused.eq(paused)) + .execute(db_conn.conn()) + } + + pub fn update( + id_num: i32, + new: &NewRecurringTx, + db_conn: &mut impl ConnCache, + ) -> Result { + use crate::schema::recurring_txs::dsl::{ + amount, details, end_date, frequency, from_method, id, last_recurred_date, + next_recurring_date, recur_interval, recur_month, recur_value, recurring_txs, + to_method, tx_type, + }; + + diesel::update(recurring_txs.filter(id.eq(id_num))) + .set(( + details.eq(new.details), + from_method.eq(new.from_method), + to_method.eq(new.to_method), + amount.eq(new.amount), + tx_type.eq(new.tx_type), + frequency.eq(new.frequency), + recur_interval.eq(new.recur_interval), + recur_value.eq(new.recur_value), + recur_month.eq(new.recur_month), + last_recurred_date.eq(new.last_recurred_date), + next_recurring_date.eq(new.next_recurring_date), + end_date.eq(new.end_date), + )) + .execute(db_conn.conn()) + } + + pub fn delete_by_id(id_num: i32, db_conn: &mut impl ConnCache) -> Result { + use crate::schema::recurring_txs::dsl::{id, recurring_txs}; + + diesel::delete(recurring_txs.filter(id.eq(id_num))).execute(db_conn.conn()) + } +} + +impl FullRecurringTx { + pub fn get_all(db_conn: &mut impl ConnCache) -> Result, Error> { + let all = RecurringTx::get_all(db_conn)?; + + FullRecurringTx::convert_to_full(all, db_conn) + } + + pub fn get_by_id(id_num: i32, db_conn: &mut impl ConnCache) -> Result { + let recurring_tx = RecurringTx::get_by_id(id_num, db_conn)?; + + Ok( + FullRecurringTx::convert_to_full(vec![recurring_tx], db_conn)? + .pop() + .unwrap(), + ) + } + + pub fn convert_to_full( + recurring_txs: Vec, + db_conn: &mut impl ConnCache, + ) -> Result, Error> { + let ids = recurring_txs.iter().map(|t| t.id).collect::>(); + + let recurring_tx_tags = RecurringTxTag::get_by_recurring_tx_ids(ids, db_conn)?; + + let mut tags_map = HashMap::new(); + + for tag in recurring_tx_tags { + tags_map + .entry(tag.recurring_tx_id) + .or_insert(Vec::new()) + .push(tag.tag_id); + } + + let mut to_return = Vec::new(); + + for recurring_tx in recurring_txs { + let tags: Vec = { + let tag_ids = tags_map.get(&recurring_tx.id).unwrap_or(&EMPTY); + let mut v = Vec::with_capacity(tag_ids.len()); + + for tag_id in tag_ids { + v.push(db_conn.cache().tags.get(tag_id).unwrap().clone()); + } + + v + }; + + let full = FullRecurringTx { + id: recurring_tx.id, + details: recurring_tx.details, + from_method: db_conn + .cache() + .tx_methods + .get(&recurring_tx.from_method) + .unwrap() + .clone(), + to_method: recurring_tx + .to_method + .map(|method_id| db_conn.cache().tx_methods.get(&method_id).unwrap().clone()), + amount: Cent::new(recurring_tx.amount), + tx_type: recurring_tx.tx_type.as_str().into(), + frequency: recurring_tx.frequency.as_str().into(), + recur_interval: recurring_tx.recur_interval, + recur_value: recurring_tx.recur_value, + recur_month: recurring_tx.recur_month, + last_recurred_date: recurring_tx.last_recurred_date, + next_recurring_date: recurring_tx.next_recurring_date, + end_date: recurring_tx.end_date, + is_paused: recurring_tx.is_paused, + tags, + }; + + to_return.push(full); + } + + Ok(to_return) + } + + #[must_use] + pub fn frequency_label(&self) -> String { + if self.recur_interval <= 1 { + self.frequency.to_string() + } else { + match self.frequency { + RecurrenceFrequency::Daily => format!("Every {} Days", self.recur_interval), + RecurrenceFrequency::Weekly => format!("Every {} Weeks", self.recur_interval), + RecurrenceFrequency::Monthly => format!("Every {} Months", self.recur_interval), + RecurrenceFrequency::Yearly => format!("Every {} Years", self.recur_interval), + } + } + } + + #[must_use] + pub fn to_array(&self) -> Vec { + let mut method = self.from_method.name.clone(); + + if let Some(to_method) = &self.to_method { + method = format!("{} → {}", self.from_method.name, to_method.name); + } + + vec![ + self.next_recurring_date.format("%Y-%m-%d").to_string(), + self.frequency_label(), + self.details.clone().unwrap_or_default(), + method, + format!("{:.2}", self.amount.dollar()), + self.tx_type.to_string(), + self.tags + .iter() + .map(|t| t.name.clone()) + .collect::>() + .join(", "), + if self.is_paused { "Yes" } else { "No" }.to_string(), + ] + } +} diff --git a/db/src/schema.rs b/db/src/schema.rs index 8227fdb2..87466c93 100644 --- a/db/src/schema.rs +++ b/db/src/schema.rs @@ -41,6 +41,34 @@ diesel::table! { } } +diesel::table! { + recurring_tx_tags (recurring_tx_id, tag_id) { + recurring_tx_id -> Integer, + tag_id -> Integer, + is_primary -> Bool, + } +} + +diesel::table! { + recurring_txs (id) { + id -> Integer, + created_at -> Timestamp, + details -> Nullable, + from_method -> Integer, + to_method -> Nullable, + amount -> BigInt, + tx_type -> Text, + frequency -> Text, + recur_interval -> Integer, + recur_value -> Nullable, + recur_month -> Nullable, + last_recurred_date -> Nullable, + next_recurring_date -> Date, + end_date -> Nullable, + is_paused -> Bool, + } +} + diesel::table! { tags (id) { id -> Integer, @@ -81,6 +109,8 @@ diesel::joinable!(activity_tx_tags -> activity_txs (tx_id)); diesel::joinable!(activity_tx_tags -> tags (tag_id)); diesel::joinable!(activity_txs -> activities (activity_num)); diesel::joinable!(balances -> tx_methods (method_id)); +diesel::joinable!(recurring_tx_tags -> recurring_txs (recurring_tx_id)); +diesel::joinable!(recurring_tx_tags -> tags (tag_id)); diesel::joinable!(tx_tags -> tags (tag_id)); diesel::joinable!(tx_tags -> txs (tx_id)); @@ -89,6 +119,8 @@ diesel::allow_tables_to_appear_in_same_query!( activity_tx_tags, activity_txs, balances, + recurring_tx_tags, + recurring_txs, tags, tx_methods, tx_tags, diff --git a/tui/Cargo.toml b/tui/Cargo.toml index a1623bb7..599224d8 100644 --- a/tui/Cargo.toml +++ b/tui/Cargo.toml @@ -25,6 +25,7 @@ atty = "0.2.14" chrono.workspace = true crossterm = "0.29.0" dirs = "6.0.0" +log = "0.4.34" open = "5.3.6" ratatui = "0.30.2" reqwest = { version = "0.13.4", features = ["blocking", "json"] } @@ -34,6 +35,7 @@ rfd = "0.17.2" semver = "1.0.28" serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.150" +simplelog = "0.12.2" strum.workspace = true strum_macros.workspace = true thousands = "0.2.0" diff --git a/tui/src/key_checker/activity.rs b/tui/src/key_checker/activity.rs index 51a6aae7..88f2e6ed 100644 --- a/tui/src/key_checker/activity.rs +++ b/tui/src/key_checker/activity.rs @@ -16,6 +16,7 @@ pub fn activity_keys(handler: &mut InputKeyHandler) -> Result handler.do_help_popup(), KeyCode::Char('z') => handler.go_summary()?, KeyCode::Char('w') => handler.go_search(), + KeyCode::Char('u') => handler.go_recurring(), KeyCode::Char('v') => handler.show_activity_tx_details()?, KeyCode::Char('t') => handler.next_theme()?, KeyCode::Right => handler.handle_right_arrow()?, diff --git a/tui/src/key_checker/add_tx.rs b/tui/src/key_checker/add_tx.rs index dcb1f1b9..fd39bcf9 100644 --- a/tui/src/key_checker/add_tx.rs +++ b/tui/src/key_checker/add_tx.rs @@ -22,6 +22,7 @@ pub fn add_tx_keys(handler: &mut InputKeyHandler) -> Result handler.go_search(), KeyCode::Char('c') => handler.clear_input()?, KeyCode::Char('y') => handler.go_activity(), + KeyCode::Char('u') => handler.go_recurring(), KeyCode::Char('t') => handler.next_theme()?, KeyCode::Enter => handler.select_date_field(), KeyCode::Char(c) if c.is_numeric() => { @@ -42,7 +43,12 @@ pub fn add_tx_keys(handler: &mut InputKeyHandler) -> Result handler.handle_amount()?, TxTab::TxType => handler.handle_tx_type()?, TxTab::Tags => handler.handle_tags(), - TxTab::Nothing => {} + TxTab::Nothing + | TxTab::Frequency + | TxTab::RecurInterval + | TxTab::RecurValue + | TxTab::RecurMonth + | TxTab::EndDate => {} }, }, }, diff --git a/tui/src/key_checker/chart.rs b/tui/src/key_checker/chart.rs index 1f1454dc..c1cce822 100644 --- a/tui/src/key_checker/chart.rs +++ b/tui/src/key_checker/chart.rs @@ -19,6 +19,7 @@ pub fn chart_keys(handler: &mut InputKeyHandler) -> Result handler.do_chart_lgeneds(), KeyCode::Char('w') => handler.go_search(), KeyCode::Char('y') => handler.go_activity(), + KeyCode::Char('u') => handler.go_recurring(), KeyCode::Char('t') => handler.next_theme()?, KeyCode::Right => handler.handle_right_arrow()?, KeyCode::Left => handler.handle_left_arrow()?, diff --git a/tui/src/key_checker/home.rs b/tui/src/key_checker/home.rs index e18b0aac..bd774b37 100644 --- a/tui/src/key_checker/home.rs +++ b/tui/src/key_checker/home.rs @@ -19,6 +19,7 @@ pub fn home_keys(handler: &mut InputKeyHandler) -> Result KeyCode::Char('e') => handler.home_edit_tx()?, KeyCode::Char('d') => handler.do_deletion_popup(), KeyCode::Char('y') => handler.go_activity(), + KeyCode::Char('u') => handler.go_recurring(), KeyCode::Char(',') => handler.switch_tx_position_up()?, KeyCode::Char('.') => handler.switch_tx_position_down()?, KeyCode::Char('v') => handler.show_home_tx_details(), diff --git a/tui/src/key_checker/key_handler.rs b/tui/src/key_checker/key_handler.rs index fa7d03d7..5d8672a4 100644 --- a/tui/src/key_checker/key_handler.rs +++ b/tui/src/key_checker/key_handler.rs @@ -1,6 +1,6 @@ use anyhow::{Result, anyhow}; use crossterm::event::{KeyCode, KeyEvent}; -use rex_app::conn::{DbConn, FetchNature}; +use rex_app::conn::{DbConn, FetchNature, FullRecurringTx, RecurrenceFrequency}; use rex_app::ui_helper::{DateType, StepType}; use rex_app::views::{ActivityView, ChartView, FullSummary, SearchView, SummaryView, TxViewGroup}; use std::collections::HashMap; @@ -15,8 +15,8 @@ use crate::page_handler::{ }; use crate::pages::{ ACTIVITY_TABLE_ID, ChoicePopupState, ConfigChoices, DeletionChoices, HOME_TABLE_ID, - InfoPopupState, MovementDirection, NewPathChoices, PopupType, SEARCH_TABLE_ID, - SUMMARY_TABLE_ID, + InfoPopupState, MovementDirection, NewPathChoices, PopupType, RECURRING_TABLE_ID, + SEARCH_TABLE_ID, SUMMARY_TABLE_ID, }; use crate::theme::Theme; use crate::tx_handler::TxData; @@ -61,6 +61,10 @@ pub struct InputKeyHandler<'a> { activity_tab: &'a mut ActivityTab, activity_view: &'a mut ActivityView, activity_table: &'a mut TableData, + recurring_data: &'a mut TxData, + pub recurring_tab: &'a mut TxTab, + recurring_table: &'a mut TableData, + recurring_txs: &'a mut Vec, chart_hidden_mode: &'a mut bool, chart_hidden_legends: &'a mut bool, summary_hidden_mode: &'a mut bool, @@ -108,6 +112,10 @@ impl<'a> InputKeyHandler<'a> { activity_tab: &'a mut ActivityTab, activity_view: &'a mut ActivityView, activity_table: &'a mut TableData, + recurring_data: &'a mut TxData, + recurring_tab: &'a mut TxTab, + recurring_table: &'a mut TableData, + recurring_txs: &'a mut Vec, chart_hidden_mode: &'a mut bool, chart_hidden_legends: &'a mut bool, summary_hidden_mode: &'a mut bool, @@ -153,6 +161,10 @@ impl<'a> InputKeyHandler<'a> { activity_tab, activity_view, activity_table, + recurring_data, + recurring_tab, + recurring_table, + recurring_txs, chart_hidden_mode, chart_hidden_legends, summary_hidden_mode, @@ -164,7 +176,8 @@ impl<'a> InputKeyHandler<'a> { } } - /// Moves the interface to Home page and resets any selected widget/data from Add TX or Transfer page to Nothing + /// Moves the interface to the Home page and clears both the fields and the + /// selected widget of the Add TX, Search and Recurring pages pub fn go_home_reset(&mut self) { match self.page { CurrentUi::AddTx => { @@ -175,6 +188,10 @@ impl<'a> InputKeyHandler<'a> { *self.search_data = TxData::new_empty(); *self.search_tab = TxTab::Nothing; } + CurrentUi::Recurring => { + *self.recurring_data = TxData::new_empty(); + *self.recurring_tab = TxTab::Nothing; + } _ => {} } self.go_home(); @@ -229,6 +246,13 @@ impl<'a> InputKeyHandler<'a> { self.lerp_state.clear(); } + /// Moves the interface to the Recurring Transactions page + pub fn go_recurring(&mut self) { + *self.page = CurrentUi::Recurring; + *self.recurring_tab = TxTab::Nothing; + self.lerp_state.clear(); + } + /// Turns on help popup pub fn do_help_popup(&mut self) { let popup_state = match self.page { @@ -238,6 +262,7 @@ impl<'a> InputKeyHandler<'a> { CurrentUi::Summary => InfoPopupState::SummaryHelp, CurrentUi::Search => InfoPopupState::SearchHelp, CurrentUi::Activity => InfoPopupState::ActivityHelp, + CurrentUi::Recurring => InfoPopupState::RecurringHelp, CurrentUi::Initial => unreachable!(), }; @@ -265,7 +290,7 @@ impl<'a> InputKeyHandler<'a> { *self.popup_status = PopupType::new_choice_config(self.theme); } - /// Turns on deletion confirmation popup + /// Turns on deletion confirmation popup for the selected transaction pub fn do_deletion_popup(&mut self) { match self.page { CurrentUi::Home if self.home_table.state.selected().is_some() => { @@ -275,6 +300,9 @@ impl<'a> InputKeyHandler<'a> { CurrentUi::Search if self.search_table.state.selected().is_some() => { *self.popup_status = PopupType::new_choice_deletion(self.theme); } + CurrentUi::Recurring if self.recurring_table.state.selected().is_some() => { + *self.popup_status = PopupType::new_choice_deletion(self.theme); + } _ => {} } } @@ -322,7 +350,7 @@ impl<'a> InputKeyHandler<'a> { if self.search_data.check_all_empty() { self.search_data.add_tx_status( "Search: All fields cannot be empty".to_string(), - LogType::Info, + LogType::Error, ); } else { let search_txs = self.search_data.get_search_tx(self.conn)?; @@ -359,7 +387,8 @@ impl<'a> InputKeyHandler<'a> { match status { Ok(()) => { self.go_home_reset(); - // We just added a new TX, select the month tab again + reload the data of balance and table widgets to get updated data + // A new tx was added, so go back to the month tab and refetch the + // balances and the table to show it *self.home_tab = HomeTab::Months; self.reload_home_table(true)?; self.reload_chart_data()?; @@ -367,7 +396,9 @@ impl<'a> InputKeyHandler<'a> { self.reset_search_data(); self.reload_activity_table()?; } - Err(e) => self.add_tx_data.add_tx_status(e.to_string(), LogType::Info), + Err(e) => self + .add_tx_data + .add_tx_status(e.to_string(), LogType::Error), } Ok(()) @@ -464,6 +495,19 @@ impl<'a> InputKeyHandler<'a> { _ => {} }, }, + CurrentUi::Recurring => { + let tabs = recurring_number_jump_tabs( + self.recurring_data.get_tx_type(), + self.recurring_data.get_frequency(), + ); + + if let KeyCode::Char(c) = self.key.code + && let Some(digit) = c.to_digit(10) + && let Some(tab) = tabs.into_iter().nth(digit_to_tab_index(digit)) + { + *self.recurring_tab = tab; + } + } _ => {} } self.go_correct_index(); @@ -487,6 +531,7 @@ impl<'a> InputKeyHandler<'a> { }, CurrentUi::AddTx => self.add_tx_data.move_index_left(self.add_tx_tab), CurrentUi::Search => self.search_data.move_index_left(self.search_tab), + CurrentUi::Recurring => self.recurring_data.move_index_left(self.recurring_tab), CurrentUi::Chart => { if !*self.chart_hidden_mode { match self.chart_tab { @@ -570,6 +615,10 @@ impl<'a> InputKeyHandler<'a> { self.search_data.move_index_right(self.search_tab); self.lerp_state.clear_lerp(SEARCH_TABLE_ID); } + CurrentUi::Recurring => { + self.recurring_data.move_index_right(self.recurring_tab); + self.lerp_state.clear_lerp(RECURRING_TABLE_ID); + } CurrentUi::Chart => { if !*self.chart_hidden_mode { match self.chart_tab { @@ -649,6 +698,7 @@ impl<'a> InputKeyHandler<'a> { CurrentUi::Chart => self.do_chart_up(), CurrentUi::Search => self.do_search_step(step_type), CurrentUi::Activity => self.do_activity_up(), + CurrentUi::Recurring => self.do_recurring_step(step_type), CurrentUi::Initial => {} } self.check_autofill(); @@ -664,6 +714,7 @@ impl<'a> InputKeyHandler<'a> { CurrentUi::Chart => self.do_chart_down(), CurrentUi::Search => self.do_search_step(step_type), CurrentUi::Activity => self.do_activity_down(), + CurrentUi::Recurring => self.do_recurring_step(step_type), CurrentUi::Initial => {} } self.check_autofill(); @@ -674,6 +725,7 @@ impl<'a> InputKeyHandler<'a> { match self.page { CurrentUi::AddTx => self.check_add_tx_date(), CurrentUi::Search => self.check_search_date(), + CurrentUi::Recurring => self.check_recurring_date(), _ => {} } } @@ -683,6 +735,7 @@ impl<'a> InputKeyHandler<'a> { match self.page { CurrentUi::AddTx => self.check_add_tx_details(), CurrentUi::Search => self.check_search_details(), + CurrentUi::Recurring => self.check_recurring_details(), _ => {} } self.check_autofill(); @@ -701,6 +754,11 @@ impl<'a> InputKeyHandler<'a> { TxTab::ToMethod => self.check_search_to(), _ => {} }, + CurrentUi::Recurring => match self.recurring_tab { + TxTab::FromMethod => self.check_recurring_from()?, + TxTab::ToMethod => self.check_recurring_to()?, + _ => {} + }, _ => {} } self.check_autofill(); @@ -713,17 +771,19 @@ impl<'a> InputKeyHandler<'a> { match self.page { CurrentUi::AddTx => self.check_add_tx_amount()?, CurrentUi::Search => self.check_search_amount()?, + CurrentUi::Recurring => self.check_recurring_amount()?, _ => {} } Ok(()) } - // Checks and verifies TX type field + /// Checks and verifies TX type field pub fn handle_tx_type(&mut self) -> Result<()> { match self.page { CurrentUi::AddTx => self.check_add_tx_type()?, CurrentUi::Search => self.check_search_type(), + CurrentUi::Recurring => self.check_recurring_type()?, _ => {} } self.check_autofill(); @@ -736,11 +796,28 @@ impl<'a> InputKeyHandler<'a> { match self.page { CurrentUi::AddTx => self.check_add_tx_tags(), CurrentUi::Search => self.check_search_tags(), + CurrentUi::Recurring => self.check_recurring_tags(), _ => {} } self.check_autofill(); } + /// Checks and verifies the recurrence-specific fields (frequency, interval, day + /// value, month, end date) + pub fn handle_recurring_field(&mut self) -> Result<()> { + match self.recurring_tab { + TxTab::Frequency => self.check_recurring_frequency(), + TxTab::RecurInterval => self.check_recurring_recur_interval(), + TxTab::RecurValue => self.check_recurring_recur_value(), + TxTab::RecurMonth => self.check_recurring_recur_month(), + TxTab::EndDate => self.check_recurring_end_date(), + _ => {} + } + self.check_autofill(); + + Ok(()) + } + /// Resets all input boxes on Add TX and Transfer page pub fn clear_input(&mut self) -> Result<()> { match self.page { @@ -752,6 +829,9 @@ impl<'a> InputKeyHandler<'a> { *self.search_data = TxData::new_empty(); self.reset_search_data(); } + CurrentUi::Recurring => { + *self.recurring_data = TxData::new_empty(); + } _ => {} } @@ -763,6 +843,7 @@ impl<'a> InputKeyHandler<'a> { match self.page { CurrentUi::AddTx => self.add_tx_data.accept_autofill(self.add_tx_tab), CurrentUi::Search => self.search_data.accept_autofill(self.search_tab), + CurrentUi::Recurring => self.recurring_data.accept_autofill(self.recurring_tab), _ => {} } } @@ -773,6 +854,7 @@ impl<'a> InputKeyHandler<'a> { match self.page { CurrentUi::AddTx => *self.add_tx_tab = TxTab::Date, CurrentUi::Search => *self.search_tab = TxTab::Date, + CurrentUi::Recurring => *self.recurring_tab = TxTab::Date, _ => {} } self.go_correct_index(); @@ -824,6 +906,10 @@ impl<'a> InputKeyHandler<'a> { self.search_delete_tx()?; *self.popup_status = PopupType::Nothing; } + CurrentUi::Recurring => { + self.recurring_delete_selected()?; + *self.popup_status = PopupType::Nothing; + } _ => {} }, DeletionChoices::No => *self.popup_status = PopupType::Nothing, @@ -1022,10 +1108,83 @@ impl<'a> InputKeyHandler<'a> { Ok(()) } + /// Creates a new recurring transaction rule, or saves changes if one is being edited + pub fn save_recurring_tx(&mut self) -> Result<()> { + let status = self.recurring_data.add_recurring_tx(self.conn); + + match status { + Ok(()) => { + *self.recurring_data = TxData::new_empty(); + *self.recurring_tab = TxTab::Nothing; + // new recurring tx can potentially add new pending txs, in case the start date + // matches or below the current date. In such a case if new txs are added but tables + // are not reloaded, stale data will be shown. + self.reload_recurring_table()?; + self.reload_home_table(true)?; + self.reload_chart_data()?; + self.reload_summary()?; + self.reset_search_data(); + self.reload_activity_table()?; + } + Err(e) => self + .recurring_data + .add_tx_status(e.to_string(), LogType::Error), + } + + Ok(()) + } + + /// Loads the selected recurring rule from the table into the form for editing + pub fn recurring_edit_selected(&mut self) { + if let Some(index) = self.recurring_table.state.selected() { + let target = &self.recurring_txs[index]; + *self.recurring_data = TxData::from_full_recurring_tx(target, true); + *self.recurring_tab = TxTab::Nothing; + } + } + + /// Deletes the selected recurring rule. Previously materialized transactions are + /// untouched - only the schedule itself is removed. + pub fn recurring_delete_selected(&mut self) -> Result<()> { + let Some(index) = self.recurring_table.state.selected() else { + return Ok(()); + }; + + let target_id = self.recurring_txs[index].id; + self.conn.delete_recurring_tx(target_id)?; + + self.reload_recurring_table()?; + + if self.recurring_table.items.is_empty() { + self.recurring_table.state.select(None); + } else if index >= self.recurring_table.items.len() { + self.recurring_table + .state + .select(Some(self.recurring_table.items.len() - 1)); + } + + Ok(()) + } + + /// Pauses/unpauses the selected recurring rule + pub fn recurring_toggle_pause(&mut self) -> Result<()> { + if let Some(index) = self.recurring_table.state.selected() { + let target = &self.recurring_txs[index]; + let new_paused = !target.is_paused; + let id = target.id; + + self.conn.set_recurring_paused(id, new_paused)?; + self.reload_recurring_table()?; + } + + Ok(()) + } + pub fn switch_tx_position_up(&mut self) -> Result<()> { if let Some(index) = self.home_table.state.selected() { - // Don't do anything if there is 2 or less items or is selecting the first index which can't be moved up - if self.home_table.items.len() <= 2 || index == 0 { + // Nothing to swap with 1 tx, or when the first one is + // selected as it cannot move up + if self.home_table.items.len() <= 1 || index == 0 { return Ok(()); } @@ -1045,8 +1204,9 @@ impl<'a> InputKeyHandler<'a> { pub fn switch_tx_position_down(&mut self) -> Result<()> { if let Some(index) = self.home_table.state.selected() { - // Don't do anything if there is 1 or less items or is selecting the last index which can't be moved up - if self.home_table.items.len() <= 2 || index == self.home_table.items.len() - 1 { + // Nothing to swap with 1 tx, or when the last one is + // selected as it cannot move down + if self.home_table.items.len() <= 1 || index == self.home_table.items.len() - 1 { return Ok(()); } @@ -1147,10 +1307,9 @@ impl InputKeyHandler<'_> { fn do_home_up(&mut self) { match &self.home_tab { HomeTab::Table => { - // Do not select any table rows in the table section If - // there is no transaction - // if arrow key up is pressed and table index is 0, select the Month widget - // else just select the upper index of the table + // With no tx to select, step straight out of the table. Otherwise + // the first row moves the focus up to the month widget and any + // other row just moves the selection up if self.home_txs.is_empty() { *self.home_tab = self.home_tab.change_tab_up(); } else if self.home_table.state.selected() == Some(0) { @@ -1161,13 +1320,11 @@ impl InputKeyHandler<'_> { } } HomeTab::Years => { - // Do not select any table rows in the table section If - // there is no transaction + // With no tx to select, stay on the year widget. Otherwise enter + // the table on its last row if self.home_txs.is_empty() { *self.home_tab = self.home_tab.change_tab_down(); } else { - // Move to the selected value on table widget - // to the last row if pressed up on Year section self.home_table .state .select(Some(self.home_table.items.len() - 1)); @@ -1182,10 +1339,9 @@ impl InputKeyHandler<'_> { fn do_home_down(&mut self) { match &self.home_tab { HomeTab::Table => { - // Do not proceed to the table section If - // there is no transaction - // if arrow key down is pressed and table index is final, select the year widget - // else just select the next index of the table + // With no tx to select, step straight out of the table. Otherwise + // the last row moves the focus down to the year widget and any + // other row just moves the selection down if self.home_txs.is_empty() { *self.home_tab = self.home_tab.change_tab_down(); } else if self.home_table.state.selected() == Some(self.home_table.items.len() - 1) @@ -1197,8 +1353,8 @@ impl InputKeyHandler<'_> { } } HomeTab::Months => { - // Do not select any table rows in the table section If - // there is no transaction + // With no tx to select, stay on the month widget. Otherwise enter + // the table on its first row if self.home_txs.is_empty() { *self.home_tab = self.home_tab.change_tab_up(); } else { @@ -1388,7 +1544,7 @@ impl InputKeyHandler<'_> { } } - /// Handle key inputs for the Details field on the Add TX page + /// Handle key inputs for the Date field on the Add TX page fn check_add_tx_date(&mut self) { match self.key.code { KeyCode::Enter => { @@ -1645,6 +1801,474 @@ impl InputKeyHandler<'_> { } } + /// Handle key inputs for the Date field on the Recurring page + fn check_recurring_date(&mut self) { + match self.key.code { + KeyCode::Enter => { + let status = self.recurring_data.check_date(DateType::Exact, self.conn); + + match status { + Ok(data) => { + *self.recurring_tab = TxTab::Details; + self.go_correct_index(); + + self.recurring_data + .add_tx_status(data.to_string(), LogType::Info); + } + Err(e) => { + self.recurring_data + .add_tx_status(e.to_string(), LogType::Error); + } + } + } + KeyCode::Esc => { + let status = self.recurring_data.check_date(DateType::Exact, self.conn); + + match status { + Ok(data) => { + *self.recurring_tab = TxTab::Nothing; + + self.recurring_data + .add_tx_status(data.to_string(), LogType::Info); + } + Err(e) => { + self.recurring_data + .add_tx_status(e.to_string(), LogType::Error); + } + } + } + KeyCode::Backspace => self.recurring_data.edit_date(None), + KeyCode::Char(a) => self.recurring_data.edit_date(Some(a)), + _ => {} + } + } + + /// Handle key inputs for the Details field on the Recurring page + fn check_recurring_details(&mut self) { + match self.key.code { + KeyCode::Enter => { + *self.recurring_tab = TxTab::TxType; + self.go_correct_index(); + } + KeyCode::Esc => *self.recurring_tab = TxTab::Nothing, + KeyCode::Backspace => self.recurring_data.edit_details(None), + KeyCode::Char(a) => self.recurring_data.edit_details(Some(a)), + _ => {} + } + } + + /// Handle key inputs for the TX Type field on the Recurring page + fn check_recurring_type(&mut self) -> Result<()> { + match self.key.code { + KeyCode::Enter => { + let status = self.recurring_data.check_tx_type(self.conn); + + match status { + Ok(data) => { + *self.recurring_tab = TxTab::FromMethod; + self.go_correct_index(); + + self.recurring_data + .add_tx_status(data.to_string(), LogType::Info); + } + Err(e) => { + self.recurring_data + .add_tx_status(e.to_string(), LogType::Error); + } + } + } + KeyCode::Esc => { + let status = self.recurring_data.check_tx_type(self.conn); + + match status { + Ok(data) => { + *self.recurring_tab = TxTab::Nothing; + + self.recurring_data + .add_tx_status(data.to_string(), LogType::Info); + } + Err(e) => { + self.recurring_data + .add_tx_status(e.to_string(), LogType::Error); + } + } + } + KeyCode::Backspace => self.recurring_data.edit_tx_type(None), + KeyCode::Char(a) => self.recurring_data.edit_tx_type(Some(a)), + _ => {} + } + + Ok(()) + } + + /// Handle key inputs for the From Method field on the Recurring page + fn check_recurring_from(&mut self) -> Result<()> { + match self.key.code { + KeyCode::Enter => { + let status = self.recurring_data.check_from_method(self.conn); + + match status { + Ok(data) => { + match self.recurring_data.get_tx_type() { + TxType::IncomeExpense => *self.recurring_tab = TxTab::Amount, + TxType::Transfer => *self.recurring_tab = TxTab::ToMethod, + } + self.go_correct_index(); + + self.recurring_data + .add_tx_status(data.to_string(), LogType::Info); + } + Err(e) => { + self.recurring_data + .add_tx_status(e.to_string(), LogType::Error); + } + } + } + KeyCode::Esc => { + let status = self.recurring_data.check_from_method(self.conn); + + match status { + Ok(data) => { + *self.recurring_tab = TxTab::Nothing; + + self.recurring_data + .add_tx_status(data.to_string(), LogType::Info); + } + Err(e) => { + self.recurring_data + .add_tx_status(e.to_string(), LogType::Error); + } + } + } + KeyCode::Backspace => self.recurring_data.edit_from_method(None), + KeyCode::Char(a) => self.recurring_data.edit_from_method(Some(a)), + _ => {} + } + + Ok(()) + } + + /// Handle key inputs for the To Method field on the Recurring page + fn check_recurring_to(&mut self) -> Result<()> { + match self.key.code { + KeyCode::Enter => { + let status = self.recurring_data.check_to_method(self.conn); + + match status { + Ok(data) => { + *self.recurring_tab = TxTab::Amount; + self.go_correct_index(); + + self.recurring_data + .add_tx_status(data.to_string(), LogType::Info); + } + Err(e) => { + self.recurring_data + .add_tx_status(e.to_string(), LogType::Error); + } + } + } + KeyCode::Esc => { + let status = self.recurring_data.check_to_method(self.conn); + + match status { + Ok(data) => { + *self.recurring_tab = TxTab::Nothing; + + self.recurring_data + .add_tx_status(data.to_string(), LogType::Info); + } + Err(e) => { + self.recurring_data + .add_tx_status(e.to_string(), LogType::Error); + } + } + } + KeyCode::Backspace => self.recurring_data.edit_to_method(None), + KeyCode::Char(a) => self.recurring_data.edit_to_method(Some(a)), + _ => {} + } + + Ok(()) + } + + /// Handle key inputs for the Amount field on the Recurring page + fn check_recurring_amount(&mut self) -> Result<()> { + match self.key.code { + KeyCode::Enter => { + let status = self.recurring_data.check_amount(false, self.conn); + + match status { + Ok(data) => { + *self.recurring_tab = TxTab::Frequency; + self.go_correct_index(); + + self.recurring_data + .add_tx_status(data.to_string(), LogType::Info); + } + Err(e) => { + self.recurring_data + .add_tx_status(e.to_string(), LogType::Error); + } + } + } + KeyCode::Esc => { + let status = self.recurring_data.check_amount(false, self.conn); + + match status { + Ok(data) => { + *self.recurring_tab = TxTab::Nothing; + + self.recurring_data + .add_tx_status(data.to_string(), LogType::Info); + } + Err(e) => { + self.recurring_data + .add_tx_status(e.to_string(), LogType::Error); + } + } + } + KeyCode::Backspace => self.recurring_data.edit_amount(None), + KeyCode::Char(a) => self.recurring_data.edit_amount(Some(a)), + _ => {} + } + + Ok(()) + } + + /// Handle key inputs for the Frequency field on the Recurring page + fn check_recurring_frequency(&mut self) { + match self.key.code { + KeyCode::Enter => { + let status = self.recurring_data.check_frequency(self.conn); + + match status { + Ok(data) => { + *self.recurring_tab = TxTab::RecurInterval; + self.go_correct_index(); + + self.recurring_data + .add_tx_status(data.to_string(), LogType::Info); + } + Err(e) => { + self.recurring_data + .add_tx_status(e.to_string(), LogType::Error); + } + } + } + KeyCode::Esc => { + let status = self.recurring_data.check_frequency(self.conn); + + match status { + Ok(data) => { + *self.recurring_tab = TxTab::Nothing; + + self.recurring_data + .add_tx_status(data.to_string(), LogType::Info); + } + Err(e) => { + self.recurring_data + .add_tx_status(e.to_string(), LogType::Error); + } + } + } + KeyCode::Backspace => self.recurring_data.edit_frequency(None), + KeyCode::Char(a) => self.recurring_data.edit_frequency(Some(a)), + _ => {} + } + } + + /// Handle key inputs for the Recur Interval field on the Recurring page + fn check_recurring_recur_interval(&mut self) { + match self.key.code { + KeyCode::Enter => { + let status = self.recurring_data.check_recur_interval(self.conn); + + match status { + Ok(data) => { + *self.recurring_tab = match self.recurring_data.get_frequency() { + RecurrenceFrequency::Daily => TxTab::EndDate, + RecurrenceFrequency::Weekly + | RecurrenceFrequency::Monthly + | RecurrenceFrequency::Yearly => TxTab::RecurValue, + }; + self.go_correct_index(); + + self.recurring_data + .add_tx_status(data.to_string(), LogType::Info); + } + Err(e) => { + self.recurring_data + .add_tx_status(e.to_string(), LogType::Error); + } + } + } + KeyCode::Esc => { + let status = self.recurring_data.check_recur_interval(self.conn); + + match status { + Ok(data) => { + *self.recurring_tab = TxTab::Nothing; + + self.recurring_data + .add_tx_status(data.to_string(), LogType::Info); + } + Err(e) => { + self.recurring_data + .add_tx_status(e.to_string(), LogType::Error); + } + } + } + KeyCode::Backspace => self.recurring_data.edit_recur_interval(None), + KeyCode::Char(a) => self.recurring_data.edit_recur_interval(Some(a)), + _ => {} + } + } + + /// Handle key inputs for the Recur Value field on the Recurring page + fn check_recurring_recur_value(&mut self) { + match self.key.code { + KeyCode::Enter => { + let status = self.recurring_data.check_recur_value(self.conn); + + match status { + Ok(data) => { + *self.recurring_tab = match self.recurring_data.get_frequency() { + RecurrenceFrequency::Yearly => TxTab::RecurMonth, + RecurrenceFrequency::Daily + | RecurrenceFrequency::Weekly + | RecurrenceFrequency::Monthly => TxTab::EndDate, + }; + self.go_correct_index(); + + self.recurring_data + .add_tx_status(data.to_string(), LogType::Info); + } + Err(e) => { + self.recurring_data + .add_tx_status(e.to_string(), LogType::Error); + } + } + } + KeyCode::Esc => { + let status = self.recurring_data.check_recur_value(self.conn); + + match status { + Ok(data) => { + *self.recurring_tab = TxTab::Nothing; + + self.recurring_data + .add_tx_status(data.to_string(), LogType::Info); + } + Err(e) => { + self.recurring_data + .add_tx_status(e.to_string(), LogType::Error); + } + } + } + KeyCode::Backspace => self.recurring_data.edit_recur_value(None), + KeyCode::Char(a) => self.recurring_data.edit_recur_value(Some(a)), + _ => {} + } + } + + /// Handle key inputs for the Recur Month field on the Recurring page + fn check_recurring_recur_month(&mut self) { + match self.key.code { + KeyCode::Enter => { + let status = self.recurring_data.check_recur_month(self.conn); + + match status { + Ok(data) => { + *self.recurring_tab = TxTab::EndDate; + self.go_correct_index(); + + self.recurring_data + .add_tx_status(data.to_string(), LogType::Info); + } + Err(e) => { + self.recurring_data + .add_tx_status(e.to_string(), LogType::Error); + } + } + } + KeyCode::Esc => { + let status = self.recurring_data.check_recur_month(self.conn); + + match status { + Ok(data) => { + *self.recurring_tab = TxTab::Nothing; + + self.recurring_data + .add_tx_status(data.to_string(), LogType::Info); + } + Err(e) => { + self.recurring_data + .add_tx_status(e.to_string(), LogType::Error); + } + } + } + KeyCode::Backspace => self.recurring_data.edit_recur_month(None), + KeyCode::Char(a) => self.recurring_data.edit_recur_month(Some(a)), + _ => {} + } + } + + /// Handle key inputs for the End Date field on the Recurring page + fn check_recurring_end_date(&mut self) { + match self.key.code { + KeyCode::Enter => { + let status = self.recurring_data.check_end_date(self.conn); + + match status { + Ok(data) => { + *self.recurring_tab = TxTab::Tags; + self.go_correct_index(); + + self.recurring_data + .add_tx_status(data.to_string(), LogType::Info); + } + Err(e) => { + self.recurring_data + .add_tx_status(e.to_string(), LogType::Error); + } + } + } + KeyCode::Esc => { + let status = self.recurring_data.check_end_date(self.conn); + + match status { + Ok(data) => { + *self.recurring_tab = TxTab::Nothing; + + self.recurring_data + .add_tx_status(data.to_string(), LogType::Info); + } + Err(e) => { + self.recurring_data + .add_tx_status(e.to_string(), LogType::Error); + } + } + } + KeyCode::Backspace => self.recurring_data.edit_end_date(None), + KeyCode::Char(a) => self.recurring_data.edit_end_date(Some(a)), + _ => {} + } + } + + /// Handle key inputs for the Tag field on the Recurring page + fn check_recurring_tags(&mut self) { + match self.key.code { + KeyCode::Enter | KeyCode::Esc => { + *self.recurring_tab = TxTab::Nothing; + self.recurring_data.check_tags(self.conn); + } + KeyCode::Backspace => self.recurring_data.edit_tags(None), + KeyCode::Char(a) => self.recurring_data.edit_tags(Some(a)), + _ => {} + } + } + /// Handle key inputs for the Date field on the Search page fn check_search_date(&mut self) { match self.key.code { @@ -2023,11 +2647,29 @@ impl InputKeyHandler<'_> { Ok(()) } + /// Refetches the recurring transaction rules and rebuilds the Recurring page's table + fn reload_recurring_table(&mut self) -> Result<()> { + *self.recurring_txs = self.conn.get_recurring_txs()?; + + let old_table_position = self.recurring_table.state; + + let items = self + .recurring_txs + .iter() + .map(FullRecurringTx::to_array) + .collect(); + *self.recurring_table = TableData::new(items); + self.recurring_table.state = old_table_position; + + Ok(()) + } + /// Move the cursor for text fields to the correct position, if it's misplaced fn go_correct_index(&mut self) { match self.page { CurrentUi::AddTx => self.add_tx_data.go_current_index(self.add_tx_tab), CurrentUi::Search => self.search_data.go_current_index(self.search_tab), + CurrentUi::Recurring => self.recurring_data.go_current_index(self.recurring_tab), _ => {} } } @@ -2046,7 +2688,8 @@ impl InputKeyHandler<'_> { }; if let Err(e) = status { - self.add_tx_data.add_tx_status(e.to_string(), LogType::Info); + self.add_tx_data + .add_tx_status(e.to_string(), LogType::Error); } } @@ -2075,11 +2718,58 @@ impl InputKeyHandler<'_> { } Ok(()) } + TxTab::Details + | TxTab::Frequency + | TxTab::RecurInterval + | TxTab::RecurValue + | TxTab::RecurMonth + | TxTab::EndDate => Ok(()), + }; + + if let Err(e) = status { + self.search_data + .add_tx_status(e.to_string(), LogType::Error); + } + } + + fn do_recurring_step(&mut self, step_type: StepType) { + let status = match self.recurring_tab { + TxTab::Date => self + .recurring_data + .step_date(DateType::Exact, step_type, self.conn), + TxTab::FromMethod => self.recurring_data.step_from_method(step_type, self.conn), + TxTab::ToMethod => self.recurring_data.step_to_method(step_type, self.conn), + TxTab::Amount => self.recurring_data.step_amount(false, step_type, self.conn), + TxTab::TxType => self.recurring_data.step_tx_type(step_type, self.conn), + TxTab::Tags => self.recurring_data.step_tags(step_type, self.conn), + TxTab::Frequency => self.recurring_data.step_frequency(step_type, self.conn), + TxTab::RecurInterval => self + .recurring_data + .step_recur_interval(step_type, self.conn), + TxTab::RecurValue => self.recurring_data.step_recur_value(step_type, self.conn), + TxTab::RecurMonth => self.recurring_data.step_recur_month(step_type, self.conn), + TxTab::EndDate => self.recurring_data.step_end_date(step_type, self.conn), + TxTab::Nothing => { + match step_type { + StepType::StepUp => { + if !self.recurring_table.items.is_empty() { + self.recurring_table.previous(); + } + } + StepType::StepDown => { + if !self.recurring_table.items.is_empty() { + self.recurring_table.next(); + } + } + } + Ok(()) + } TxTab::Details => Ok(()), }; if let Err(e) = status { - self.search_data.add_tx_status(e.to_string(), LogType::Info); + self.recurring_data + .add_tx_status(e.to_string(), LogType::Error); } } @@ -2138,6 +2828,9 @@ impl InputKeyHandler<'_> { match self.page { CurrentUi::AddTx => self.add_tx_data.check_autofill(self.add_tx_tab, self.conn), CurrentUi::Search => self.search_data.check_autofill(self.search_tab, self.conn), + CurrentUi::Recurring => self + .recurring_data + .check_autofill(self.recurring_tab, self.conn), _ => {} } } @@ -2200,3 +2893,148 @@ impl InputKeyHandler<'_> { (previous_month, previous_year) } } + +fn recurring_number_jump_tabs(tx_type: TxType, frequency: RecurrenceFrequency) -> Vec { + let mut tabs = vec![ + TxTab::Date, + TxTab::Details, + TxTab::TxType, + TxTab::FromMethod, + ]; + + if let TxType::Transfer = tx_type { + tabs.push(TxTab::ToMethod); + } + + tabs.push(TxTab::Amount); + tabs.push(TxTab::Frequency); + tabs.push(TxTab::RecurInterval); + + match frequency { + RecurrenceFrequency::Daily => {} + RecurrenceFrequency::Weekly | RecurrenceFrequency::Monthly => { + tabs.push(TxTab::RecurValue); + } + RecurrenceFrequency::Yearly => { + tabs.push(TxTab::RecurValue); + tabs.push(TxTab::RecurMonth); + } + } + + tabs.push(TxTab::EndDate); + tabs.push(TxTab::Tags); + + tabs +} + +/// Maps a pressed digit to a 0-based field index: 1-9 -> 0-8 (the 1st-9th field), 0 -> 9 +/// (the 10th field). +fn digit_to_tab_index(digit: u32) -> usize { + if digit == 0 { 9 } else { digit as usize - 1 } +} + +#[cfg(test)] +mod number_jump_tests { + use super::*; + + #[test] + fn digit_to_tab_index_maps_1_through_9_to_0_through_8() { + for digit in 1..=9 { + assert_eq!(digit_to_tab_index(digit), digit as usize - 1); + } + } + + #[test] + fn digit_to_tab_index_maps_0_to_the_tenth_field() { + assert_eq!(digit_to_tab_index(0), 9); + } + + #[test] + fn income_expense_daily_has_nine_fields_both_end_date_and_tags_reachable() { + let tabs = recurring_number_jump_tabs(TxType::IncomeExpense, RecurrenceFrequency::Daily); + assert_eq!( + tabs, + vec![ + TxTab::Date, + TxTab::Details, + TxTab::TxType, + TxTab::FromMethod, + TxTab::Amount, + TxTab::Frequency, + TxTab::RecurInterval, + TxTab::EndDate, + TxTab::Tags, + ] + ); + assert_eq!(tabs[digit_to_tab_index(8)], TxTab::EndDate); + assert_eq!(tabs[digit_to_tab_index(9)], TxTab::Tags); + // No 10th field in this combination, so key 0 has nothing to jump to + assert!(tabs.get(digit_to_tab_index(0)).is_none()); + } + + #[test] + fn income_expense_weekly_or_monthly_has_ten_fields_both_end_date_and_tags_reachable() { + let tabs = recurring_number_jump_tabs(TxType::IncomeExpense, RecurrenceFrequency::Weekly); + assert_eq!(tabs.len(), 10); + assert_eq!(tabs[digit_to_tab_index(8)], TxTab::RecurValue); + assert_eq!(tabs[digit_to_tab_index(9)], TxTab::EndDate); + assert_eq!(tabs[digit_to_tab_index(0)], TxTab::Tags); + } + + #[test] + fn income_expense_yearly_has_eleven_fields_tags_has_no_key_left() { + let tabs = recurring_number_jump_tabs(TxType::IncomeExpense, RecurrenceFrequency::Yearly); + assert_eq!(tabs.len(), 11); + assert_eq!(tabs[digit_to_tab_index(8)], TxTab::RecurValue); + assert_eq!(tabs[digit_to_tab_index(9)], TxTab::RecurMonth); + assert_eq!(tabs[digit_to_tab_index(0)], TxTab::EndDate); + // Tags is the 11th field - out of digits (1-9 then 0 only covers 10) + assert!(tabs.get(10).is_some()); + assert_eq!(tabs[10], TxTab::Tags); + } + + #[test] + fn transfer_daily_has_ten_fields_both_end_date_and_tags_reachable() { + let tabs = recurring_number_jump_tabs(TxType::Transfer, RecurrenceFrequency::Daily); + assert_eq!(tabs.len(), 10); + assert_eq!(tabs[4], TxTab::ToMethod); + assert_eq!(tabs[digit_to_tab_index(9)], TxTab::EndDate); + assert_eq!(tabs[digit_to_tab_index(0)], TxTab::Tags); + } + + #[test] + fn transfer_weekly_or_monthly_has_eleven_fields_tags_has_no_key_left() { + let tabs = recurring_number_jump_tabs(TxType::Transfer, RecurrenceFrequency::Monthly); + assert_eq!(tabs.len(), 11); + assert_eq!(tabs[digit_to_tab_index(9)], TxTab::RecurValue); + assert_eq!(tabs[digit_to_tab_index(0)], TxTab::EndDate); + assert_eq!(tabs[10], TxTab::Tags); + } + + #[test] + fn transfer_yearly_has_twelve_fields_end_date_and_tags_have_no_key_left() { + let tabs = recurring_number_jump_tabs(TxType::Transfer, RecurrenceFrequency::Yearly); + assert_eq!(tabs.len(), 12); + assert_eq!( + &tabs[..10], + [ + TxTab::Date, + TxTab::Details, + TxTab::TxType, + TxTab::FromMethod, + TxTab::ToMethod, + TxTab::Amount, + TxTab::Frequency, + TxTab::RecurInterval, + TxTab::RecurValue, + TxTab::RecurMonth, + ] + ); + assert_eq!(tabs[digit_to_tab_index(9)], TxTab::RecurValue); + assert_eq!(tabs[digit_to_tab_index(0)], TxTab::RecurMonth); + // End Date and Tags are the 11th/12th fields here - genuinely out of digits, + // only reachable via sequential Enter in this one combination + assert_eq!(tabs[10], TxTab::EndDate); + assert_eq!(tabs[11], TxTab::Tags); + } +} diff --git a/tui/src/key_checker/mod.rs b/tui/src/key_checker/mod.rs index 736a59b1..46a6c5f5 100644 --- a/tui/src/key_checker/mod.rs +++ b/tui/src/key_checker/mod.rs @@ -5,6 +5,7 @@ mod home; mod initial; mod key_handler; mod popup; +mod recurring; mod search; mod summary; @@ -15,5 +16,6 @@ pub use home::home_keys; pub use initial::initial_keys; pub use key_handler::InputKeyHandler; pub use popup::popup_keys; +pub use recurring::recurring_keys; pub use search::search_keys; pub use summary::summary_keys; diff --git a/tui/src/key_checker/recurring.rs b/tui/src/key_checker/recurring.rs new file mode 100644 index 00000000..af9c3066 --- /dev/null +++ b/tui/src/key_checker/recurring.rs @@ -0,0 +1,64 @@ +use anyhow::Result; +use crossterm::event::KeyCode; + +use crate::key_checker::{InputKeyHandler, popup_keys}; +use crate::outputs::HandlingOutput; +use crate::page_handler::TxTab; +use crate::pages::PopupType; + +/// Tracks the keys of the Recurring Transactions page and calls relevant function based on it +pub fn recurring_keys(handler: &mut InputKeyHandler) -> Result> { + match handler.popup_status { + // We don't want to move this interface while the popup is on + PopupType::Nothing => match handler.recurring_tab { + TxTab::Nothing => match handler.key.code { + KeyCode::Char('a') => handler.go_add_tx()?, + KeyCode::Char('r') => handler.go_chart(), + KeyCode::Char('z') => handler.go_summary()?, + KeyCode::Char('q') => return Ok(Some(HandlingOutput::QuitUi)), + KeyCode::Char('j') => handler.do_config_popup(), + KeyCode::Char('f') => handler.go_home(), + KeyCode::Char('h') => handler.do_help_popup(), + KeyCode::Char('s') => handler.save_recurring_tx()?, + KeyCode::Char('c') => handler.clear_input()?, + KeyCode::Char('e') => handler.recurring_edit_selected(), + KeyCode::Char('d') => handler.do_deletion_popup(), + KeyCode::Char('p') => handler.recurring_toggle_pause()?, + KeyCode::Char('w') => handler.go_search(), + KeyCode::Char('y') => handler.go_activity(), + KeyCode::Char('t') => handler.next_theme()?, + KeyCode::Up => handler.handle_up_arrow(), + KeyCode::Down => handler.handle_down_arrow(), + KeyCode::Enter => handler.select_date_field(), + KeyCode::Char(c) if c.is_numeric() => { + handler.handle_number_press(); + } + _ => {} + }, + _ => match handler.key.code { + KeyCode::Right => handler.handle_right_arrow()?, + KeyCode::Left => handler.handle_left_arrow()?, + KeyCode::Up => handler.handle_up_arrow(), + KeyCode::Down => handler.handle_down_arrow(), + KeyCode::Tab => handler.do_autofill(), + _ => match handler.recurring_tab { + TxTab::Date => handler.handle_date(), + TxTab::Details => handler.handle_details(), + TxTab::FromMethod | TxTab::ToMethod => handler.handle_tx_method()?, + TxTab::Amount => handler.handle_amount()?, + TxTab::TxType => handler.handle_tx_type()?, + TxTab::Tags => handler.handle_tags(), + TxTab::Frequency + | TxTab::RecurInterval + | TxTab::RecurValue + | TxTab::RecurMonth + | TxTab::EndDate => handler.handle_recurring_field()?, + TxTab::Nothing => {} + }, + }, + }, + _ => return popup_keys(handler), + } + + Ok(None) +} diff --git a/tui/src/key_checker/search.rs b/tui/src/key_checker/search.rs index 08f3f92b..7abe5631 100644 --- a/tui/src/key_checker/search.rs +++ b/tui/src/key_checker/search.rs @@ -6,7 +6,7 @@ use crate::outputs::HandlingOutput; use crate::page_handler::TxTab; use crate::pages::PopupType; -/// Tracks the keys of the Add Tx page and calls relevant function based on it +/// Tracks the keys of the Search page and calls relevant function based on it pub fn search_keys(handler: &mut InputKeyHandler) -> Result> { match handler.popup_status { // We don't want to move this interface while the popup is on @@ -25,6 +25,7 @@ pub fn search_keys(handler: &mut InputKeyHandler) -> Result handler.search_edit_tx()?, KeyCode::Char('d') => handler.do_deletion_popup(), KeyCode::Char('y') => handler.go_activity(), + KeyCode::Char('u') => handler.go_recurring(), KeyCode::Char('t') => handler.next_theme()?, KeyCode::Up => handler.handle_up_arrow(), KeyCode::Down => handler.handle_down_arrow(), @@ -47,7 +48,12 @@ pub fn search_keys(handler: &mut InputKeyHandler) -> Result handler.handle_amount()?, TxTab::TxType => handler.handle_tx_type()?, TxTab::Tags => handler.handle_tags(), - TxTab::Nothing => {} + TxTab::Nothing + | TxTab::Frequency + | TxTab::RecurInterval + | TxTab::RecurValue + | TxTab::RecurMonth + | TxTab::EndDate => {} }, }, }, diff --git a/tui/src/key_checker/summary.rs b/tui/src/key_checker/summary.rs index db72706b..0ab3860d 100644 --- a/tui/src/key_checker/summary.rs +++ b/tui/src/key_checker/summary.rs @@ -19,6 +19,7 @@ pub fn summary_keys(handler: &mut InputKeyHandler) -> Result handler.do_summary_hidden_mode(), KeyCode::Char('x') => handler.change_summary_sort(), KeyCode::Char('y') => handler.go_activity(), + KeyCode::Char('u') => handler.go_recurring(), KeyCode::Char('t') => handler.next_theme()?, KeyCode::Right => handler.handle_right_arrow()?, KeyCode::Left => handler.handle_left_arrow()?, diff --git a/tui/src/main.rs b/tui/src/main.rs index bd55ef74..cb3d0fda 100644 --- a/tui/src/main.rs +++ b/tui/src/main.rs @@ -8,9 +8,11 @@ mod tx_handler; mod utility; use dirs::data_local_dir; +use log::LevelFilter; use page_handler::initialize_app; +use simplelog::{Config, WriteLogger}; use std::env::{current_dir, set_current_dir}; -use std::fs; +use std::fs::{self, File}; fn main() { if let Some(dir) = data_local_dir() { @@ -32,6 +34,12 @@ fn main() { return; }; + let _ = WriteLogger::init( + LevelFilter::Info, + Config::default(), + File::create("logs.log").unwrap(), + ); + let mut migrated_path = working_path.clone(); migrated_path.push("rex.sqlite"); diff --git a/tui/src/outputs/error.rs b/tui/src/outputs/error.rs index fa7ae7b6..2df67f6c 100644 --- a/tui/src/outputs/error.rs +++ b/tui/src/outputs/error.rs @@ -24,6 +24,14 @@ pub enum CheckingError { EmptyTxType, #[strum(to_string = "Tx Method: From and To methods cannot be the same for Transfer")] SameTxMethod, + #[strum(to_string = "Frequency: Frequency cannot be empty")] + EmptyFrequency, + #[strum(to_string = "Recur Interval: Recur Interval cannot be empty")] + EmptyRecurInterval, + #[strum(to_string = "Recur Value: Recur Value cannot be empty for this frequency")] + EmptyRecurValue, + #[strum(to_string = "Recur Month: Recur Month cannot be empty for Yearly frequency")] + EmptyRecurMonth, } impl Error for CheckingError {} diff --git a/tui/src/page_handler/initializer.rs b/tui/src/page_handler/initializer.rs index 1372dc73..5b9759b1 100644 --- a/tui/src/page_handler/initializer.rs +++ b/tui/src/page_handler/initializer.rs @@ -16,13 +16,15 @@ use crate::utility::{ check_version, enter_tui_interface, exit_tui_interface, migrate_to_new_schema, start_terminal, }; -/// Initialize the TUI loop +/// Sets up a terminal, the migrated database and config plus a background +/// version check, then runs the TUI loop until the user quits pub fn initialize_app( old_db_path: &PathBuf, migrated_db_path: &Path, original_dir: &PathBuf, ) -> Result<()> { - // If is not terminal, try to start a terminal otherwise create an error.txt file with the error message + // Without a terminal, try to start one. If that fails, write the message to + // Error.txt and exit if !atty::is(Stream::Stdout) && !start_terminal(original_dir.to_str().unwrap()) { let mut error_location = PathBuf::from(&original_dir); error_location.push("Error.txt"); @@ -86,22 +88,17 @@ pub fn initialize_app( migrated_db_path.to_path_buf() }; - let mut migrated_conn = get_conn(new_db_path.display().to_string().as_str()); + let mut conn = get_conn(new_db_path.display().to_string().as_str()); loop { let mut terminal = enter_tui_interface()?; - let result = start_app( - &mut terminal, - new_update.clone(), - &mut config, - &mut migrated_conn, - ); + let result = start_app(&mut terminal, new_update.clone(), &mut config, &mut conn); exit_tui_interface()?; match result { Ok(output) => match output { HandlingOutput::QuitUi => { - drop(migrated_conn); + drop(conn); config.save_backup(&new_db_path.clone()); break; } diff --git a/tui/src/page_handler/ui_handler.rs b/tui/src/page_handler/ui_handler.rs index 30eacfff..328e5cfc 100644 --- a/tui/src/page_handler/ui_handler.rs +++ b/tui/src/page_handler/ui_handler.rs @@ -1,7 +1,7 @@ use crossterm::event::{self, Event, KeyEventKind, poll}; use ratatui::Terminal; use ratatui::backend::Backend; -use rex_app::conn::{DbConn, FetchNature}; +use rex_app::conn::{DbConn, FetchNature, FullRecurringTx}; use rex_app::ui_helper::DateType; use rex_app::views::SearchView; use std::sync::{Arc, Mutex}; @@ -9,8 +9,8 @@ use std::time::Duration; use crate::config::Config; use crate::key_checker::{ - InputKeyHandler, activity_keys, add_tx_keys, chart_keys, home_keys, initial_keys, search_keys, - summary_keys, + InputKeyHandler, activity_keys, add_tx_keys, chart_keys, home_keys, initial_keys, + recurring_keys, search_keys, summary_keys, }; use crate::outputs::{HandlingOutput, UiHandlingError}; use crate::page_handler::{ @@ -18,47 +18,48 @@ use crate::page_handler::{ TxTab, }; use crate::pages::{ - InfoPopupState, PopupType, activity_ui, add_tx_ui, chart_ui, home_ui, initial_ui, search_ui, - summary_ui, + InfoPopupState, InitialData, PopupType, activity_ui, add_tx_ui, chart_ui, home_ui, initial_ui, + recurring_ui, search_ui, summary_ui, }; use crate::theme::Theme; use crate::tx_handler::TxData; use crate::utility::LerpState; -/// Starts the interface and run the app +/// Starts the interface and runs the app pub fn start_app( terminal: &mut Terminal, new_version_data: Arc>>>, config: &mut Config, conn: &mut DbConn, ) -> Result { - // Setting up some default values. Let's go through all of them + // Set up the initial state of every page and widget + let mut popup_status = PopupType::Nothing; + + let recurring_added = match conn.process_due_recurring_txs() { + Ok(count) => count, + Err(e) => { + let state = InfoPopupState::Error(format!("Failed to process recurring TXs: {e}")); + popup_status = PopupType::new_info(state); + 0 + } + }; let mut theme = Theme::new_index(config.theme_index.unwrap_or(0)); - // Contains the homepage month list that is indexed + // Indexed month/year/mode/method lists backing each page's selectors. The + // `_no_local` variants start at the first entry instead of the device's + // current month or year let mut home_months = IndexedData::new_monthly(); - // Contains the homepage year list that is indexed let mut home_years = IndexedData::new_yearly(); - // Contains the chart page month list that is indexed let mut chart_months = IndexedData::new_monthly_no_local(); - // Contains the chart page year list that is indexed let mut chart_years = IndexedData::new_yearly_no_local(); - // Contains the chart page mode selection list that is indexed let mut chart_modes = IndexedData::new_modes(); - // Contains the chart page TX method selection list that is indexed let mut chart_tx_methods = IndexedData::new_tx_methods_cumulative(conn); - - // Contains the summary page month list that is indexed let mut summary_months = IndexedData::new_monthly_no_local(); - // Contains the summary page year list that is indexed let mut summary_years = IndexedData::new_yearly_no_local(); - // Contains the summary page mode selection list that is indexed let mut summary_modes = IndexedData::new_modes(); - // Contains the Activity page month list that is indexed - let mut activity_years = IndexedData::new_yearly(); - // Contains the Activity page month list that is indexed let mut activity_months = IndexedData::new_monthly(); + let mut activity_years = IndexedData::new_yearly(); // The selected widget on the homepage. Default set to the month selection let mut home_tab = HomeTab::Months; @@ -84,8 +85,6 @@ pub fn start_app( // The page which is currently selected. Default is the initial page let mut page = CurrentUi::Initial; - let mut popup_status = PopupType::Nothing; - // Stores the current selected widget on Add Transaction page let mut add_tx_tab = TxTab::Nothing; // Store the current selected widget on Chart page @@ -98,13 +97,17 @@ pub fn start_app( let mut search_date_type = DateType::Exact; // Store the current selected widget on Activity page let mut activity_tab = ActivityTab::Years; + // Store the current selected widget on Recurring page + let mut recurring_tab = TxTab::Nothing; // Holds the data that will be/are inserted into the Add TX page's input fields let mut add_tx_data = TxData::new(); // Holds the data that will be/are inserted into the Search page's input fields let mut search_data = TxData::new_empty(); + // Holds the data that will be/are inserted into the Recurring page's input fields + let mut recurring_data = TxData::new_empty(); - // Chart view contains TX list to create the chart. + // TXs of the selected period, used to draw the chart let mut chart_view = conn .get_chart_view_with_str( chart_months.get_selected_value(), @@ -113,7 +116,7 @@ pub fn start_app( ) .unwrap(); - // Summary view contains TX list to create the summary. + // TXs of the selected period, used to generate the summary let mut summary_view = conn .get_summary_with_str( summary_months.get_selected_value(), @@ -122,7 +125,7 @@ pub fn start_app( ) .unwrap(); - // Activity view contains TX list to create the activity. + // TXs of the selected month and year, used to generate the activity table let mut activity_view = conn .get_activity_view_with_str( activity_months.get_selected_value(), @@ -142,6 +145,20 @@ pub fn start_app( // Data for the Activity Page's table let mut activity_table = TableData::new(activity_view.get_activity_table()); + // The currently known recurring transaction rules + let mut recurring_txs = conn.get_recurring_txs().unwrap(); + + // Startup screen data, loaded once since that page redraws every few milliseconds + let initial_data = InitialData::load(conn, &recurring_txs, recurring_added); + + // Data for the Recurring Page's table + let mut recurring_table = TableData::new( + recurring_txs + .iter() + .map(FullRecurringTx::to_array) + .collect(), + ); + // The initial page REX loading index let mut starter_index = 0; @@ -163,19 +180,18 @@ pub fn start_app( // The generated balance section on the Add TX UI let mut add_tx_balance = Vec::new(); - // Home and add TX page balance section's column space let mut lerp_state = LerpState::new(1.0); let mut version_checked = false; - // How it work: - // Default value from above -> Goes to an interface page and render -> Wait for an event key press. + // How it works: + // Every value set up above -> rendered on the matching page -> wait for a key press. // - // Based on whether there is any active lerp, the UI will continue to render until all lerp ends + // As long as a lerp is running the UI keeps re-rendering until all of them end // - // If key press is detected, send most of the mutable values to InputKeyHandler -> Gets mutated based on keypress - // -> loop ends -> start from beginning -> Send the new mutated values to the interface -> Keep up + // On a key press the mutable values are handed to InputKeyHandler, which mutates + // them -> the loop restarts -> the new values are sent to the interface -> repeat loop { if !version_checked { let update_lock = new_version_data.lock().unwrap(); @@ -187,7 +203,7 @@ pub fn start_app( version_checked = true; } } - // If TX method list is empty, forcefully ask to create a new TX method + // With no TX method at all, keep asking the user to create one if conn.is_tx_method_empty() && let PopupType::Nothing = popup_status { @@ -220,7 +236,7 @@ pub fn start_app( conn, ), - CurrentUi::Initial => initial_ui(f, starter_index, &theme), + CurrentUi::Initial => initial_ui(f, starter_index, &initial_data, &theme), CurrentUi::Chart => chart_ui( f, @@ -271,6 +287,14 @@ pub fn start_app( &mut lerp_state, &theme, ), + CurrentUi::Recurring => recurring_ui( + f, + &recurring_data, + &recurring_tab, + &mut recurring_table, + &mut lerp_state, + &theme, + ), } popup_status.show_ui(f, &theme); @@ -294,7 +318,8 @@ pub fn start_app( | CurrentUi::Summary | CurrentUi::Search | CurrentUi::Chart - | CurrentUi::Activity => { + | CurrentUi::Activity + | CurrentUi::Recurring => { // If at least 1 lerp is in progress and no key press detected, continue the loop if lerp_state.has_active_lerps() && !poll(Duration::from_millis(2)).map_err(UiHandlingError::Polling)? @@ -346,6 +371,10 @@ pub fn start_app( &mut activity_tab, &mut activity_view, &mut activity_table, + &mut recurring_data, + &mut recurring_tab, + &mut recurring_table, + &mut recurring_txs, &mut chart_hidden_mode, &mut chart_hidden_legends, &mut summary_hidden_mode, @@ -364,6 +393,7 @@ pub fn start_app( CurrentUi::Summary => summary_keys(&mut handler), CurrentUi::Search => search_keys(&mut handler), CurrentUi::Activity => activity_keys(&mut handler), + CurrentUi::Recurring => recurring_keys(&mut handler), }; match status { diff --git a/tui/src/page_handler/ui_state.rs b/tui/src/page_handler/ui_state.rs index 6e16b9f8..c48c7d3f 100644 --- a/tui/src/page_handler/ui_state.rs +++ b/tui/src/page_handler/ui_state.rs @@ -26,21 +26,20 @@ pub const YEARS: [&str; 16] = [ pub const MODES: [&str; 3] = ["Monthly", "Yearly", "All Time"]; -/// The struct stores all transaction data for the Transaction widget -/// and creates an index to keep track of which transactions row is selected -/// if any. Each vec inside the vec of items contains 1 full transaction. +/// The rows of a transaction table, each row holding the already formatted +/// columns of one transaction, plus the state ratatui needs to know which row +/// is selected and where the viewport starts. /// -/// state : `None` or an index -/// items : `[["2022-05-01", "test", "source_1", "15.50", Expense], ]` +/// state: `None` when nothing is selected, otherwise the selected row index +/// items: `[["2022-05-01", "test", "source_1", "15.50", "Expense"], ]` pub struct TableData { pub state: TableState, pub items: Vec>, } impl TableData { - /// Creates the default table state and adds the manual transaction data - /// that was passed to it as an argument to consider them as a value of an index. - /// State is the library default. + /// Wraps the given rows in a table whose selection state is ratatui's + /// default, meaning nothing is selected yet #[must_use] pub fn new(data: Vec>) -> Self { TableData { @@ -80,8 +79,8 @@ impl TableData { } } -/// This struct takes anything inside a vector and adds an index it it. -/// It is used for keeping track of the Months and Years current index. +/// Wraps a list of values and keeps track of which one is currently selected. +/// Used for the month and year lists of the selectors. /// /// titles: `["January", "February",]` #[derive(Clone)] @@ -185,10 +184,8 @@ impl IndexedData { } } -/// The enum is used to keep track of which tab is currently set at active -/// or being interacted with in the Homepage. There are 3 interact-able widgets -/// in the homepage thus three values. The goal is to keep them cycling through -/// all values. +/// Which of the homepage's 3 selectable widgets is currently focused. The focus +/// cycles through all of them, so it never gets stuck at an end. pub enum HomeTab { Years, Months, @@ -215,9 +212,10 @@ impl HomeTab { } } -/// This enum is used inside the Add Transaction page. -/// This is targeted to be used to keep track which widget of the Add Transaction -/// is currently being interacted with. +/// Which field of a transaction form is currently being edited. The Add +/// Transaction, Search and Recurring pages each keep their own value of this. +/// The last five values only ever appear on the Recurring page. +#[derive(Debug, PartialEq)] pub enum TxTab { Date, Details, @@ -226,11 +224,15 @@ pub enum TxTab { Amount, TxType, Tags, + Frequency, + RecurInterval, + RecurValue, + RecurMonth, + EndDate, Nothing, } -/// Shows the currently active page in the terminal. Used to properly -/// direct keypresses to the relevant structs and widget selection. +/// Which page is currently shown, used to send keypresses to the right handler pub enum CurrentUi { Initial, Home, @@ -239,6 +241,7 @@ pub enum CurrentUi { Summary, Search, Activity, + Recurring, } pub enum ChartTab { diff --git a/tui/src/pages/activity_ui.rs b/tui/src/pages/activity_ui.rs index 4af252c6..c6db7ff0 100644 --- a/tui/src/pages/activity_ui.rs +++ b/tui/src/pages/activity_ui.rs @@ -30,7 +30,7 @@ pub fn activity_ui( false }; - // Extra status field for search txs and edit txs + // Search and edit txs get an extra Status column let activity_tx_header_vec = if add_extra_field { vec![ "Date", @@ -45,7 +45,7 @@ pub fn activity_ui( vec!["Date", "Details", "TX Method", "Amount", "Type", "Tags"] }; - // Based on extra field, allocate size + // The column widths have to match the extra column above let activity_tx_header_widths = if add_extra_field { vec![ Constraint::Percentage(10), @@ -119,8 +119,8 @@ pub fn activity_ui( let activity_tx_rows = activity_txs_table.items.iter().map(|item| { let height = 1; - // First index is the date field. Do not add commas to the value - // In case search happens by yearly value, this can add comma to the year + // The first column is the date, which gets no separators. A yearly search + // would otherwise end up with a comma in the year let mut first_index_passed = false; let cells = item.iter().map(|c| { if first_index_passed { diff --git a/tui/src/pages/add_tx_ui.rs b/tui/src/pages/add_tx_ui.rs index c4cb4b99..7e66daf7 100644 --- a/tui/src/pages/add_tx_ui.rs +++ b/tui/src/pages/add_tx_ui.rs @@ -39,8 +39,9 @@ pub fn add_tx_ui( } let status_data = add_tx_data.get_tx_status(); - // Contains date, details, from method, to method, amount, tx type, tags. - // Except to method, rest will be used for the widgets + // Date, details, from method, to method, amount, tx type, tags and autofill. + // The widgets are rendered from this, skipping the to method unless the tx is + // a transfer let input_data = add_tx_data.get_all_texts(); // The index of the cursor position let current_index = add_tx_data.get_current_index(); @@ -70,8 +71,8 @@ pub fn add_tx_ui( ]) .split(size); - // Based on the tx type divide the first chunk into 5 or 6 parts horizontally - // this chunk contains the input boxes take takes input + // Depending on the tx type, split the first chunk into 5 or 6 columns. + // This chunk holds the input boxes that take the user input let input_chunk = { match tx_type { TxType::IncomeExpense => Layout::default() @@ -132,8 +133,8 @@ pub fn add_tx_ui( let lerp_id = format!("{row_type}:{index}"); let to_show = lerp_state.lerp(&lerp_id, actual_data, None); - // re-add the previously removed symbol if is the Changes row - // Otherwise separate the number with commas + // Put the symbol back on the Changes row, otherwise the number + // gets thousands separators if let Some(sym) = symbol { format!("{sym}{to_show:.2}",).separate_with_commas() } else { @@ -165,8 +166,8 @@ pub fn add_tx_ui( let mut status_text = vec![]; - // Iter through the data in reverse mode because we want the latest status text - // to be at the top which is the final value of the vector. + // Iterate in reverse so the latest status line, which is the last value of the + // vector, ends up at the top for i in status_data.iter().rev() { let (initial, rest) = i.text.split_once(':').unwrap(); @@ -196,7 +197,8 @@ pub fn add_tx_ui( } } - // We already fetched the data for each of these. Assign them now and then use them to load the widget + // The values were already fetched above, so they just need to be assigned + // and loaded into their widgets let date_text = Line::from(format!("{} ", input_data[0])); let mut details_text = Line::from(format!("{} ", input_data[1])); @@ -285,8 +287,8 @@ pub fn add_tx_ui( .block(styled_block("Tags", theme)) .alignment(Alignment::Left); - // We will be adding a cursor based on which tab is selected + the selected index. - // This was created utilizing the tui-rs example named user_input.rs + // Put the cursor at the index of the currently selected field. + // Adapted from the tui-rs example named user_input.rs match add_tx_tab { TxTab::Date => f.set_cursor_position(Position { x: input_chunk[0].x + current_index as u16 + 1, diff --git a/tui/src/pages/chart_ui.rs b/tui/src/pages/chart_ui.rs index 2026816d..1f4e3a39 100644 --- a/tui/src/pages/chart_ui.rs +++ b/tui/src/pages/chart_ui.rs @@ -33,7 +33,7 @@ pub fn chart_ui( ) { let size = f.area(); - // Divide the terminal into various chunks to draw the interface. This is a vertical chunk + // Split the terminal into the vertical chunks the interface is drawn from let mut main_layout = Layout::default().direction(Direction::Vertical).margin(2); // Don't create any other chunk if hidden mode is enabled. Create 1 chunk that will be used for the chart itself @@ -104,12 +104,12 @@ pub fn chart_ui( let mut all_tx_methods: Vec<&str> = tx_methods.iter().map(|t| t.name.as_str()).collect(); all_tx_methods.push("Cumulative"); - // A vector containing another vector vec![X, Y] with coordinate of where to render chart points + // One [X, Y] coordinate per point to draw, one such vec per tx method let mut datasets: Vec> = Vec::new(); let mut last_balances = Vec::new(); - // Adding default initial value if no data to load + // Default initial values in case there is no data to load if chart_view.is_empty() { for _ in 0..all_tx_methods.len() { datasets.push(vec![(0.0, 0.0)]); @@ -141,9 +141,9 @@ pub fn chart_ui( date_labels.push(checking_date.to_string()); date_labels.push(final_date.to_string()); - // `data_num` represents which index to check out from all the txs and balances data. - // `to_add_again` will become true in cases where two or more transactions shares the same date simultaneously. - // Same date transactions movements will be combined together into 1 chart location + // `data_num` is the index to read the txs and balances from. + // `to_add_again` becomes true when two or more txs share the same date, as + // the movements of such a day are combined into a single chart point let mut to_add_again = false; let mut data_num = 0; @@ -157,8 +157,9 @@ pub fn chart_ui( if chart_view.len() > data_num + 1 { next_date = chart_view.get_tx(data_num + 1).date.date(); } - // New valid transactions so the earlier looped balance is not required. - // If no tx exists in a date, data from last_balances/previous valid date is used to compensate for it + // A tx was found for this date, so the balances from the previous + // valid date are no longer needed. A date without a tx is filled in + // from `last_balances` instead last_balances = Vec::new(); let mut cumulative_balance = 0.0; @@ -176,9 +177,10 @@ pub fn chart_ui( balance.value() }; - // We will not consider the highest/lowest balance if the method is currently deactivated on chart. - // We can't directly skip it because the dataset vector expects something in the index of this method. - // We will have the data but it just won't be shown on the chart. + // A deactivated method must not move the highest/lowest + // balance, but it cannot be skipped either since the dataset + // expects a value at every method's index. The data is there, + // it just isn't drawn if chart_activated_methods[all_tx_methods[method_index]] { if current_balance > highest_balance { highest_balance = current_balance; @@ -188,10 +190,10 @@ pub fn chart_ui( } if to_add_again { - // If to_add_again is true, means in the last loop, the date, and the current date was the same - // as the date is the same, the data needs to be merged thus using the same x y point in the chart. - // Pop the last one added and that to last_balance. If the next date is the same, - // last_balance will be used to keep on merging the data. + // `to_add_again` means the last loop already added a point + // for this same date, so pop it and re-add it with this + // loop's balance merged in. As long as the next date stays + // the same, `last_balance` keeps feeding the merge. let (position, _balance) = datasets[method_index].pop().unwrap(); let to_push = vec![(position, current_balance)]; @@ -210,7 +212,7 @@ pub fn chart_ui( } if next_date == checking_date { - // The axis won't move if the next date is the same. + // The axis stays put while the next date is the same to_add_again = true; } else { to_add_again = false; @@ -218,10 +220,10 @@ pub fn chart_ui( checking_date += Duration::days(1); } - // Successfully checked a transaction, we will check the new index in the next iteration + // This tx was consumed, so read the next index on the next iteration data_num += 1; } else { - // As the date does not exist in the transaction list, we will use the last used balance and add a point in the chart + // No tx on this date, so repeat the last known balance as a point for method_index in 0..all_tx_methods.len() { let to_push = vec![(current_axis, last_balances[method_index])]; datasets[method_index].extend(to_push); @@ -245,8 +247,8 @@ pub fn chart_ui( } } } - // Add a few % extra value to the highest and the lowest balance - // so the chart can properly render + + // Pad the highest and lowest balance by a few % so the chart has some room highest_balance += highest_balance * 5.0 / 100.0; lowest_balance -= lowest_balance * 5.0 / 100.0; @@ -254,9 +256,10 @@ pub fn chart_ui( let mut to_add = lowest_balance; - // Go through the lowest balance and keep adding the difference until the highest point + // Start at the lowest balance and keep adding the difference to reach the + // highest one. 10 labels, so loop 10 times let mut labels = vec![lowest_balance.to_string()]; - // 10 labels, so loop 10 times + for _i in 0..10 { to_add += diff; labels.push(format!("{to_add:.2}")); diff --git a/tui/src/pages/home_ui.rs b/tui/src/pages/home_ui.rs index eb2586e9..63c060d6 100644 --- a/tui/src/pages/home_ui.rs +++ b/tui/src/pages/home_ui.rs @@ -99,17 +99,10 @@ pub fn home_ui( .style(Style::default().bg(theme.background()).fg(theme.text())) }); - // Decides how many chunks of spaces in the terminal will be. - // Each constraint creates an empty space in the terminal with the given - // length. The final one was given 0 as minimum value which is the Transaction - // field to keep it expanding. - - // Chunks are used in this format respectively // - The Balance tab // - The year tab // - The month tab // - The transaction list/Table - let chunks = Layout::default() .direction(Direction::Vertical) .margin(2) @@ -190,8 +183,8 @@ pub fn home_ui( let lerp_id = format!("{row_type}:{index}"); let to_show = lerp_state.lerp(&lerp_id, actual_data, None); - // re-add the previously removed symbol if is the Changes row - // Otherwise separate the number with commas + // Put the symbol back on the Changes row, otherwise the number + // gets thousands separators if let Some(sym) = symbol { format!("{sym}{to_show:.2}",).separate_with_commas() } else { @@ -224,11 +217,11 @@ pub fn home_ui( .style(Style::default().fg(theme.border())); match current_tab { - // Previously added a black block to year and month widget if a value is not selected - // Now we will turn that black block into green if a value is selected + // A black block was added to the year and month widgets when no value is + // selected, so turn it green for the selected one HomeTab::Months => month_tab = month_tab.highlight_style(tab_highlight_style(theme)), HomeTab::Years => year_tab = year_tab.highlight_style(tab_highlight_style(theme)), - // Changes the color of row based on Expense or Income tx type on Transaction widget. + // Color the selected row based on its Expense or Income tx type HomeTab::Table => { if let Some(a) = home_table.state.selected() { table_area = table_area.highlight_symbol(">> "); @@ -266,12 +259,12 @@ pub fn home_ui( *home_table.state.offset_mut() = index - 10; } - // After all data is in place, render the widgets one by one - // the chunks are selected based on the format I want the widgets to render + // Now that every widget holds its data, render them one by one into the + // chunks laid out above f.render_widget(balance_area, chunks[0]); f.render_widget(month_tab, chunks[2]); f.render_widget(year_tab, chunks[1]); - // This one is different because the Transaction widget interface works differently + // The table is the only stateful widget, as its selection has to be tracked f.render_stateful_widget(table_area, chunks[3], &mut home_table.state); } diff --git a/tui/src/pages/initial_ui.rs b/tui/src/pages/initial_ui.rs index e8873e5c..e0da4936 100644 --- a/tui/src/pages/initial_ui.rs +++ b/tui/src/pages/initial_ui.rs @@ -1,14 +1,145 @@ +use chrono::{Days, Local, NaiveDate}; use ratatui::Frame; use ratatui::layout::{Alignment, Constraint, Direction, Layout}; use ratatui::style::{Modifier, Style}; -use ratatui::widgets::{Paragraph, Wrap}; +use ratatui::text::{Line, Span}; +use ratatui::widgets::{Cell, Paragraph, Row, Table, Wrap}; +use rex_app::conn::{DbConn, FullRecurringTx}; +use thousands::Separable; -use crate::pages::{A, F, H, J, Q, R, V, W, Y, Z}; +use crate::pages::{A, F, J, Q, R, U, W, Y, Z}; use crate::theme::Theme; use crate::utility::{create_bolded_text, main_block, styled_block}; +/// How many days ahead the startup screen looks for upcoming recurring transactions +const UPCOMING_DAYS: u64 = 7; +/// The most upcoming recurring transactions listed on the startup screen +const UPCOMING_MAX_ROWS: usize = 10; + +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum Sign { + Positive, + Negative, + Neutral, +} + +pub struct UpcomingRow { + pub date: String, + pub details: String, + pub amount: String, + pub sign: Sign, +} + +/// Everything the startup screen shows besides the static key reference. Loaded once +/// before the UI loop starts since the page is redrawn every few milliseconds. +pub struct InitialData { + pub recurring_added: usize, + /// Method name, formatted balance, whether the balance is negative + pub balances: Vec<(String, String, bool)>, + pub total: Option<(String, bool)>, + pub upcoming: Vec, + pub paused_count: usize, +} + +impl InitialData { + pub fn load(conn: &mut DbConn, recurring: &[FullRecurringTx], recurring_added: usize) -> Self { + let final_balances = conn.get_final_balances().unwrap_or_default(); + + let mut balances = Vec::new(); + let mut total_cents = 0_i64; + + for method in conn.get_tx_methods_sorted() { + let cents = final_balances.get(&method.id).map_or(0, |b| b.balance); + total_cents += cents; + balances.push((method.name.clone(), format_cents(cents), cents < 0)); + } + + let total = (balances.len() > 1).then(|| (format_cents(total_cents), total_cents < 0)); + + let today = Local::now().date_naive(); + + let mut due_soon: Vec<&FullRecurringTx> = recurring + .iter() + .filter(|r| { + is_upcoming( + r.next_recurring_date, + r.end_date, + r.is_paused, + today, + UPCOMING_DAYS, + ) + }) + .collect(); + + due_soon.sort_by_key(|r| r.next_recurring_date); + + let upcoming = due_soon + .into_iter() + .take(UPCOMING_MAX_ROWS) + .map(|r| { + let tx_type = r.tx_type.to_string(); + + let sign = match tx_type.as_str() { + "Income" | "Borrow" | "Lend Repay" => Sign::Positive, + "Expense" | "Lend" | "Borrow Repay" => Sign::Negative, + _ => Sign::Neutral, + }; + + UpcomingRow { + date: r.next_recurring_date.format("%a %b %d").to_string(), + details: r.details.clone().unwrap_or(tx_type), + amount: format!("{:.2}", r.amount.dollar()).separate_with_commas(), + sign, + } + }) + .collect(); + + Self { + recurring_added, + balances, + total, + upcoming, + paused_count: recurring.iter().filter(|r| r.is_paused).count(), + } + } +} + +fn format_cents(cents: i64) -> String { + format!("{:.2}", cents as f64 / 100.0).separate_with_commas() +} + +/// Whether a recurring rule's next occurrence falls within the next `days` days and will +/// actually fire (not paused, not past its end date) +fn is_upcoming( + next: NaiveDate, + end_date: Option, + is_paused: bool, + today: NaiveDate, + days: u64, +) -> bool { + if is_paused || end_date.is_some_and(|end| next > end) { + return false; + } + + next >= today && next <= today + Days::new(days) +} + +/// Tables inherit their block's border-coloured foreground for unstyled cells, so every row +/// gets the theme's text colour explicitly, like the other pages do. +fn row_style(theme: &Theme) -> Style { + Style::default().bg(theme.background()).fg(theme.text()) +} + +fn sign_style(sign: Sign, theme: &Theme) -> Style { + match sign { + Sign::Positive => Style::default().fg(theme.positive()), + Sign::Negative => Style::default().fg(theme.negative()), + Sign::Neutral => Style::default().fg(theme.text()), + } +} + /// The function draws the Initial page of the interface. -pub fn initial_ui(f: &mut Frame, start_from: usize, theme: &Theme) { +pub fn initial_ui(f: &mut Frame, start_from: usize, data: &InitialData, theme: &Theme) { let size = f.area(); let chunks = Layout::default() .direction(Direction::Vertical) @@ -20,20 +151,20 @@ pub fn initial_ui(f: &mut Frame, start_from: usize, theme: &Theme) { ]) .split(size); - let horizontal_help_chunks = Layout::default() + let columns = Layout::default() .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) + .constraints([Constraint::Percentage(55), Constraint::Percentage(45)]) .split(chunks[2]); - let help_chunk_1 = Layout::default() + let left_column = Layout::default() .direction(Direction::Vertical) .constraints([Constraint::Percentage(40), Constraint::Percentage(60)]) - .split(horizontal_help_chunks[0]); + .split(columns[0]); - let help_chunk_2 = Layout::default() + let right_column = Layout::default() .direction(Direction::Vertical) - .constraints([Constraint::Percentage(40), Constraint::Percentage(60)]) - .split(horizontal_help_chunks[1]); + .constraints([Constraint::Length(11), Constraint::Min(0)]) + .split(columns[1]); f.render_widget(main_block(theme), size); @@ -46,13 +177,14 @@ pub fn initial_ui(f: &mut Frame, start_from: usize, theme: &Theme) { |_| \_\ |______| /_/ \_\" .to_string(); - // To work with this and add a slight touch of animation, we will split the entire - // text by \n. Once it is done, we will loop through each line and take a specific amount of chars from each line. + // To animate it, the text is split by \n and each line reveals `total_to_add` + // chars starting at `start_from`, wrapping around to the beginning of the line + // when the end is reached let split_text = text.split('\n').collect::>(); let mut upper_text = String::new(); for line in split_text { - // If the line is 20 chars and the index is 15, take the chars from 15-20 and 0-3 indexes. + // A line of 20 chars at index 15 takes chars 15-20 and then 0-3 // This var stores how many to take from 0 index let mut to_add_from_start = 0; // amount of chars per line @@ -60,8 +192,8 @@ pub fn initial_ui(f: &mut Frame, start_from: usize, theme: &Theme) { if start_from + total_to_add > line.len() { let extra_index = (start_from + total_to_add) - line.len(); - // Add extra index to take from beginning and remove from the starting point - // if it will go out of bound + // Take that many chars from the start instead, since taking them from + // the starting point would go out of bounds total_to_add -= extra_index; to_add_from_start += extra_index; } @@ -69,15 +201,15 @@ pub fn initial_ui(f: &mut Frame, start_from: usize, theme: &Theme) { // Go through each char of the line for (index, char) in line.chars().enumerate() { if to_add_from_start != 0 { - // Add chars if we have to take anything from index 0 to something index + // Wrapping around, so keep the chars until the count runs out upper_text.push(char); to_add_from_start -= 1; } else if total_to_add != 0 && index >= start_from { - // If we are at the start point, take the char + // Past the start point and the limit isn't reached yet upper_text.push(char); total_to_add -= 1; } else if index != start_from || total_to_add == 0 { - // If the 10 char limit is crossed, only add empty space + // Everything past the 10 char limit stays blank upper_text.push(' '); } } @@ -91,53 +223,22 @@ pub fn initial_ui(f: &mut Frame, start_from: usize, theme: &Theme) { {Z} {Y} {W} +{U} {J} -{Q} -{H}" +{Q}" ); - let unmodified_second_help = "Arrow Up/Down: Cycle between widgets + let unmodified_second_help = "H: Show the keys of the page you are currently on +Arrow Up/Down: Cycle between widgets Arrow Left/Right: Cycle values of a widget -H: Show help of the page the UI is currently on -X: Change sort type on summary/Change date type on Search page +Number keys: Jump to a field on Add Transaction/Search/Recurring pages T: Cycle through themes -Double r (Lowercase): Go to the Chart page and hide top widget. Press again to unhide -Double R (Upeercase): Go to the Chart page and hide chart legends. Press again to unhide -Double Z: Go to the Summary page and hide top widget. Press again to unhide -"; - - let unmodified_third_help = format!( - "Arrow Up/Down: Move between year/month/transaction selections and scroll -Arrow Left/Right: Change values of the year/month selection -D: Delete selected Transaction -J: Take user input for various actions -E: Edit Selected Transaction -{V} -,: Move the transaction upward (Only if on the same date) -.: Move the transaction downward (Only if on the same date)" - ); +X: Change sort type on Summary/Change date type on Search page +Double r/R/Z: Hide chart top widgets/legends or summary top widget"; - let unmodified_fourth_help = - "Arrow Left/Right: Move cursor left/right (If a field is selected) -Arrow Up/Down: Go to the next value of the field (If a field is selected) -1: Edit Date -2: Edit TX details -3: Edit TX Type -4: Edit TX Method -5: Edit Amount -6: Edit Tags -S: Save inputted data as a Transaction -C: Clear all fields and reset -X: Change date type (Only on search page) -Enter: Select the first field if nothing is selected -Enter: Verify, submit a field and continue -Esc: Stop editing a field"; - - // Bold a part of the text before rendering + // create_bolded_text does the bolding while rendering let first_text = create_bolded_text(&unmodified_first_help); let second_text = create_bolded_text(unmodified_second_help); - let third_text = create_bolded_text(&unmodified_third_help); - let fourth_text = create_bolded_text(unmodified_fourth_help); let middle_text = "Press Any Key To Continue"; @@ -169,20 +270,291 @@ Esc: Stop editing a field"; .block(styled_block("Other Keys", theme)) .wrap(Wrap { trim: true }); - let help_3 = Paragraph::new(third_text) - .style(Style::default().bg(theme.background()).fg(theme.text())) - .block(styled_block("Home Page Keys", theme)) - .wrap(Wrap { trim: true }); - - let help_4 = Paragraph::new(fourth_text) - .style(Style::default().bg(theme.background()).fg(theme.text())) - .block(styled_block("Transaction Field Keys", theme)) - .wrap(Wrap { trim: true }); - f.render_widget(paragraph, chunks[0]); f.render_widget(paragraph_2, chunks[1]); - f.render_widget(help_1, help_chunk_1[0]); - f.render_widget(help_3, help_chunk_1[1]); - f.render_widget(help_2, help_chunk_2[0]); - f.render_widget(help_4, help_chunk_2[1]); + render_balances(f, left_column[0], data, theme); + render_recurring(f, left_column[1], data, theme); + f.render_widget(help_1, right_column[0]); + f.render_widget(help_2, right_column[1]); +} + +fn render_balances(f: &mut Frame, area: ratatui::layout::Rect, data: &InitialData, theme: &Theme) { + let block = styled_block("Balances", theme); + + if data.balances.is_empty() { + let empty = Paragraph::new("No transaction methods yet. Press J to add one.") + .style(Style::default().bg(theme.background()).fg(theme.text())) + .block(block) + .wrap(Wrap { trim: true }); + f.render_widget(empty, area); + return; + } + + let balance_cell = |value: &str, negative: bool, bold: bool| { + let color = if negative { + theme.negative() + } else { + theme.text() + }; + let mut style = Style::default().fg(color); + + if bold { + style = style.add_modifier(Modifier::BOLD); + } + + Cell::from(Line::from(Span::styled(value.to_string(), style)).right_aligned()) + }; + + let mut rows: Vec = data + .balances + .iter() + .map(|(name, balance, negative)| { + Row::new([ + Cell::from(name.clone()), + balance_cell(balance, *negative, false), + ]) + .style(row_style(theme)) + }) + .collect(); + + if let Some((total, negative)) = &data.total { + rows.push( + Row::new([ + Cell::from(Span::styled( + "Total", + Style::default().add_modifier(Modifier::BOLD), + )), + balance_cell(total, *negative, true), + ]) + .style(row_style(theme)), + ); + } + + let table = Table::new( + rows, + [Constraint::Percentage(50), Constraint::Percentage(50)], + ) + .style(Style::default().bg(theme.background()).fg(theme.text())) + .block(block); + + f.render_widget(table, area); +} + +fn render_recurring(f: &mut Frame, area: ratatui::layout::Rect, data: &InitialData, theme: &Theme) { + let title = format!("Recurring: next {UPCOMING_DAYS} days"); + let block = styled_block(&title, theme); + let inner = block.inner(area); + f.render_widget(block, area); + + let sections = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(2), Constraint::Min(0)]) + .split(inner); + + let added_text = match data.recurring_added { + 0 => "No recurring transactions were due on startup".to_string(), + 1 => "1 recurring transaction was added on startup".to_string(), + n => format!("{n} recurring transactions were added on startup"), + }; + + let added_style = if data.recurring_added > 0 { + Style::default() + .fg(theme.positive()) + .add_modifier(Modifier::BOLD) + } else { + Style::default().fg(theme.text()) + }; + + let mut status_lines = vec![Line::from(Span::styled(added_text, added_style))]; + + if data.paused_count > 0 { + status_lines.push(Line::from(format!("{} paused", data.paused_count))); + } + + f.render_widget( + Paragraph::new(status_lines) + .style(Style::default().bg(theme.background()).fg(theme.text())), + sections[0], + ); + + if data.upcoming.is_empty() { + f.render_widget( + Paragraph::new("Nothing coming up. Press U to manage recurring transactions.") + .style(Style::default().bg(theme.background()).fg(theme.text())) + .wrap(Wrap { trim: true }), + sections[1], + ); + return; + } + + let rows = data.upcoming.iter().map(|row| { + Row::new([ + Cell::from(row.date.clone()), + Cell::from(row.details.clone()), + Cell::from( + Line::from(Span::styled( + row.amount.clone(), + sign_style(row.sign, theme), + )) + .right_aligned(), + ), + ]) + .style(row_style(theme)) + }); + + let table = Table::new( + rows, + [ + Constraint::Length(11), + Constraint::Min(10), + Constraint::Length(12), + ], + ) + .style(Style::default().bg(theme.background()).fg(theme.text())); + + f.render_widget(table, sections[1]); +} + +#[cfg(test)] +mod tests { + use super::*; + + fn date(y: i32, m: u32, d: u32) -> NaiveDate { + NaiveDate::from_ymd_opt(y, m, d).unwrap() + } + + #[test] + fn upcoming_includes_today_and_the_last_day_of_the_window() { + let today = date(2026, 9, 29); + assert!(is_upcoming(today, None, false, today, 7)); + assert!(is_upcoming(date(2026, 10, 6), None, false, today, 7)); + } + + #[test] + fn upcoming_excludes_outside_the_window() { + let today = date(2026, 9, 29); + assert!(!is_upcoming(date(2026, 10, 7), None, false, today, 7)); + assert!(!is_upcoming(date(2026, 9, 28), None, false, today, 7)); + } + + #[test] + fn upcoming_excludes_paused_rules() { + let today = date(2026, 9, 29); + assert!(!is_upcoming(date(2026, 9, 30), None, true, today, 7)); + } + + #[test] + fn upcoming_excludes_rules_past_their_end_date() { + let today = date(2026, 9, 29); + let next = date(2026, 10, 2); + assert!(!is_upcoming(next, Some(date(2026, 10, 1)), false, today, 7)); + assert!(is_upcoming(next, Some(date(2026, 10, 2)), false, today, 7)); + } + + #[test] + fn format_cents_adds_separators_and_keeps_sign() { + assert_eq!(format_cents(123_456_789), "1,234,567.89"); + assert_eq!(format_cents(-250), "-2.50"); + assert_eq!(format_cents(0), "0.00"); + } + + fn sample_data() -> InitialData { + InitialData { + recurring_added: 3, + balances: vec![ + ("Cash".to_string(), "1,200.50".to_string(), false), + ("Bank".to_string(), "-40.00".to_string(), true), + ], + total: Some(("1,160.50".to_string(), false)), + upcoming: vec![UpcomingRow { + date: "Wed Sep 30".to_string(), + details: "Rent".to_string(), + amount: "800.00".to_string(), + sign: Sign::Negative, + }], + paused_count: 1, + } + } + + fn render(width: u16, height: u16, data: &InitialData) -> String { + use ratatui::Terminal; + use ratatui::backend::TestBackend; + + let mut terminal = Terminal::new(TestBackend::new(width, height)).unwrap(); + let theme = Theme::new_index(0); + terminal.draw(|f| initial_ui(f, 5, data, &theme)).unwrap(); + + let buffer = terminal.backend().buffer().clone(); + (0..height) + .map(|y| { + (0..width) + .map(|x| buffer[(x, y)].symbol().to_string()) + .collect::() + }) + .collect::>() + .join("\n") + } + + #[test] + fn renders_balances_recurring_and_keys() { + let screen = render(120, 40, &sample_data()); + + assert!(screen.contains("Balances")); + assert!(screen.contains("1,200.50")); + assert!(screen.contains("Total")); + assert!(screen.contains("3 recurring transactions were added on startup")); + assert!(screen.contains("1 paused")); + assert!(screen.contains("Rent")); + assert!(screen.contains("U: Recurring Transactions Page")); + } + + #[test] + fn renders_empty_states() { + let data = InitialData { + recurring_added: 0, + balances: Vec::new(), + total: None, + upcoming: Vec::new(), + paused_count: 0, + }; + let screen = render(120, 40, &data); + + assert!(screen.contains("No transaction methods yet")); + assert!(screen.contains("No recurring transactions were due on startup")); + assert!(screen.contains("Nothing coming up")); + } + + #[test] + fn does_not_panic_on_tiny_terminals() { + for (w, h) in [(1, 1), (20, 5), (40, 12), (80, 24)] { + render(w, h, &sample_data()); + } + } + + #[test] + fn table_text_uses_the_text_colour_not_the_border_colour() { + use ratatui::Terminal; + use ratatui::backend::TestBackend; + + for idx in 0..12 { + let theme = Theme::new_index(idx); + let mut terminal = Terminal::new(TestBackend::new(120, 36)).unwrap(); + let data = sample_data(); + terminal.draw(|f| initial_ui(f, 5, &data, &theme)).unwrap(); + let buffer = terminal.backend().buffer().clone(); + + for word in ["Cash", "Total", "Rent", "Wed Sep 30"] { + let (x, y) = (0..36u16) + .find_map(|y| { + let row: String = (0..120u16) + .map(|x| buffer[(x, y)].symbol().to_string()) + .collect(); + row.find(word).map(|x| (x as u16, y)) + }) + .unwrap(); + + assert_eq!(buffer[(x, y)].fg, theme.text(), "{word} in theme {idx}"); + } + } + } } diff --git a/tui/src/pages/mod.rs b/tui/src/pages/mod.rs index ffbb2281..bbd464ca 100644 --- a/tui/src/pages/mod.rs +++ b/tui/src/pages/mod.rs @@ -4,6 +4,7 @@ mod chart_ui; mod home_ui; mod initial_ui; mod popups; +mod recurring_ui; mod search_ui; mod summary_ui; @@ -13,5 +14,6 @@ pub use chart_ui::*; pub use home_ui::*; pub use initial_ui::*; pub use popups::*; +pub use recurring_ui::*; pub use search_ui::*; pub use summary_ui::*; diff --git a/tui/src/pages/popups/help_text.rs b/tui/src/pages/popups/help_text.rs index 3c402f6f..4eaad1c6 100644 --- a/tui/src/pages/popups/help_text.rs +++ b/tui/src/pages/popups/help_text.rs @@ -4,6 +4,7 @@ pub const R: &str = "R: Chart Page"; pub const Z: &str = "Z: Summary Page"; pub const Y: &str = "Y: Activity Page"; pub const W: &str = "W: Search Page"; +pub const U: &str = "U: Recurring Transactions Page"; pub const Q: &str = "Q: Quit"; pub const H: &str = "H: Show help"; pub const V: &str = "V: Show selected transaction details"; @@ -57,6 +58,7 @@ Example amount: 100 + b, b + b, 5 * b, 1.2k + 1m {Z} {Y} {W} +{U} {H} {J} {Q} @@ -83,6 +85,7 @@ Arrow Left/Right: Move value of the widget {Z} {Y} {W} +{U} {H} {J} {Q} @@ -109,6 +112,7 @@ Arrow Left/Right: Move value of the widget {R} {Y} {W} +{U} {H} {J} {Q} @@ -138,6 +142,7 @@ Swapping transaction location will only work if they are on the same date. {Z} {Y} {W} +{U} {H} {J} {Q} @@ -193,6 +198,7 @@ Example amount : <1000, >=10000 {R} {Z} {Y} +{U} {H} {J} {Q} @@ -218,6 +224,58 @@ Arrow Left/Right: Move value of the widget {R} {Z} {W} +{U} +{H} +{J} +{Q} +" + ) +} + +pub fn recurring_help_text() -> String { + format!( + "This page is for managing recurring transactions. Rules created here are \ +automatically turned into real transactions as they come due, each time the app starts. \ +Number keys go 1-9 then 0 for a 10th field, in some combination Enter from the previous field is the \ +only way to reach them. + +1: Start Date Example: 2022-05-12, YYYY-MM-DD +2: TX details Example: For Grocery, Salary +3: TX Type Example: Income/Expense/Transfer/I/E/T +3: New TX Type Example: Borrow/Borrow Repay/Lend/Lend Repay/b/br/l/lr +4: TX Method Example: Cash, Bank, Card +5: Amount Example: 1000, 100+50, b - 100 +6: Frequency Example: Daily/Weekly/Monthly/Yearly/d/w/m/y +7: Every N Example: with Monthly, 1 = every month, 2 = every 2 months, 3 = every 3 months +8: Day of Week/Month Example: Monday (Weekly), 15 (Monthly/Yearly) +9: Month Example: January (Yearly only) +End Date Optional, comes right after the fields above. Leave empty for no end +Tags Example: Food, Car. Add a Comma for a new tag. Comes right after End Date + +S: Save the inputted data as a new recurring rule, or update the one being edited +E: Edit the selected recurring rule from the table below +D: Delete the selected recurring rule from the table below +P: Pause/Unpause the selected recurring rule +Enter: Submit field and continue. Also selects the first field if nothing is selected +Esc: Stop editing field +Tab: Accept Autocompletion. Pressing again will remove the autocompleted value + +Arrow Up/Down: Steps value up/down by 1. With no field selected, moves the table selection +Arrow Left/Right: Move cursor on input fields + +C: Clear all fields/Reset all changes, including discarding an edit if one was in progress +b: On amount field 'b' gets replaced with the current balance of Tx Method field +k: On amount field 'k' is considered as 1000 or a thousand +m: On amount field 'm' is considered as 1,000,000 or a million +Calculation: Amount field supports simple calculation with +, -, *, / + +{F} +{T} +{A} +{R} +{Z} +{W} +{Y} {H} {J} {Q} diff --git a/tui/src/pages/popups/info.rs b/tui/src/pages/popups/info.rs index 3a763167..4f57c8a6 100644 --- a/tui/src/pages/popups/info.rs +++ b/tui/src/pages/popups/info.rs @@ -8,7 +8,8 @@ use ratatui::widgets::{ use crate::pages::{ InfoPopup, InfoPopupState, activity_help_text, add_tx_help_text, chart_help_text, choice_help, - home_help_text, new_update_text, reposition_help, search_help_text, summary_help_text, + home_help_text, new_update_text, recurring_help_text, reposition_help, search_help_text, + summary_help_text, }; use crate::theme::Theme; use crate::utility::{centered_rect_exact, create_bolded_text, main_block}; @@ -45,6 +46,9 @@ impl InfoPopup { InfoPopupState::ActivityHelp => { message = activity_help_text(); } + InfoPopupState::RecurringHelp => { + message = recurring_help_text(); + } InfoPopupState::Error(err) => { title = "Error"; message = err.clone(); diff --git a/tui/src/pages/popups/models.rs b/tui/src/pages/popups/models.rs index 8a60f489..5fb813b3 100644 --- a/tui/src/pages/popups/models.rs +++ b/tui/src/pages/popups/models.rs @@ -30,6 +30,7 @@ pub enum InfoPopupState { SummaryHelp, SearchHelp, ActivityHelp, + RecurringHelp, ChoiceHelp, RepositionHelp, Error(String), diff --git a/tui/src/pages/recurring_ui.rs b/tui/src/pages/recurring_ui.rs new file mode 100644 index 00000000..24e81453 --- /dev/null +++ b/tui/src/pages/recurring_ui.rs @@ -0,0 +1,525 @@ +use ratatui::Frame; +use ratatui::layout::{Alignment, Constraint, Direction, Layout, Position}; +use ratatui::style::{Modifier, Style}; +use ratatui::text::{Line, Span}; +use ratatui::widgets::{Cell, Paragraph, Row, Table}; +use rex_app::conn::RecurrenceFrequency; +use thousands::Separable; + +use crate::outputs::TxType; +use crate::page_handler::{LogType, TableData, TxTab}; +use crate::theme::Theme; +use crate::tx_handler::TxData; +use crate::utility::{LerpState, main_block, styled_block}; + +pub const RECURRING_TABLE_ID: &str = "recurring_table_row"; + +pub fn recurring_ui( + f: &mut Frame, + recurring_data: &TxData, + recurring_tab: &TxTab, + recurring_table: &mut TableData, + lerp_state: &mut LerpState, + theme: &Theme, +) { + let status_data = recurring_data.get_tx_status(); + // date, details, from method, to method, amount, tx type, tags, autofill + let input_data = recurring_data.get_all_texts(); + // frequency, recur interval, recur value, recur month, end date + let recur_data = recurring_data.get_recurring_texts(); + // The index of the cursor position + let current_index = recurring_data.get_current_index(); + + let tx_type = recurring_data.get_tx_type(); + let frequency = recurring_data.get_frequency(); + + let size = f.area(); + + let from_method_name = match tx_type { + TxType::IncomeExpense => "TX Method", + TxType::Transfer => "From Method", + }; + + let tx_count = recurring_table.items.len(); + let lerp_id = "recurring_count"; + let lerp_tx_count = lerp_state.lerp(lerp_id, tx_count as f64, None) as i64; + + let lerp_row = lerp_state.lerp(RECURRING_TABLE_ID, tx_count as f64, Some(0.50)) as usize; + + let table_name = format!("Recurring Transactions: {lerp_tx_count}"); + + let header_cells = [ + "Next Date", + "Frequency", + "Details", + "Method", + "Amount", + "Type", + "Tags", + "Paused", + ] + .iter() + .map(|h| Cell::from(*h).style(Style::default().fg(theme.background()))); + + let header = Row::new(header_cells) + .style(Style::default().bg(theme.header())) + .height(1) + .bottom_margin(0); + + let rows = recurring_table.items.iter().take(lerp_row).map(|item| { + let height = 1; + let cells = item.iter().enumerate().map(|(index, c)| { + // Index 4 is the Amount column - the only one that should get thousands + // separators + if index == 4 { + Cell::from(c.separate_with_commas()) + } else { + Cell::from(c.clone()) + } + }); + Row::new(cells) + .height(height as u16) + .bottom_margin(0) + .style(Style::default().bg(theme.background()).fg(theme.text())) + }); + + let mut table_area = Table::new( + rows, + [ + Constraint::Percentage(12), + Constraint::Percentage(13), + Constraint::Percentage(25), + Constraint::Percentage(13), + Constraint::Percentage(11), + Constraint::Percentage(8), + Constraint::Percentage(11), + Constraint::Percentage(7), + ], + ) + .header(header) + .block(styled_block(&table_name, theme)); + + // Divide the terminal into 5 parts vertically + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(2) + .constraints([ + // core tx fields + Constraint::Length(3), + // recurrence schedule fields + Constraint::Length(3), + // details input chunk + Constraint::Length(3), + // status chunk + Constraint::Length(10), + // recurring rules list chunk + Constraint::Min(0), + ]) + .split(size); + + // Same field set/order as the Add Transaction page, plus Tags + let core_chunk = match tx_type { + TxType::IncomeExpense => Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Percentage(20), + Constraint::Percentage(20), + Constraint::Percentage(20), + Constraint::Percentage(20), + Constraint::Percentage(20), + ]) + .split(chunks[0]), + TxType::Transfer => Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Percentage(16), + Constraint::Percentage(16), + Constraint::Percentage(16), + Constraint::Percentage(16), + Constraint::Percentage(16), + Constraint::Percentage(20), + ]) + .split(chunks[0]), + }; + + // Number of recurrence boxes to render: frequency and interval are always + // shown, then the frequency decides whether a value, a month and the end date + // follow + let recur_box_count = match frequency { + RecurrenceFrequency::Daily => 3, + RecurrenceFrequency::Weekly | RecurrenceFrequency::Monthly => 4, + RecurrenceFrequency::Yearly => 5, + }; + + let recur_percent = 100 / recur_box_count; + let recur_chunk = Layout::default() + .direction(Direction::Horizontal) + .constraints(vec![ + Constraint::Percentage(recur_percent); + recur_box_count as usize + ]) + .split(chunks[1]); + + f.render_widget(main_block(theme), size); + + let mut status_text = vec![]; + + for i in status_data.iter().rev() { + let (initial, rest) = i.text.split_once(':').unwrap(); + + match i.log_type { + LogType::Info => { + status_text.push(Line::from(vec![ + Span::styled( + initial, + Style::default() + .fg(theme.positive()) + .add_modifier(Modifier::BOLD), + ), + Span::styled(format!(":{rest}"), Style::default().fg(theme.positive())), + ])); + } + LogType::Error => { + status_text.push(Line::from(vec![ + Span::styled( + initial, + Style::default() + .fg(theme.negative()) + .add_modifier(Modifier::BOLD), + ), + Span::styled(format!(":{rest}"), Style::default().fg(theme.negative())), + ])); + } + } + } + + let date_text = Line::from(format!("{} ", input_data[0])); + let mut details_text = Line::from(format!("{} ", input_data[1])); + let mut from_method_text = Line::from(format!("{} ", input_data[2])); + let mut to_method_text = Line::from(format!("{} ", input_data[3])); + let amount_text = Line::from(format!("{} ", input_data[4])); + let mut tx_type_text = Line::from(format!("{} ", input_data[5])); + let mut tags_text = Line::from(format!("{} ", input_data[6])); + + let mut frequency_text = Line::from(format!("{} ", recur_data[0])); + let recur_interval_text = Line::from(format!("{} ", recur_data[1])); + let mut recur_value_text = Line::from(format!("{} ", recur_data[2])); + let mut recur_month_text = Line::from(format!("{} ", recur_data[3])); + let end_date_text = Line::from(format!("{} ", recur_data[4])); + + match recurring_tab { + TxTab::Details => { + details_text = Line::from(vec![ + Span::from(format!("{} ", input_data[1])), + Span::styled(input_data[7], Style::default().fg(theme.autocomplete())), + ]); + } + TxTab::FromMethod => { + from_method_text = Line::from(vec![ + Span::from(format!("{} ", input_data[2])), + Span::styled(input_data[7], Style::default().fg(theme.autocomplete())), + ]); + } + TxTab::ToMethod => { + to_method_text = Line::from(vec![ + Span::from(format!("{} ", input_data[3])), + Span::styled(input_data[7], Style::default().fg(theme.autocomplete())), + ]); + } + TxTab::Tags => { + tags_text = Line::from(vec![ + Span::from(format!("{} ", input_data[6])), + Span::styled(input_data[7], Style::default().fg(theme.autocomplete())), + ]); + } + TxTab::Frequency => { + frequency_text = Line::from(vec![ + Span::from(format!("{} ", recur_data[0])), + Span::styled(input_data[7], Style::default().fg(theme.autocomplete())), + ]); + } + TxTab::RecurValue => { + recur_value_text = Line::from(vec![ + Span::from(format!("{} ", recur_data[2])), + Span::styled(input_data[7], Style::default().fg(theme.autocomplete())), + ]); + } + TxTab::RecurMonth => { + recur_month_text = Line::from(vec![ + Span::from(format!("{} ", recur_data[3])), + Span::styled(input_data[7], Style::default().fg(theme.autocomplete())), + ]); + } + TxTab::TxType => { + tx_type_text = Line::from(vec![ + Span::from(format!("{} ", input_data[5])), + Span::styled(input_data[7], Style::default().fg(theme.autocomplete())), + ]); + } + _ => {} + } + + let status_sec = Paragraph::new(status_text) + .style(Style::default().bg(theme.background()).fg(theme.text())) + .block(styled_block("Status", theme)) + .alignment(Alignment::Left); + + let date_sec = Paragraph::new(date_text) + .style(Style::default().bg(theme.background()).fg(theme.text())) + .block(styled_block("Start Date", theme)) + .alignment(Alignment::Left); + + let from_method_sec = Paragraph::new(from_method_text) + .style(Style::default().bg(theme.background()).fg(theme.text())) + .block(styled_block(from_method_name, theme)) + .alignment(Alignment::Left); + + let to_method_sec = Paragraph::new(to_method_text) + .style(Style::default().bg(theme.background()).fg(theme.text())) + .block(styled_block("To Method", theme)) + .alignment(Alignment::Left); + + let amount_sec = Paragraph::new(amount_text) + .style(Style::default().bg(theme.background()).fg(theme.text())) + .block(styled_block("Amount", theme)) + .alignment(Alignment::Left); + + let tx_type_sec = Paragraph::new(tx_type_text) + .style(Style::default().bg(theme.background()).fg(theme.text())) + .block(styled_block("TX Type", theme)) + .alignment(Alignment::Left); + + let details_sec = Paragraph::new(details_text) + .style(Style::default().bg(theme.background()).fg(theme.text())) + .block(styled_block("Details", theme)) + .alignment(Alignment::Left); + + let tags_sec = Paragraph::new(tags_text) + .style(Style::default().bg(theme.background()).fg(theme.text())) + .block(styled_block("Tags", theme)) + .alignment(Alignment::Left); + + let frequency_sec = Paragraph::new(frequency_text) + .style(Style::default().bg(theme.background()).fg(theme.text())) + .block(styled_block("Frequency", theme)) + .alignment(Alignment::Left); + + let recur_interval_title = match frequency { + RecurrenceFrequency::Daily => "Every N Days", + RecurrenceFrequency::Weekly => "Every N Weeks", + RecurrenceFrequency::Monthly => "Every N Months", + RecurrenceFrequency::Yearly => "Every N Years", + }; + + let recur_interval_sec = Paragraph::new(recur_interval_text) + .style(Style::default().bg(theme.background()).fg(theme.text())) + .block(styled_block(recur_interval_title, theme)) + .alignment(Alignment::Left); + + let recur_value_title = match frequency { + RecurrenceFrequency::Weekly => "Day of Week", + _ => "Day of Month", + }; + + let recur_value_sec = Paragraph::new(recur_value_text) + .style(Style::default().bg(theme.background()).fg(theme.text())) + .block(styled_block(recur_value_title, theme)) + .alignment(Alignment::Left); + + let recur_month_sec = Paragraph::new(recur_month_text) + .style(Style::default().bg(theme.background()).fg(theme.text())) + .block(styled_block("Month", theme)) + .alignment(Alignment::Left); + + let end_date_sec = Paragraph::new(end_date_text) + .style(Style::default().bg(theme.background()).fg(theme.text())) + .block(styled_block("End Date (Optional)", theme)) + .alignment(Alignment::Left); + + match recurring_tab { + TxTab::Date => f.set_cursor_position(Position { + x: core_chunk[0].x + current_index as u16 + 1, + y: core_chunk[0].y + 1, + }), + TxTab::Details => f.set_cursor_position(Position { + x: chunks[2].x + current_index as u16 + 1, + y: chunks[2].y + 1, + }), + TxTab::TxType => f.set_cursor_position(Position { + x: core_chunk[1].x + current_index as u16 + 1, + y: core_chunk[1].y + 1, + }), + TxTab::FromMethod => f.set_cursor_position(Position { + x: core_chunk[2].x + current_index as u16 + 1, + y: core_chunk[2].y + 1, + }), + _ => {} + } + + match tx_type { + TxType::IncomeExpense => match recurring_tab { + TxTab::Amount => f.set_cursor_position(Position { + x: core_chunk[3].x + current_index as u16 + 1, + y: core_chunk[3].y + 1, + }), + TxTab::Tags => f.set_cursor_position(Position { + x: core_chunk[4].x + current_index as u16 + 1, + y: core_chunk[4].y + 1, + }), + _ => {} + }, + TxType::Transfer => match recurring_tab { + TxTab::ToMethod => f.set_cursor_position(Position { + x: core_chunk[3].x + current_index as u16 + 1, + y: core_chunk[3].y + 1, + }), + TxTab::Amount => f.set_cursor_position(Position { + x: core_chunk[4].x + current_index as u16 + 1, + y: core_chunk[4].y + 1, + }), + TxTab::Tags => f.set_cursor_position(Position { + x: core_chunk[5].x + current_index as u16 + 1, + y: core_chunk[5].y + 1, + }), + _ => {} + }, + } + + match recurring_tab { + TxTab::Frequency => f.set_cursor_position(Position { + x: recur_chunk[0].x + current_index as u16 + 1, + y: recur_chunk[0].y + 1, + }), + TxTab::RecurInterval => f.set_cursor_position(Position { + x: recur_chunk[1].x + current_index as u16 + 1, + y: recur_chunk[1].y + 1, + }), + TxTab::RecurValue => f.set_cursor_position(Position { + x: recur_chunk[2].x + current_index as u16 + 1, + y: recur_chunk[2].y + 1, + }), + TxTab::RecurMonth if recur_box_count == 5 => f.set_cursor_position(Position { + x: recur_chunk[3].x + current_index as u16 + 1, + y: recur_chunk[3].y + 1, + }), + TxTab::EndDate => { + let end_date_index = (recur_box_count - 1) as usize; + f.set_cursor_position(Position { + x: recur_chunk[end_date_index].x + current_index as u16 + 1, + y: recur_chunk[end_date_index].y + 1, + }); + } + _ => {} + } + + f.render_widget(details_sec, chunks[2]); + f.render_widget(status_sec, chunks[3]); + f.render_widget(date_sec, core_chunk[0]); + f.render_widget(tx_type_sec, core_chunk[1]); + f.render_widget(from_method_sec, core_chunk[2]); + + match tx_type { + TxType::IncomeExpense => { + f.render_widget(amount_sec, core_chunk[3]); + f.render_widget(tags_sec, core_chunk[4]); + } + TxType::Transfer => { + f.render_widget(to_method_sec, core_chunk[3]); + f.render_widget(amount_sec, core_chunk[4]); + f.render_widget(tags_sec, core_chunk[5]); + } + } + + f.render_widget(frequency_sec, recur_chunk[0]); + f.render_widget(recur_interval_sec, recur_chunk[1]); + + match frequency { + RecurrenceFrequency::Daily => { + f.render_widget(end_date_sec, recur_chunk[2]); + } + RecurrenceFrequency::Weekly | RecurrenceFrequency::Monthly => { + f.render_widget(recur_value_sec, recur_chunk[2]); + f.render_widget(end_date_sec, recur_chunk[3]); + } + RecurrenceFrequency::Yearly => { + f.render_widget(recur_value_sec, recur_chunk[2]); + f.render_widget(recur_month_sec, recur_chunk[3]); + f.render_widget(end_date_sec, recur_chunk[4]); + } + } + + if recurring_table.state.selected().is_some() { + table_area = table_area.highlight_symbol(">> "); + + let add_modifier = theme.add_reverse_modifier(); + + let mut style = Style::default(); + + if add_modifier { + style = style.fg(theme.selected()).add_modifier(Modifier::REVERSED); + } else { + style = style.bg(theme.selected()); + } + + table_area = table_area.row_highlight_style(style); + } + + if let Some(index) = recurring_table.state.selected() + && index > 10 + { + *recurring_table.state.offset_mut() = index - 10; + } + + f.render_stateful_widget(table_area, chunks[4], &mut recurring_table.state); +} + +#[cfg(test)] +mod tests { + use super::*; + use ratatui::Terminal; + use ratatui::backend::TestBackend; + + fn render(data: &TxData, tab: &TxTab) -> String { + let (width, height) = (140, 40); + let mut terminal = Terminal::new(TestBackend::new(width, height)).unwrap(); + let theme = Theme::new_index(0); + let mut table = TableData::new(Vec::new()); + let mut lerp = LerpState::new(1.0); + + terminal + .draw(|f| recurring_ui(f, data, tab, &mut table, &mut lerp, &theme)) + .unwrap(); + + let buffer = terminal.backend().buffer().clone(); + (0..height) + .map(|y| { + (0..width) + .map(|x| buffer[(x, y)].symbol().to_string()) + .collect::() + }) + .collect::>() + .join("\n") + } + + #[test] + fn suggestion_shows_next_to_the_selected_recurrence_field_only() { + let mut data = TxData::new_empty(); + // Yearly is the only frequency that shows all three boxes at once + data.frequency = "y".to_string(); + data.recur_value = "mon".to_string(); + data.recur_month = "sep".to_string(); + data.autofill = "SUGGESTION".to_string(); + + for tab in [TxTab::Frequency, TxTab::RecurValue, TxTab::RecurMonth] { + let screen = render(&data, &tab); + assert_eq!( + screen.matches("SUGGESTION").count(), + 1, + "exactly one box should show the suggestion for {tab:?}" + ); + } + + let screen = render(&data, &TxTab::Nothing); + assert!(!screen.contains("SUGGESTION")); + } +} diff --git a/tui/src/pages/search_ui.rs b/tui/src/pages/search_ui.rs index 2f56c839..f5f578f5 100644 --- a/tui/src/pages/search_ui.rs +++ b/tui/src/pages/search_ui.rs @@ -113,8 +113,8 @@ pub fn search_ui( ]) .split(size); - // Based on the tx type divide the first chunk into 5 or 6 parts horizontally - // this chunk contains the input boxes take takes input + // Depending on the tx type, split the first chunk into 5 or 6 columns. + // This chunk holds the input boxes that take the user input let input_chunk = { match tx_type { TxType::IncomeExpense => Layout::default() @@ -160,8 +160,8 @@ pub fn search_ui( let mut status_text = vec![]; - // Iter through the data in reverse mode because we want the latest status text - // to be at the top which is the final value of the vector. + // Iterate in reverse so the latest status line, which is the last value of the + // vector, ends up at the top for i in status_data.iter().rev() { let (initial, rest) = i.text.split_once(':').unwrap(); @@ -191,7 +191,8 @@ pub fn search_ui( } } - // We already fetched the data for each of these. Assign them now and then use them to load the widget + // The values were already fetched above, so they just need to be assigned + // and loaded into their widgets let date_text = Line::from(format!("{} ", input_data[0])); let mut details_text = Line::from(format!("{} ", input_data[1])); @@ -281,8 +282,8 @@ pub fn search_ui( .block(styled_block("Tags", theme)) .alignment(Alignment::Left); - // We will be adding a cursor based on which tab is selected + the selected index. - // This was created utilizing the tui-rs example named user_input.rs + // Put the cursor at the index of the currently selected field. + // Adapted from the tui-rs example named user_input.rs match search_tab { TxTab::Date => f.set_cursor_position(Position { x: input_chunk[0].x + current_index as u16 + 1, diff --git a/tui/src/pages/summary_ui.rs b/tui/src/pages/summary_ui.rs index 7ff61356..fe352e12 100644 --- a/tui/src/pages/summary_ui.rs +++ b/tui/src/pages/summary_ui.rs @@ -190,8 +190,8 @@ pub fn summary_ui( let chunks = main_layout.split(size); - // Horizontal split for the peak + largest area. Split the main block into several vertical - // chunk => Take a specific vertical take and split it into two Horizontal chunk once again + // The peak + largest area sits in a horizontal split: take the matching + // vertical chunk of the main layout and split that one horizontally again let summary_chunk = if summary_hidden_mode { summary_layout.split(chunks[3]) } else { @@ -340,7 +340,7 @@ pub fn summary_ui( let new_c = lerp_state.lerp(&lerp_id, parsed_num, None); let text = if index == 1 && row_index == 2 { - // Month checked value. No need float for this + // Month count, so whole numbers are enough here let new_c = new_c as i64; format!("{new_c}").separate_with_commas() } else { @@ -581,8 +581,8 @@ pub fn summary_ui( .style(Style::default().fg(theme.border())); match current_page { - // Previously added a black block to year and month widget if a value is not selected - // Now we will turn that black block into green if a value is selected + // A black block was added to the year and month widgets when no value is + // selected, so turn it green for the selected one SummaryTab::Months => month_tab = month_tab.highlight_style(tab_highlight_style(theme)), SummaryTab::Years => year_tab = year_tab.highlight_style(tab_highlight_style(theme)), SummaryTab::ModeSelection => { diff --git a/tui/src/tx_handler/tx_data.rs b/tui/src/tx_handler/tx_data.rs index 9d8cf834..610f347a 100644 --- a/tui/src/tx_handler/tx_data.rs +++ b/tui/src/tx_handler/tx_data.rs @@ -1,8 +1,9 @@ use anyhow::Result as AResult; use chrono::prelude::Local; -use rex_app::conn::DbConn; -use rex_app::modifier::{parse_search_fields, parse_tx_fields}; +use rex_app::conn::{DbConn, FullRecurringTx, RecurrenceFrequency}; +use rex_app::modifier::{parse_recurring_tx_fields, parse_search_fields, parse_tx_fields}; use rex_app::ui_helper::{DateType, Output, StepType, SteppingError, VerifierError}; +use rex_app::utils::{num_to_month_name, num_to_weekday_name}; use rex_app::views::{FullTx, PartialTx, SearchView, TxViewGroup}; use rex_shared::models::Cent; use std::cmp::Ordering; @@ -11,7 +12,8 @@ use crate::outputs::{CheckingError, ComparisonType, TxType}; use crate::page_handler::{LogData, LogType, TxTab}; use crate::utility::{add_char_to, check_comparison}; -/// Contains all data for a Transaction to work +/// Every field of the Add TX, Search and Recurring page forms, plus the cursor +/// index, the status log shown under the inputs and the editing state #[derive(Default)] pub struct TxData { pub date: String, @@ -21,6 +23,11 @@ pub struct TxData { pub amount: String, pub tx_type: String, pub tags: String, + pub frequency: String, + pub recur_interval: String, + pub recur_value: String, + pub recur_month: String, + pub end_date: String, pub tx_status: Vec, pub editing_tx: bool, pub id_num: i32, @@ -30,8 +37,8 @@ pub struct TxData { } impl TxData { - /// Creates an instance of the struct however the date field is - /// edited with the current local date of the device. + /// All fields start out empty, except the date which is prefilled with the + /// device's current local date #[must_use] pub fn new() -> Self { let current_date = Local::now().to_string(); @@ -44,6 +51,11 @@ impl TxData { amount: String::new(), tx_type: String::new(), tags: String::new(), + frequency: String::new(), + recur_interval: String::new(), + recur_value: String::new(), + recur_month: String::new(), + end_date: String::new(), tx_status: Vec::new(), editing_tx: false, id_num: 0, @@ -63,6 +75,11 @@ impl TxData { amount: String::new(), tx_type: String::new(), tags: String::new(), + frequency: String::new(), + recur_interval: String::new(), + recur_value: String::new(), + recur_month: String::new(), + end_date: String::new(), tx_status: Vec::new(), editing_tx: false, id_num: 0, @@ -88,6 +105,11 @@ impl TxData { .map(|t| t.name.as_str()) .collect::>() .join(", "), + frequency: String::new(), + recur_interval: String::new(), + recur_value: String::new(), + recur_month: String::new(), + end_date: String::new(), tx_status: Vec::new(), editing_tx: edit, id_num: tx.id, @@ -97,8 +119,60 @@ impl TxData { } } - /// Used to adding custom pre-defined data inside the widgets of Add Transaction Page. - /// Currently used on Editing transaction. + /// Populates the Recurring page's form from an existing recurring rule, for editing. + #[must_use] + pub fn from_full_recurring_tx(tx: &FullRecurringTx, edit: bool) -> Self { + let recur_value = match tx.frequency { + RecurrenceFrequency::Daily => String::new(), + RecurrenceFrequency::Weekly => tx + .recur_value + .and_then(|v| num_to_weekday_name(v).ok()) + .unwrap_or_default() + .to_string(), + RecurrenceFrequency::Monthly | RecurrenceFrequency::Yearly => { + tx.recur_value.map(|v| v.to_string()).unwrap_or_default() + } + }; + + let recur_month = tx + .recur_month + .and_then(|m| num_to_month_name(m as u32).ok()) + .unwrap_or_default() + .to_string(); + + Self { + date: tx.next_recurring_date.format("%Y-%m-%d").to_string(), + details: tx.details.clone().unwrap_or_default(), + from_method: tx.from_method.name.clone(), + to_method: tx.to_method.clone().map(|t| t.name).unwrap_or_default(), + amount: format!("{:.2}", tx.amount.dollar()), + tx_type: tx.tx_type.to_string(), + tags: tx + .tags + .iter() + .map(|t| t.name.as_str()) + .collect::>() + .join(", "), + frequency: tx.frequency.to_string(), + recur_interval: tx.recur_interval.to_string(), + recur_value, + recur_month, + end_date: tx + .end_date + .map(|d| d.format("%Y-%m-%d").to_string()) + .unwrap_or_default(), + tx_status: Vec::new(), + editing_tx: edit, + id_num: tx.id, + current_index: 0, + autofill: String::new(), + from_search: false, + } + } + + /// Builds a `TxData` with only the given fields prefilled. Currently used to + /// open the Search page with a single tag already filled in. + /// `date` is expected as `DD-MM-YYYY` and gets stored as `YYYY-MM-DD`. #[must_use] pub fn custom( date: &str, @@ -128,6 +202,11 @@ impl TxData { amount: amount.to_string(), tx_type: tx_type.to_string(), tags: tags.to_string(), + frequency: String::new(), + recur_interval: String::new(), + recur_value: String::new(), + recur_month: String::new(), + end_date: String::new(), tx_status: Vec::new(), editing_tx: true, id_num, @@ -137,7 +216,7 @@ impl TxData { } } - /// Returns all the data saved + /// Returns the Add TX page's text fields in widget order, autofill included #[must_use] pub fn get_all_texts(&self) -> Vec<&str> { vec![ @@ -169,6 +248,35 @@ impl TxData { TxType::IncomeExpense } + /// Returns the Recurring page's own fields: frequency, recur interval, recur value, + /// recur month and end date, in that order. + #[must_use] + pub fn get_recurring_texts(&self) -> Vec<&str> { + vec![ + &self.frequency, + &self.recur_interval, + &self.recur_value, + &self.recur_month, + &self.end_date, + ] + } + + /// Same safe-default idea as `get_tx_type`: never panics on an empty/partial frequency + /// field, defaulting to Daily. + #[must_use] + pub fn get_frequency(&self) -> RecurrenceFrequency { + if let Some(first_letter) = self.frequency.chars().next() { + match first_letter.to_ascii_lowercase() { + 'd' => return RecurrenceFrequency::Daily, + 'w' => return RecurrenceFrequency::Weekly, + 'm' => return RecurrenceFrequency::Monthly, + 'y' => return RecurrenceFrequency::Yearly, + _ => {} + } + } + RecurrenceFrequency::Daily + } + /// Insert or remove from date field according to the index point pub fn edit_date(&mut self, to_add: Option) { add_char_to(to_add, &mut self.current_index, &mut self.date); @@ -204,8 +312,33 @@ impl TxData { add_char_to(to_add, &mut self.current_index, &mut self.tags); } + /// Insert or remove from frequency field according to the index point + pub fn edit_frequency(&mut self, to_add: Option) { + add_char_to(to_add, &mut self.current_index, &mut self.frequency); + } + + /// Insert or remove from recur interval field according to the index point + pub fn edit_recur_interval(&mut self, to_add: Option) { + add_char_to(to_add, &mut self.current_index, &mut self.recur_interval); + } + + /// Insert or remove from recur value field according to the index point + pub fn edit_recur_value(&mut self, to_add: Option) { + add_char_to(to_add, &mut self.current_index, &mut self.recur_value); + } + + /// Insert or remove from recur month field according to the index point + pub fn edit_recur_month(&mut self, to_add: Option) { + add_char_to(to_add, &mut self.current_index, &mut self.recur_month); + } + + /// Insert or remove from end date field according to the index point + pub fn edit_end_date(&mut self, to_add: Option) { + add_char_to(to_add, &mut self.current_index, &mut self.end_date); + } + /// Takes all data and adds it as a transaction - pub fn add_tx(&mut self, tx_view: &TxViewGroup, migrated_conn: &mut DbConn) -> AResult<()> { + pub fn add_tx(&mut self, tx_view: &TxViewGroup, conn: &mut DbConn) -> AResult<()> { self.check_all_fields()?; let editing_tx = self.editing_tx; @@ -216,7 +349,7 @@ impl TxData { &self.to_method, &self.amount, &self.tx_type, - migrated_conn, + conn, )?; if editing_tx { @@ -224,16 +357,44 @@ impl TxData { let old_tx = if let Some(tx) = tx_view.get_tx_by_id(old_tx_id) { tx } else { - &migrated_conn.fetch_tx_with_id(old_tx_id)? + &conn.fetch_tx_with_id(old_tx_id)? }; - migrated_conn.edit_tx(old_tx, parsed_tx, &self.tags) + conn.edit_tx(old_tx, parsed_tx, &self.tags) + } else { + conn.add_new_tx(parsed_tx, &self.tags) + } + } + + /// Takes all Recurring page data and creates or updates a recurring transaction rule + pub fn add_recurring_tx(&mut self, conn: &mut DbConn) -> AResult<()> { + self.check_all_recurring_fields()?; + + let editing_tx = self.editing_tx; + + let parsed = parse_recurring_tx_fields( + &self.date, + &self.details, + &self.from_method, + &self.to_method, + &self.amount, + &self.tx_type, + &self.frequency, + &self.recur_interval, + &self.recur_value, + &self.recur_month, + &self.end_date, + conn, + )?; + + if editing_tx { + conn.edit_recurring_tx(self.id_num, parsed, &self.tags) } else { - migrated_conn.add_new_tx(parsed_tx, &self.tags) + conn.add_recurring_tx(parsed, &self.tags) } } - pub fn get_search_tx(&self, migrated_conn: &mut DbConn) -> AResult { + pub fn get_search_tx(&self, conn: &mut DbConn) -> AResult { let new_search = parse_search_fields( &self.date, &self.details, @@ -242,10 +403,10 @@ impl TxData { &self.amount, &self.tx_type, &self.tags, - migrated_conn, + conn, )?; - migrated_conn.search_txs(new_search) + conn.search_txs(new_search) } /// Adds a value to tx status @@ -267,6 +428,11 @@ impl TxData { TxTab::ToMethod => conn.autofill().tx_method(&self.to_method), TxTab::Tags => conn.autofill().tags(&self.tags), TxTab::TxType => conn.autofill().tx_type(&self.tx_type), + TxTab::Frequency => conn.autofill().frequency(&self.frequency), + TxTab::RecurValue => conn + .autofill() + .recur_value(&self.recur_value, self.get_frequency()), + TxTab::RecurMonth => conn.autofill().recur_month(&self.recur_month), _ => String::new(), } } @@ -277,6 +443,9 @@ impl TxData { TxTab::FromMethod => self.from_method = self.autofill.clone(), TxTab::ToMethod => self.to_method = self.autofill.clone(), TxTab::TxType => self.tx_type = self.autofill.clone(), + TxTab::Frequency => self.frequency = self.autofill.clone(), + TxTab::RecurValue => self.recur_value = self.autofill.clone(), + TxTab::RecurMonth => self.recur_month = self.autofill.clone(), TxTab::Tags => { let mut split_tags = self.tags.split(',').map(str::trim).collect::>(); @@ -377,7 +546,64 @@ impl TxData { status } - /// Checks the inputted tags to make sure it's properly separated by a comma + /// Checks the inputted recurrence frequency by the user upon pressing Enter/Esc + pub fn check_frequency(&mut self, conn: &mut DbConn) -> Result { + let mut frequency = self.frequency.clone(); + + let status = conn.verify().frequency(&mut frequency); + + self.frequency = frequency; + self.go_current_index(&TxTab::Frequency); + status + } + + /// Checks the inputted recur interval by the user upon pressing Enter/Esc + pub fn check_recur_interval(&mut self, conn: &mut DbConn) -> Result { + let mut recur_interval = self.recur_interval.clone(); + + let status = conn.verify().recur_interval(&mut recur_interval); + + self.recur_interval = recur_interval; + self.go_current_index(&TxTab::RecurInterval); + status + } + + /// Checks the inputted recur value by the user upon pressing Enter/Esc + pub fn check_recur_value(&mut self, conn: &mut DbConn) -> Result { + let mut recur_value = self.recur_value.clone(); + let frequency = self.get_frequency(); + + let status = conn.verify().recur_value(&mut recur_value, frequency); + + self.recur_value = recur_value; + self.go_current_index(&TxTab::RecurValue); + status + } + + /// Checks the inputted recur month by the user upon pressing Enter/Esc + pub fn check_recur_month(&mut self, conn: &mut DbConn) -> Result { + let mut recur_month = self.recur_month.clone(); + + let status = conn.verify().recur_month(&mut recur_month); + + self.recur_month = recur_month; + self.go_current_index(&TxTab::RecurMonth); + status + } + + /// Checks the inputted end date by the user upon pressing Enter/Esc. Empty is valid - + /// it means the recurrence never ends. + pub fn check_end_date(&mut self, conn: &mut DbConn) -> Result { + let mut end_date = self.end_date.clone(); + + let status = conn.verify().date(&mut end_date, DateType::Exact); + + self.end_date = end_date; + self.go_current_index(&TxTab::EndDate); + status + } + + /// Trims and de-duplicates the inputted tags, keeping unknown ones pub fn check_tags(&mut self, conn: &mut DbConn) { let mut tags = self.tags.clone(); @@ -387,7 +613,8 @@ impl TxData { self.go_current_index(&TxTab::Tags); } - /// Checks the inputted tags to make sure it's properly separated by a comma + /// Same as [`Self::check_tags`], but tags that don't exist in the database are + /// dropped and reported back as an error pub fn check_tags_forced(&mut self, conn: &mut DbConn) -> Result { let mut tags = self.tags.clone(); @@ -398,7 +625,7 @@ impl TxData { status } - /// Checks all field and verifies anything important is not empty + /// Ensures every field that must be filled in before saving a tx is filled in pub fn check_all_fields(&mut self) -> Result<(), CheckingError> { if self.date.is_empty() { return Err(CheckingError::EmptyDate); @@ -415,7 +642,53 @@ impl TxData { { return Err(CheckingError::EmptyMethod); } - // Empty tags in a tx becomes as unknown + // A tx saved without any tag is stored as "Unknown" + if self.tags.is_empty() { + self.tags = "Unknown".to_string(); + } + Ok(()) + } + + /// Ensures every field that must be filled in before saving a recurring rule + /// is filled in, plus the ones the chosen frequency needs + pub fn check_all_recurring_fields(&mut self) -> Result<(), CheckingError> { + if self.date.is_empty() { + return Err(CheckingError::EmptyDate); + } else if self.from_method.is_empty() && self.tx_type != "Transfer" { + return Err(CheckingError::EmptyMethod); + } else if self.amount.is_empty() { + return Err(CheckingError::EmptyAmount); + } else if self.tx_type.is_empty() { + return Err(CheckingError::EmptyTxType); + } else if self.tx_type == "Transfer" && self.from_method == self.to_method { + return Err(CheckingError::SameTxMethod); + } else if self.tx_type == "Transfer" + && (self.from_method.is_empty() || self.to_method.is_empty()) + { + return Err(CheckingError::EmptyMethod); + } else if self.frequency.is_empty() { + return Err(CheckingError::EmptyFrequency); + } else if self.recur_interval.is_empty() { + return Err(CheckingError::EmptyRecurInterval); + } + + match self.get_frequency() { + RecurrenceFrequency::Daily => {} + RecurrenceFrequency::Weekly | RecurrenceFrequency::Monthly => { + if self.recur_value.is_empty() { + return Err(CheckingError::EmptyRecurValue); + } + } + RecurrenceFrequency::Yearly => { + if self.recur_value.is_empty() { + return Err(CheckingError::EmptyRecurValue); + } else if self.recur_month.is_empty() { + return Err(CheckingError::EmptyRecurMonth); + } + } + } + + // A tx saved without any tag is stored as "Unknown" if self.tags.is_empty() { self.tags = "Unknown".to_string(); } @@ -442,12 +715,12 @@ impl TxData { false } - /// Checks for b on amount field to replace with the balance of the tx method field + /// Replaces a `b` in the amount with the current balance of the from method fn check_b_field(&mut self, conn: &mut DbConn) -> Result<(), VerifierError> { self.check_suffixes(); let user_amount = self.amount.to_lowercase(); - // 'b' represents the current balance of the original tx method + // 'b' stands for the current balance of the from method if user_amount.contains('b') && !self.from_method.is_empty() { let final_balances = conn .get_final_balances() @@ -457,8 +730,7 @@ impl TxData { .get_tx_method_by_name(self.from_method.as_str()) .map_err(|e| VerifierError::Others(e.to_string()))?; - // Get all the method's final balance, loop through the balances and match the tx method name - + // Find the final balance of the target method for balance in final_balances.values() { if balance.method_id == target_method.id { self.amount = user_amount @@ -471,7 +743,8 @@ impl TxData { Ok(()) } - /// If `k` and `m` are present, multiplies the number 1 thousand and 1 million respectively + /// Multiplies a number by 1 thousand for each `k` and by 1 million for each `m` + /// that follows it fn check_suffixes(&mut self) { let mut user_amount = self.amount.to_lowercase(); let mut failed_to_parse = false; @@ -480,10 +753,9 @@ impl TxData { let target_letters = ['k', 'm']; 'starter: for letter in target_letters { - // How many times this target char is present in the string + // One replacement per occurrence, so this runs `count` times let count = user_amount.chars().filter(|c| c == &letter).count(); - // We will loop the `count` of times to ensure all the target are handled for _ in 0..count { // Convert the string to a vec of char for easier indexing let amount_vec: Vec = user_amount.chars().collect(); @@ -492,8 +764,9 @@ impl TxData { let target_index = user_amount.find(letter).unwrap(); let mut gathered_value = String::new(); - // Ending here would be smaller than the target index. - // We are looping from target index to the beginning of the string + // Walk left from the suffix to collect the number it applies to. + // `ending_index` marks the char right after that number, so the + // suffix can be replaced without touching the rest of the string let mut ending_index = 0; for index in (0..target_index).rev() { @@ -511,17 +784,15 @@ impl TxData { if let Ok(num_amount) = value.to_string().parse::() { gathered_value = format!("{num_amount}{gathered_value}"); } else { - // 100 + 5k - // If this is the + char, then break the loop. The ending index is index of '+' + 1 - // + 1 so we don't replace the original char itself after the calculation is done + // e.g. `100 + 5k`: stop at the `+` and set `ending_index` + // to the char right after it, so the `+` itself is kept ending_index = index + 1; break; } } - // In the case like this: 1k1, 5m12 - // This is invalid. This ensure the next char where k or m was found is not a number - // If number, we can't parse it here + // A digit right after the suffix (`1k1`, `5m12`) is invalid, so give + // up and leave the amount untouched for value in amount_vec .iter() .take(user_amount.len()) @@ -545,9 +816,9 @@ impl TxData { _ => unreachable!(), }; - // Example string: 100 + 5k - // Target index would be the index of 'k' and ending index is the index of '+' + 1 - // Replace everything in that range with the new calculated value + // e.g. `100 + 5k`: the target index is the index of `k` and + // `ending_index` is the index of `+` + 1, so everything in + // between is replaced with the expanded number user_amount = user_amount.replacen( &user_amount[ending_index..=target_index], &suffixed_added_value.to_string(), @@ -582,6 +853,11 @@ impl TxData { TxTab::Amount => self.amount.len(), TxTab::TxType => self.tx_type.len(), TxTab::Tags => self.tags.len(), + TxTab::Frequency => self.frequency.len(), + TxTab::RecurInterval => self.recur_interval.len(), + TxTab::RecurValue => self.recur_value.len(), + TxTab::RecurMonth => self.recur_month.len(), + TxTab::EndDate => self.end_date.len(), TxTab::Nothing => 0, } } @@ -646,7 +922,7 @@ impl TxData { step_status } - /// Steps To Value value by one + /// Steps the To Method value by one pub fn step_to_method( &mut self, step_type: StepType, @@ -678,6 +954,84 @@ impl TxData { step_status } + /// Steps Frequency value by one + pub fn step_frequency( + &mut self, + step_type: StepType, + conn: &mut DbConn, + ) -> Result<(), SteppingError> { + let mut frequency = self.frequency.clone(); + + let step_status = conn.step().frequency(&mut frequency, step_type); + self.frequency = frequency; + + self.go_current_index(&TxTab::Frequency); + step_status + } + + /// Steps Recur Interval value by one + pub fn step_recur_interval( + &mut self, + step_type: StepType, + conn: &mut DbConn, + ) -> Result<(), SteppingError> { + let mut recur_interval = self.recur_interval.clone(); + + let step_status = conn.step().recur_interval(&mut recur_interval, step_type); + self.recur_interval = recur_interval; + + self.go_current_index(&TxTab::RecurInterval); + step_status + } + + /// Steps Recur Value value by one + pub fn step_recur_value( + &mut self, + step_type: StepType, + conn: &mut DbConn, + ) -> Result<(), SteppingError> { + let mut recur_value = self.recur_value.clone(); + let frequency = self.get_frequency(); + + let step_status = conn + .step() + .recur_value(&mut recur_value, frequency, step_type); + self.recur_value = recur_value; + + self.go_current_index(&TxTab::RecurValue); + step_status + } + + /// Steps Recur Month value by one + pub fn step_recur_month( + &mut self, + step_type: StepType, + conn: &mut DbConn, + ) -> Result<(), SteppingError> { + let mut recur_month = self.recur_month.clone(); + + let step_status = conn.step().recur_month(&mut recur_month, step_type); + self.recur_month = recur_month; + + self.go_current_index(&TxTab::RecurMonth); + step_status + } + + /// Steps End Date value by one + pub fn step_end_date( + &mut self, + step_type: StepType, + conn: &mut DbConn, + ) -> Result<(), SteppingError> { + let mut end_date = self.end_date.clone(); + + let step_status = conn.step().date(&mut end_date, step_type, DateType::Exact); + self.end_date = end_date; + + self.go_current_index(&TxTab::EndDate); + step_status + } + /// Steps Amount value by one pub fn step_amount( &mut self, @@ -761,7 +1115,7 @@ impl TxData { &self, tx_view: &TxViewGroup, index: Option, - migrated_conn: &mut DbConn, + conn: &mut DbConn, ) -> AResult>> { if self.generation_fields_exists() { let partial_tx = PartialTx { @@ -771,9 +1125,50 @@ impl TxData { tx_type: &self.tx_type, }; - tx_view.add_tx_balance_array(index, Some(partial_tx), migrated_conn) + tx_view.add_tx_balance_array(index, Some(partial_tx), conn) } else { - tx_view.add_tx_balance_array(index, None, migrated_conn) + tx_view.add_tx_balance_array(index, None, conn) } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn accept_autofill_fills_frequency_day_of_week_and_month() { + let mut data = TxData::new_empty(); + + data.frequency = "we".to_string(); + data.autofill = "Weekly".to_string(); + data.accept_autofill(&TxTab::Frequency); + assert_eq!(data.frequency, "Weekly"); + assert!(data.autofill.is_empty()); + assert_eq!(data.get_current_index(), "Weekly".len()); + + data.recur_value = "mon".to_string(); + data.autofill = "Monday".to_string(); + data.accept_autofill(&TxTab::RecurValue); + assert_eq!(data.recur_value, "Monday"); + + data.recur_month = "sep".to_string(); + data.autofill = "September".to_string(); + data.accept_autofill(&TxTab::RecurMonth); + assert_eq!(data.recur_month, "September"); + } + + #[test] + fn accept_autofill_leaves_other_recurring_fields_alone() { + let mut data = TxData::new_empty(); + data.recur_interval = "2".to_string(); + data.end_date = "2027-01-01".to_string(); + data.autofill = "Weekly".to_string(); + + data.accept_autofill(&TxTab::RecurInterval); + data.accept_autofill(&TxTab::EndDate); + + assert_eq!(data.recur_interval, "2"); + assert_eq!(data.end_date, "2027-01-01"); + } +} diff --git a/tui/src/utility/utils.rs b/tui/src/utility/utils.rs index eb0448a0..01100615 100644 --- a/tui/src/utility/utils.rs +++ b/tui/src/utility/utils.rs @@ -94,8 +94,7 @@ pub fn main_block(theme: &Theme) -> Block<'_> { Block::default().style(Style::default().bg(theme.background()).fg(theme.border())) } -/// Takes a string and makes any word before the first occurrence of : to Bold -/// Used for rendering +/// Bolds everything before the first `:` of each line, for rendering #[must_use] pub fn create_bolded_text(text: &str) -> Vec> { let mut text_data = Vec::new(); @@ -115,7 +114,7 @@ pub fn create_bolded_text(text: &str) -> Vec> { text_data } -/// Tabs from some given data for the UI +/// Builds a tab widget out of the given indexed data for the UI #[must_use] pub fn create_tab<'a>(data: &'a IndexedData, name: &'a str, theme: &'a Theme) -> Tabs<'a> { let titles: Vec = data @@ -135,7 +134,8 @@ pub fn create_tab<'a>(data: &'a IndexedData, name: &'a str, theme: &'a Theme) -> ) } -/// Create a tab with some values where each value's color will depend on the provided `HashMap` bool value +/// Same as [`create_tab`], except every title is colored by the bool the +/// provided `HashMap` holds for it #[must_use] pub fn create_tab_activation<'a>( data: &'a IndexedData, @@ -169,7 +169,7 @@ pub fn parse_github_body(body: &str) -> String { let body = body.replace('*', "•"); let body = body.replace('\r', ""); let end_point = body.find("## Changes").unwrap(); - format!("\n{}\n", &body[..end_point].trim()) + format!("\n{}\n", body[..end_point].trim()) } /// Used for sorting summary table data @@ -217,8 +217,8 @@ pub fn add_char_to(to_add: Option, current_index: &mut usize, current_data /// Checks if the string contains any symbol indicating comparison #[must_use] pub fn check_comparison(input: &str) -> ComparisonType { - // Need to handle 2 letter ones first otherwise in case of >= - // it will match with > + // The 2 character symbols have to be checked first, otherwise `>=` would + // match as `>` if input.starts_with("<=") { ComparisonType::EqualOrSmaller } else if input.starts_with(">=") {