Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@kallyas
Copy link
Owner

@kallyas kallyas commented Apr 17, 2025

This pull request introduces several new view models and UI screens to enhance the loan application, loan management, and document upload functionalities in the sacco_mobile application. The changes include adding state management, business logic, and user interfaces for loan applications, loan lists, loan repayments, and document uploads. Below are the most important changes grouped by theme:

Loan Application Management

Loan List Management

Loan Repayment Management

Document Upload

Summary by CodeRabbit

  • New Features
    • Introduced comprehensive loan management screens, including loan application, loan detail, loan list, and loan repayment.
    • Added full savings account functionality: view account details, make deposits/withdrawals, and view/filter transactions with detailed transaction screens.
    • Enabled document upload and viewing for profile photos and IDs, plus a document viewer supporting images and PDFs.
    • Added profile editing and next of kin management, including add, edit, and delete flows.
  • Improvements
    • Enhanced profile screen with navigation to new profile, document, and next of kin features.
  • Chores
    • Integrated new dependencies: photo_view and image_picker.
    • Added file selector plugin support for Linux, macOS, and Windows platforms.

kallyas added 26 commits April 16, 2025 18:34
This commit introduces several enhancements to the profile screen:

- Implemented navigation to EditProfileScreen for editing profile details.
- Added navigation to DocumentUploadScreen for uploading profile photo, national ID, and passport photo, with document type handling.
- Implemented navigation to DocumentViewerScreen to view uploaded documents.
- Integrated a Next of Kin tab with functionality to add, edit, and delete next of kin information using NextOfKinFormScreen, including a delete confirmation dialog.
- Added helper functions (_buildInfoRow, _buildDocumentItem, _getRelationshipText) to improve code readability and maintainability.

These changes enhance the user's ability to manage their profile information, upload required documents, and manage their next of kin details.
This commit introduces the `SavingsAccountViewModel` to manage the state and data related to savings accounts.

The `SavingsAccountViewModel` handles fetching savings account details, making deposits, and making withdrawals using the `SavingsRepository`.

Key features include:

- Loading savings account data using `loadSavingsAccount()`.
- Making deposits via `makeDeposit()`.
- Making withdrawals via `makeWithdrawal()`, including balance validation.
- Managing state using `SavingsAccountState` enum for UI updates.
- Error handling and user-friendly error messages.
- Fetching all savings accounts for users with multiple accounts

This ViewModel will be used to provide savings account functionality to the UI.
This commit introduces the `TransactionViewModel` to manage and display
savings transactions. It includes functionality for:

- Fetching transactions from the `SavingsRepository`.
- Pagination (`loadMoreTransactions`, `_page`, `_pageSize`).
- Filtering by account ID and date range (`accountId`, `startDate`, `endDate`).
- Refreshing the transaction list (`refreshTransactions`).
- State management (`TransactionState`) to handle loading, success, and error
  states.
- Implemented `getTransactionById` for retrieving a single transaction.

The ViewModel also handles error scenarios and provides user-friendly error
messages.  The `loadTransactions` function handles initial loads and
subsequent filter applications.  `resetFilters` resets all applied filters.
This commit introduces the SavingsAccountScreen, which displays detailed
information about a user's savings account, including balance, transactions,
and available actions.

Key changes include:

- Implemented UI to display account details (account type, number, balance,
  interest rate, etc.) using data from SavingsAccountViewModel.
- Integrated TransactionViewModel to display a list of recent transactions.
- Added refresh functionality using RefreshIndicator to reload account and
  transaction data.
- Implemented Deposit and Withdraw actions using AppButton, navigating to
  their respective screens.
- Displayed transaction history with details such as date, time, amount, and
  status. Navigation to TransactionDetailScreen on tap.
- Handled loading, error, and empty state scenarios for both account and
  transaction data.
- Added a currency formatter for Uganda Shillings (UGX).

The SavingsAccountScreen fetches data from SavingsAccountViewModel and
TransactionViewModel, displaying account information and transaction history.
Users can refresh data, deposit, or withdraw funds. The screen also handles
various states, such as loading, error, and empty data scenarios.
This commit adds the registration of the `FileSelectorPlugin` for the
Linux platform in `generated_plugin_registrant.cc`. This ensures that
the file selector functionality is available when the Flutter application
is run on Linux.  The `file_selector_plugin_register_with_registrar`
function is called to properly initialize the plugin.
This commit adds the `file_selector_linux` plugin to the
`FLUTTER_PLUGIN_LIST` in `generated_plugins.cmake`. This ensures
that the `file_selector_linux` plugin is correctly included and
linked in the Linux build of the Flutter application. The plugin
provides file selection functionality, enabling the app to interact
with the operating system's file system for tasks such as opening
and saving files.
This commit adds the `FileSelectorPlugin` registration to
`GeneratedPluginRegistrant.swift`. This enables file selection
functionality on macOS builds of the Sacco Mobile application,
allowing users to select files within the app. The plugin is
registered using `FlutterPluginRegistry` and its registrar.
This commit adds the `image_picker` and `file_selector` dependencies, along with their platform implementations (Android, iOS, Linux, macOS, Windows, Web), to the `pubspec.lock` file.  Also adds `photo_view` dependency. These packages enable image and file selection functionality within the application. The addition of `http` and `cross_file` support transitive dependencies for `image_picker`.
This commit adds the `photo_view` and `image_picker` packages as
dependencies.

- `photo_view` (^0.15.0) is added to enable zooming and panning of
  images within the application, enhancing the user experience when
  viewing images.
- `image_picker` (^1.1.2) is added to allow users to select images from
  their device's gallery or camera, facilitating features such as profile
  picture updates or uploading supporting documents.
This commit adds the FileSelectorWindows plugin registration
to the `generated_plugin_registrant.cc` file. This enables
the use of file selection functionality within the Windows
version of the application.

The `FileSelectorWindowsRegisterWithRegistrar` function is
called to register the plugin with the Flutter plugin registry.
This ensures that the plugin is properly initialized and can
be accessed by the Flutter application.
This commit adds the `file_selector_windows` plugin to the
`FLUTTER_PLUGIN_LIST` in `generated_plugins.cmake`. This ensures
that the `file_selector_windows` plugin is properly linked and
available for use in the Flutter Windows application. This plugin
enables file selection functionality within the application on
Windows platforms.
This commit introduces the `DocumentUploadScreen` widget, allowing users
to upload ID documents or profile photos.

The screen provides the following functionalities:

- Image selection from gallery or camera using `image_picker`.
- Preview of the selected image.
- Upload functionality using `ProfileViewModel.uploadIdDocument` or
  `ProfileViewModel.uploadProfilePhoto` based on `documentType`.
- Displays loading indicator during upload.
- Shows error messages and success notifications using `SnackBar`.
- Clear button to remove selected image.

The screen takes `documentType` ('ID' or 'PHOTO') and `documentTitle`
as arguments to customize the UI.
This commit introduces the `DocumentViewerScreen` to display documents
from a given URL.  It supports image files (jpg, jpeg, png) using
`photo_view` for zoom and pan functionality, and includes basic error
handling and loading indicators.  A placeholder is included for PDF
viewing, with a commented-out example using `flutter_pdfview` (package
not included in this commit).  A download button is added to the
AppBar, with a placeholder implementation. The `_documentViewer`
function determines the appropriate viewer based on file extension.
This commit introduces the 'EditProfileScreen' feature, allowing users to modify their profile information.

The screen includes fields for:
- Marital Status (dropdown)
- Employment Status (dropdown)
- Occupation (text field)
- Monthly Income (number field with validation)
- Physical Address (text field)
- Postal Address (text field)
- City (text field)
- District (text field)

Form validation is implemented to ensure data integrity. The updated profile data is then sent to the ProfileViewModel to update the member's profile. A loading indicator is displayed during the update process, and a success message is shown upon completion.
This commit introduces the 'NextOfKinFormScreen' allowing users to add or
edit their next of kin information. The form includes fields for full
name, relationship, phone number, email, physical address, national ID,
and percentage share.  Input validation is implemented for each field,
including phone number, email, and percentage share (0-100). The form
uses the 'ProfileViewModel' to handle saving and updating the data. The
screen supports both adding new next of kin and editing existing ones,
distinguished by the presence of the `nextOfKin` parameter. The UI
includes error handling and displays success messages upon saving.

Keywords: NextOfKinFormScreen, ProfileViewModel, addNextOfKin,
updateNextOfKin, form validation, TextEditingController, percentage_share,
phone number validation, email validation, dropdown field.
This commit introduces the 'AllTransactionsScreen' to display a paginated list of transactions.
It includes filtering by transaction type and date range using a bottom sheet.

Key changes:
- Implemented UI for transaction list, loading, and error states.
- Added filtering functionality using a bottom sheet with transaction type and date range options.
- Implemented pagination for loading more transactions on scroll.
- Integrated with TransactionViewModel to fetch and display transactions.
- Added UI elements to display active filters.

The purpose is to provide users with a comprehensive view of their transaction history,
allowing them to filter and navigate through the data efficiently.
This commit introduces the DepositScreen, enabling users to deposit funds
into their savings accounts.

Key changes include:
- UI layout with amount, payment method, and description fields.
- Form validation to ensure valid deposit amounts.
- Integration with SavingsAccountViewModel for deposit processing.
- Display of account information (number, balance).
- Payment method selection via dropdown.
- Success/error message handling using SnackBar.
- Loading indicator during deposit processing.

The deposit functionality leverages the `makeDeposit` function
in SavingsAccountViewModel.
This commit introduces the `TransactionDetailScreen` to display
comprehensive information about a specific transaction.

The screen includes:
- A status card displaying the transaction status, type, and amount.
- Detailed transaction information such as ID, payment method, and dates.
- Description, if available.
- Action buttons for copying the transaction ID, viewing receipt, and getting help.

The screen utilizes `intl` package for date and currency formatting.
The share functionality, receipt view, and help actions are marked as TODO.
This commit introduces the `WithdrawScreen`, enabling users to withdraw funds
from their savings accounts. The screen includes:

- Account information display (balance, account number).
- Withdrawal amount input with validation (amount, available balance).
- Payment method selection (Cash, Mobile Money, Bank Transfer).
- Optional description field for withdrawal purpose.
- Confirmation dialog before submitting the withdrawal.
- Integration with `SavingsAccountViewModel` for handling withdrawal logic.
- Display error and warning messages.

Keywords: withdrawal, savings account, balance, payment method, `WithdrawScreen`,
`SavingsAccountViewModel`, confirmation dialog, validation, `_submitWithdrawal`.
This commit introduces the `LoanApplicationViewModel` to manage loan application data and state.

The following functionalities are included:
- Loading loan applications (`loadLoanApplications`) from `LoanRepository`.
- Submitting new loan applications (`submitLoanApplication`) and updating the application list.
- Retrieving a specific loan application by ID (`getLoanApplicationById`), first checking locally, then fetching from the API.
- Managing the current loan application (`setCurrentApplication`).
- Calculating monthly payments (`calculateMonthlyPayment`) and total interest (`calculateTotalInterest`).
- Providing loan type details (`getLoanTypeDetails`) from a predefined list.
- Managing the application state (`LoanApplicationState`) and error messages.
- Resetting the state (`resetState`).

This ViewModel centralizes the loan application logic, making it easier to manage the UI state and data flow in the loan application feature. Includes `LoanRepository` dependency injection.
This commit introduces the `LoanListViewModel` to manage and provide
loan data to the UI. It fetches loans from the `LoanRepository`,
handles different states (initial, loading, success, error), and
provides methods to access all loans and active loans.

The ViewModel includes:
- `loadLoans()`: Fetches all loans and filters for active loans.
- `loadActiveLoans()`: Fetches only active loans.
- `getLoanById(int loanId)`: Retrieves a specific loan by its ID,
  first checking the local list and then fetching from the API if
  necessary.
- `refreshLoans()`: Refreshes loans by calling `loadLoans()`.

This ViewModel uses `ChangeNotifier` to update the UI when the loan
data changes, ensuring a reactive and efficient data flow. It also
includes error handling to display user-friendly messages.
This commit introduces the LoanApplicationScreen, a new feature that
allows users to apply for loans through a multi-step form.  The screen
is built with a PageView to separate the application process into logical
sections: Loan Type and Amount, Purpose and Collateral, and Employment
and Summary.

Key changes include:

- Added UI components for inputting loan details such as amount, purpose,
  collateral, employment details, and monthly income.
- Implemented a progress indicator to show the user's progress through
  the application process.
- Implemented basic form validation.
- Integrated `LoanApplicationViewModel` to manage the application state
  and submission process.
- Added navigation buttons to move between the different sections of the
  form.
- Implemented conditional UI rendering based on the application state
  (e.g., submitting, success).

The LoanApplicationScreen uses `PageView`, `Form`, `TextFormField`,
`LoanApplicationViewModel` and `LinearProgressIndicator`.
This commit introduces the LoanListScreen, displaying a list of loans.

- Implemented a tabbed view (Active, All Loans, History) using TabBar and TabBarView.
- Fetches and displays loan data using LoanListViewModel and Provider.
- Displays active loans, all loans, and loan history (completed/rejected) in separate tabs.
- Includes loading, error, and empty state handling.
- Added refresh functionality to update loan data.
- Implemented loan card display for each loan with details like amount, outstanding balance, and status.
- Implemented a history loan card to display completed and rejected loans
- Adds an 'Apply for Loan' floating action button that navigates to the LoanApplicationScreen.
- Adds a LinearProgressIndicator showing the loan progress
- Implemented loan status colors depending on their states
- Next payment date is displayed where neccessary
This commit implements navigation to the AllTransactionsScreen from the SavingsAccountScreen.  It removes the placeholder comment and replaces it with a Navigator.push to display all transactions associated with the current savings account. The accountId is passed as a parameter to the AllTransactionsScreen.
This commit introduces the `LoanRepaymentViewModel` to manage the state
and logic related to loan repayments. It includes:

- Added `LoanRepaymentViewModel` class with states (initial, loading,
  success, error, processing, processed) for managing UI updates.
- Implemented `makeRepayment` function to process loan repayments via
  the `LoanRepository`.  This function also updates the local `_selectedLoan`
  with the new outstanding balance after a successful repayment.
- Added `calculatePenalty` function to calculate potential penalties based
  on missed payments. This is currently a placeholder and subject to
  change based on actual business rules.
- Implemented `resetState` to reset the viewmodel state after a
  repayment process.
- Included error handling to display user-friendly messages in case of
  failure.

The view model utilizes `LoanRepository` for API calls and
`notifyListeners()` to update the UI based on the repayment process.

Keywords: LoanRepaymentViewModel, makeRepayment, LoanRepository,
repayment, penalty, outstandingBalance, state management.
This commit introduces the LoanDetailScreen, providing a detailed view
of a specific loan.  The screen displays loan summary, payment details,
and loan activity.

Key changes:
- Implemented UI to display loan details including principal, interest,
  term, and outstanding balance using `Loan` model data.
- Added functionality to display payment details, including the next
  payment due date and a button to initiate a `LoanRepaymentScreen`.
- Displayed loan activity timeline with events like loan approval and
  disbursement.
- Added a refresh button to update loan information.
- Implemented conditional UI elements based on loan status (e.g., bottom
  navigation bar only visible for active loans).
- Handles loading and error states using `LoadingIndicator` and
  `AppErrorWidget`.
- Added calculation of monthly payment (`_calculateMonthlyPayment`)

This screen enhances the user experience by providing a central location
for all loan-related information. Keywords: LoanDetailScreen, loan,
payment, repayment, details, activity, outstanding balance.
@coderabbitai
Copy link

coderabbitai bot commented Apr 17, 2025

Walkthrough

This update introduces a comprehensive set of new features and enhancements across the loans, savings, and profile management modules of a Flutter application. It adds new view models for loan applications, loan lists, loan repayments, savings accounts, and transactions, each encapsulating state management, business logic, and error handling using the ChangeNotifier pattern. Corresponding UI screens are implemented for applying for loans, viewing loan details and lists, managing savings accounts, making deposits and withdrawals, viewing all transactions, and handling transaction details. The profile section receives new screens for editing profiles, uploading and viewing documents, and managing next of kin entries. Additional dependencies for image and document handling are added, and platform-specific plugin registrants are updated to support file selection. All changes are structured to provide a reactive and user-friendly experience.

Changes

File(s) Change Summary
.../loan_application_viewmodel.dart, .../loan_list_viewmodel.dart, .../loan_repayment_viewmodel.dart Added new view models for loan application, loan list, and loan repayment management, each with state enums, error handling, and business logic for interacting with repositories and updating UI via ChangeNotifier.
.../views/loan_application_screen.dart, .../views/loan_detail_screen.dart, .../views/loan_list_screen.dart Added new screens for loan application (multi-step form), loan details (with repayment and activity tracking), and loan list (with tabs for active, all, and history), each using the new view models for state and data management.
.../views/document_upload_screen.dart, .../views/document_viewer.dart, .../views/edit_profile_screen.dart, .../views/next_of_kin_form_screen.dart, .../views/profile_screen.dart Introduced new profile management screens: document upload (with camera/gallery), document viewer (image/PDF), edit profile (form), next of kin form (add/edit), and enhanced profile screen with navigation for editing, uploading, viewing, and managing next of kin.
.../savings_account_viewmodel.dart, .../transaction_viewmodel.dart Added view models for savings account management and transactions, including state handling, deposit/withdrawal logic, transaction filtering, pagination, and error management.
.../views/all_transactions.dart, .../views/deposit_screen.dart, .../views/savings_account_screen.dart, .../views/transaction_detail_screen.dart, .../views/withdraw_screen.dart Implemented new savings-related screens: all transactions (with filtering and pagination), deposit, savings account overview, transaction detail, and withdrawal (with confirmation and validation), all leveraging the new view models.
linux/flutter/generated_plugin_registrant.cc, linux/flutter/generated_plugins.cmake, macos/Flutter/GeneratedPluginRegistrant.swift, windows/flutter/generated_plugin_registrant.cc, windows/flutter/generated_plugins.cmake Updated platform plugin registrant files to include file selector plugins for Linux, macOS, and Windows, enabling file selection capabilities across desktop platforms.
pubspec.yaml Added dependencies: photo_view for image viewing and image_picker for image selection.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant LoanApplicationScreen
    participant LoanApplicationViewModel
    participant LoanRepository

    User->>LoanApplicationScreen: Start loan application
    LoanApplicationScreen->>LoanApplicationViewModel: Submit application data
    LoanApplicationViewModel->>LoanRepository: submitLoanApplication(data)
    LoanRepository-->>LoanApplicationViewModel: Result (success/error)
    LoanApplicationViewModel-->>LoanApplicationScreen: Update state (submitted/success/error)
    LoanApplicationScreen-->>User: Show result (success screen or error)
Loading
sequenceDiagram
    participant User
    participant SavingsAccountScreen
    participant SavingsAccountViewModel
    participant SavingsRepository

    User->>SavingsAccountScreen: View account / Make deposit / Withdraw
    SavingsAccountScreen->>SavingsAccountViewModel: loadSavingsAccount() / makeDeposit() / makeWithdrawal()
    SavingsAccountViewModel->>SavingsRepository: Fetch or update account data
    SavingsRepository-->>SavingsAccountViewModel: Result (account data / confirmation / error)
    SavingsAccountViewModel-->>SavingsAccountScreen: Update state
    SavingsAccountScreen-->>User: Show account info / confirmation / error
Loading

Poem

🐇
A hop, a skip, new screens appear,
With loans and savings crystal clear.
Profiles polished, docs in hand,
Next of kin at your command.
Transactions scroll, deposits flow—
Across all platforms, features grow!
With every change, this rabbit beams,
The app now sparkles in your dreams.
🌱✨

Tip

⚡💬 Agentic Chat (Pro Plan, General Availability)
  • We're introducing multi-step agentic chat in review comments and issue comments, within and outside of PR's. This feature enhances review and issue discussions with the CodeRabbit agentic chat by enabling advanced interactions, including the ability to create pull requests directly from comments and add commits to existing pull requests.
✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@kallyas kallyas merged commit dc28cd4 into main Apr 17, 2025
2 of 3 checks passed
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 28

🔭 Outside diff range comments (1)
sacco_mobile/lib/features/profile/views/profile_screen.dart (1)

45-73: 🛠️ Refactor suggestion

Avoid recreating ProfileViewModel every frame – lift it out of build.

ChangeNotifierProvider(create: …) inside build leads to a fresh ViewModel each rebuild, wiping state (memberProfile, tab data, errors).
Move it to initState or use .value:

-class _ProfileScreenState extends State<ProfileScreen>
-    with SingleTickerProviderStateMixin {
-  late TabController _tabController;
+class _ProfileScreenState extends State<ProfileScreen>
+    with SingleTickerProviderStateMixin {
+  late TabController _tabController;
+  late final ProfileViewModel _viewModel = getIt<ProfileViewModel>();
@@
-    return ChangeNotifierProvider(
-      create: (_) => getIt<ProfileViewModel>(),
+    return ChangeNotifierProvider.value(
+      value: _viewModel,

This preserves network results across orientation changes and prevents duplicate API calls.

🧹 Nitpick comments (28)
sacco_mobile/linux/flutter/generated_plugin_registrant.cc (1)

1-19: Be cautious about direct edits to generated files.

While the changes to register the FileSelectorPlugin are technically correct and align with the document upload functionality mentioned in the PR objectives, this is an auto-generated file as indicated by the comment at the top. Instead of manual edits, the recommended approach is to add the file_selector dependency to your pubspec.yaml file and let Flutter regenerate these files during the build process.

Make sure that the file_selector plugin is properly added to your pubspec.yaml dependencies:

dependencies:
  file_selector: ^[version]

After adding the dependency, run flutter pub get to update your dependencies and regenerate the platform-specific plugin files automatically.

sacco_mobile/lib/features/loans/views/loan_detail_screen.dart (1)

699-703: Division‑by‑zero guard for monthly‑payment calculation

If termMonths were ever 0 (e.g., corrupt data), this would raise a IntegerDivisionByZeroException.

- return loan.totalAmountPayable / loan.termMonths;
+ return loan.termMonths == 0
+     ? 0
+     : loan.totalAmountPayable / loan.termMonths;

Consider surfacing an error or fallback UI when the term is invalid.

sacco_mobile/lib/features/loans/viewmodels/loan_repayment_viewmodel.dart (2)

110-119: Document the penalty calculation business rules.

The method includes a comment mentioning this is an example calculation, but in a production app, actual business rules should be clearly documented.

// Calculate penalty amount (if any)
double calculatePenalty(Loan loan) {
  // Check if there are any missed payments
  if (loan.missedPaymentsCount > 0) {
-   // Example penalty calculation: 5% of outstanding balance per missed payment
-   // This is just an example, actual calculations would depend on business rules
+   // Penalty calculation: 5% of outstanding balance per missed payment
+   // Business Rule: For each missed payment, add 5% penalty on the outstanding balance
+   // Example: If outstanding balance is 1000 and 2 payments are missed, penalty is 100 (1000 * 0.05 * 2)
    return loan.outstandingBalance * 0.05 * loan.missedPaymentsCount;
  }
  return 0.0;
}

38-49: Consider adding more detailed error return information.

The method currently returns just a boolean success status. Consider enhancing error reporting with more details.

- Future<bool> makeRepayment({
+ Future<Map<String, dynamic>> makeRepayment({
  required int loanId,
  required double amount,
  required String paymentMethod,
  String? description,
}) async {
  if (_selectedLoan == null) {
    _errorMessage = 'No loan selected';
    notifyListeners();
-   return false;
+   return {'success': false, 'error': 'No loan selected'};
  }

And update the return statements throughout the method accordingly.

sacco_mobile/lib/features/loans/views/loan_list_screen.dart (3)

191-359: Extract loan card widgets to separate component files.

Both _buildLoanCard and _buildHistoryLoanCard methods are quite large and could be extracted into their own widget classes to improve maintainability and testability.

Create separate files for these components:

  1. loan_card.dart for the active loan card
  2. history_loan_card.dart for the loan history card

This approach would make the main file more manageable and the components reusable across the app.

Also applies to: 361-458


299-305: Use a more semantic approach for loan progress indication.

The current implementation uses the generic LinearProgressIndicator. Consider using a more domain-specific component that better communicates the loan payoff progress.

- LinearProgressIndicator(
-   value: loan.progressPercentage / 100,
-   backgroundColor: Colors.grey[200],
-   valueColor: AlwaysStoppedAnimation<Color>(
-     _getStatusColor(loan.status),
-   ),
- ),
+ // Create a custom LoanProgressIndicator
+ LoanProgressIndicator(
+   progressPercentage: loan.progressPercentage,
+   statusColor: _getStatusColor(loan.status),
+ ),

Where LoanProgressIndicator could be a custom widget that shows more contextual information about the loan payoff status.


521-538: Use an extension method for status color mapping.

The _getStatusColor method could be better implemented as an extension method on the loan status, which would make it more reusable across different screens.

Create an extension like this in a separate file:

// loan_status_extensions.dart
extension LoanStatusExtension on String {
  Color getStatusColor({bool light = false}) {
    switch (this) {
      case 'PENDING':
        return light ? Colors.amber[100]! : Colors.amber[700]!;
      case 'APPROVED':
        return light ? Colors.blue[100]! : Colors.blue[700]!;
      // ... other cases
      default:
        return light ? Colors.grey[200]! : Colors.grey[700]!;
    }
  }
}

Then update usage to:

loan.status.getStatusColor(light: true)
sacco_mobile/lib/features/loans/viewmodels/loan_application_viewmodel.dart (2)

170-177: Refactor getLoanTypeDetails to use orElse parameter instead of try-catch.

Using try-catch for control flow is inefficient when the firstWhere method provides a cleaner alternative with the orElse parameter.

Map<String, dynamic>? getLoanTypeDetails(String loanType) {
-  try {
-    return _loanTypes.firstWhere((type) => type['value'] == loanType);
-  } catch (e) {
-    return null;
-  }
+  return _loanTypes
+      .where((type) => type['value'] == loanType)
+      .firstOrNull;
}

Or use the orElse parameter correctly:

Map<String, dynamic>? getLoanTypeDetails(String loanType) {
  return _loanTypes.firstWhere(
    (type) => type['value'] == loanType,
    orElse: () => <String, dynamic>{},
  ).isEmpty ? null : _loanTypes.firstWhere((type) => type['value'] == loanType);
}

146-152: Document expected input format for financial calculations.

The financial calculation methods don't clearly document the expected format for inputs. Adding clear documentation would help prevent calculation errors.

// Calculate monthly payment
+ /// Calculates the monthly payment amount for a loan.
+ /// 
+ /// Parameters:
+ /// - amount: The principal loan amount (e.g., 10000)
+ /// - rate: The annual interest rate as a percentage (e.g., 15.0 for 15%)
+ /// - term: The loan term in months (e.g., 24 for 2 years)
+ /// 
+ /// Returns the monthly payment amount.
double calculateMonthlyPayment(double amount, double rate, int term) {
  final monthlyRate = rate / 100 / 12;
  final payment = (amount * monthlyRate * pow((1 + monthlyRate), term)) /
                (pow((1 + monthlyRate), term) - 1);
  return payment;
}
sacco_mobile/lib/features/savings/views/deposit_screen.dart (2)

110-122: Input formatter disallows decimal amounts.

FilteringTextInputFormatter.digitsOnly blocks “12.50” or “1000.75”, yet the back‑end accepts fractional values (double.parse is used later). Switch to a regex‑based formatter that permits one optional decimal point:

- inputFormatters: [
-   FilteringTextInputFormatter.digitsOnly,
- ],
+ inputFormatters: [
+   FilteringTextInputFormatter.allow(RegExp(r'^\d+\.?\d{0,2}$')),
+ ],

31-35: Minor: Use the current locale instead of hard‑coding 0 decimal digits.

If the product later supports currencies that require cents, hard‑coding decimalDigits: 0 will silently round user data.

- decimalDigits: 0,
+ decimalDigits: NumberFormat.compactSimpleCurrency(locale: 'en').decimalDigits ?? 2,
sacco_mobile/lib/features/savings/views/withdraw_screen.dart (2)

110-122: Decimal amounts are blocked for withdrawals as well.

Mirror the regex change suggested for the deposit screen so users can withdraw non‑integer figures.


399-418: Race‑condition possibility – user can tap “Withdraw” twice.

Because _submitWithdrawal awaits makeWithdrawal after showing the confirmation dialog, the user can spam the button before state switches to loading. Disable the button immediately after the dialog is accepted to guard against duplicate requests, or track an internal _isSubmitting flag.

sacco_mobile/lib/features/savings/views/savings_account_screen.dart (1)

400-408: Arrow direction for deposit / withdrawal is counter‑intuitive.

Conventionally:
• Deposit → arrow up ( money coming into the account )
• Withdrawal → arrow down ( money going out )

Consider swapping the icons so the UI communicates intent more clearly.

sacco_mobile/lib/features/savings/views/transaction_detail_screen.dart (2)

17-23: Avoid re‑creating expensive NumberFormat / DateFormat objects every build.

intl formatters allocate native resources; caching them enhances performance:

static final _currencyFormat = NumberFormat.currency(symbol: 'UGX ', decimalDigits: 0);
static final _dateFormat     = DateFormat('dd MMMM, yyyy');
static final _timeFormat     = DateFormat('hh:mm a');
...
Widget build(BuildContext context) {
  return Scaffold(
    ...
    Text(_currencyFormat.format(transaction.amount))

250-278: Fixed 140 px label width can truncate text on small devices / large font scales.

Switch to Flexible/Expanded widgets or use LayoutBuilder to allocate space dynamically, improving accessibility:

- SizedBox(
-   width: 140,
-   child: Text(label, ...),
- ),
+ Flexible(
+   flex: 3,
+   child: Text(label, overflow: TextOverflow.fade, softWrap: false, ...),
+ ),
sacco_mobile/lib/features/savings/views/all_transactions.dart (2)

623-629: “This Week” computation assumes Sunday‑based weeks.

today.weekday % 7 makes Sunday the first day; most locales consider Monday the first weekday.
Prefer the DateTime.monday constant or the intl package’s week‑of‑year helpers to avoid regional confusion.


756-775: Repeated ! assertions on color shades risk null crashes on custom themes.

Colors.orange[700]! is safe with the default palette but can return null when the primary swatch is overridden.
Either keep the nullable type and provide a fallback or use ColorScheme from the app theme.

sacco_mobile/lib/features/savings/viewmodels/transaction_viewmodel.dart (1)

67-70: Setting state to loading on every pagination request causes UI flicker.

When _page > 1, consider a separate loadingMore flag or keep _state as success and expose a boolean isPaginating to avoid replacing the entire list with a loader.

sacco_mobile/lib/features/profile/views/next_of_kin_form_screen.dart (2)

137-159: Consider extracting relationship dropdown data to constants.

The relationship options are hardcoded in the widget. For maintainability, consider moving these to a constants file or fetching them from an API if they might change in the future.

- items: const [
-   {'value': 'SPOUSE', 'label': 'Spouse'},
-   {'value': 'CHILD', 'label': 'Child'},
-   {'value': 'PARENT', 'label': 'Parent'},
-   {'value': 'SIBLING', 'label': 'Sibling'},
-   {'value': 'OTHER', 'label': 'Other'},
- ],
+ items: AppConstants.relationshipOptions,

And in your constants file:

static const List<Map<String, String>> relationshipOptions = [
  {'value': 'SPOUSE', 'label': 'Spouse'},
  {'value': 'CHILD', 'label': 'Child'},
  {'value': 'PARENT', 'label': 'Parent'},
  {'value': 'SIBLING', 'label': 'Sibling'},
  {'value': 'OTHER', 'label': 'Other'},
];

232-257: Consider allowing decimal input for percentage share.

The percentage share field only allows integer input through FilteringTextInputFormatter.digitsOnly, but is validated and stored as a double. Consider allowing decimal input for more precise percentage allocation.

- inputFormatters: [
-   FilteringTextInputFormatter.digitsOnly,
- ],
+ inputFormatters: [
+   FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d*$')),
+ ],
sacco_mobile/lib/features/profile/views/document_viewer.dart (2)

21-33: Download functionality needs implementation.

The download functionality is currently a placeholder with a TODO comment. Consider implementing this feature or creating a follow-up task to track this work.

Would you like me to suggest an implementation for the document download functionality? This could utilize Flutter's path_provider and dio packages to download and save files to the device.


120-156: PDF viewer implementation needed.

The code includes a commented-out implementation for a PDF viewer. Consider either implementing this functionality with the flutter_pdfview package or removing the commented code and creating a separate task for this feature.

Would you like me to provide guidance on implementing the PDF viewer using flutter_pdfview or another recommended package?

sacco_mobile/lib/features/profile/views/edit_profile_screen.dart (1)

176-194: Consider adding currency formatting for better UX.

The monthly income field would benefit from currency formatting to improve readability and user experience.

  // Monthly Income Field
  AppTextField(
    controller: _monthlyIncomeController,
    labelText: 'Monthly Income (UGX)',
    hintText: 'Enter your monthly income',
    keyboardType: TextInputType.number,
    inputFormatters: [
      FilteringTextInputFormatter.digitsOnly,
+     CurrencyTextInputFormatter(
+       locale: 'en_UG',
+       symbol: 'UGX ',
+       decimalDigits: 0,
+     ),
    ],
    validator: (value) {
      if (value == null || value.isEmpty) {
        return 'Please enter your monthly income';
      }
-     if (double.tryParse(value) == null) {
+     if (double.tryParse(value.replaceAll(RegExp(r'[^0-9.]'), '')) == null) {
        return 'Please enter a valid amount';
      }
      return null;
    },
  ),

You'll need to add the currency_text_input_formatter package to your pubspec.yaml:

currency_text_input_formatter: ^2.1.11
sacco_mobile/lib/features/profile/views/document_upload_screen.dart (2)

11-18: Prefer a strongly‑typed enum over string literals for documentType.

Using the raw strings 'ID' / 'PHOTO' ties correctness to spelling and letter‑case.
A small typo later ('Photo') would silently bypass your if in _uploadDocument and break uploads at runtime.

+enum DocumentType { id, photo }

 class DocumentUploadScreen extends StatefulWidget {
-  final String documentType; // 'ID' or 'PHOTO'
+  final DocumentType documentType;
   final String documentTitle;
@@
-    required this.documentType,
+    required this.documentType,

Adapt comparisons:

-if (widget.documentType == 'PHOTO') {
+if (widget.documentType == DocumentType.photo) {

The refactor provides compile‑time safety and IDE autocompletion.


95-115: Button‑enable logic can be simplified and made more robust.

Currently the “Select from Gallery / Take Photo” buttons are only disabled when BOTH _selectedFile != null and state == loading.
During an upload, _selectedFile is non‑null, so it works, but the double condition is unnecessary and slightly brittle.

-onPressed: _selectedFile != null && viewModel.state == ProfileState.loading
+onPressed: viewModel.state == ProfileState.loading

(Same for the camera button.)

This keeps the intent clear: “disable interaction while an upload is running”, regardless of current selection.

Also applies to: 121-127

sacco_mobile/lib/features/profile/views/profile_screen.dart (2)

169-176: Pass a typed DocumentType rather than raw strings when navigating.

As noted on the upload screen, using string literals like 'PHOTO' / 'ID' is error‑prone.
After introducing the DocumentType enum, update navigation:

-documentType: 'PHOTO',
+documentType: DocumentType.photo,

Same for 'ID'. This guarantees compile‑time checking across screens.

Also applies to: 330-367


548-570: Hard‑coding a 120 px label column may break on small screens & RTL locales.

Wide labels or devices in portrait with very small width could overflow, while right‑to‑left languages will mis‑align.
Consider using IntrinsicWidth or Flex with Expanded(flex: …) instead of a fixed pixel width, and wrap the whole row in Directionality to respect RTL.

Not urgent, but improves accessibility and internationalisation.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3938010 and babf7c9.

⛔ Files ignored due to path filters (1)
  • sacco_mobile/pubspec.lock is excluded by !**/*.lock
📒 Files selected for processing (24)
  • sacco_mobile/lib/features/loans/viewmodels/loan_application_viewmodel.dart (1 hunks)
  • sacco_mobile/lib/features/loans/viewmodels/loan_list_viewmodel.dart (1 hunks)
  • sacco_mobile/lib/features/loans/viewmodels/loan_repayment_viewmodel.dart (1 hunks)
  • sacco_mobile/lib/features/loans/views/loan_application_screen.dart (1 hunks)
  • sacco_mobile/lib/features/loans/views/loan_detail_screen.dart (1 hunks)
  • sacco_mobile/lib/features/loans/views/loan_list_screen.dart (1 hunks)
  • sacco_mobile/lib/features/profile/views/document_upload_screen.dart (1 hunks)
  • sacco_mobile/lib/features/profile/views/document_viewer.dart (1 hunks)
  • sacco_mobile/lib/features/profile/views/edit_profile_screen.dart (1 hunks)
  • sacco_mobile/lib/features/profile/views/next_of_kin_form_screen.dart (1 hunks)
  • sacco_mobile/lib/features/profile/views/profile_screen.dart (16 hunks)
  • sacco_mobile/lib/features/savings/viewmodels/savings_account_viewmodel.dart (1 hunks)
  • sacco_mobile/lib/features/savings/viewmodels/transaction_viewmodel.dart (1 hunks)
  • sacco_mobile/lib/features/savings/views/all_transactions.dart (1 hunks)
  • sacco_mobile/lib/features/savings/views/deposit_screen.dart (1 hunks)
  • sacco_mobile/lib/features/savings/views/savings_account_screen.dart (1 hunks)
  • sacco_mobile/lib/features/savings/views/transaction_detail_screen.dart (1 hunks)
  • sacco_mobile/lib/features/savings/views/withdraw_screen.dart (1 hunks)
  • sacco_mobile/linux/flutter/generated_plugin_registrant.cc (1 hunks)
  • sacco_mobile/linux/flutter/generated_plugins.cmake (1 hunks)
  • sacco_mobile/macos/Flutter/GeneratedPluginRegistrant.swift (1 hunks)
  • sacco_mobile/pubspec.yaml (1 hunks)
  • sacco_mobile/windows/flutter/generated_plugin_registrant.cc (1 hunks)
  • sacco_mobile/windows/flutter/generated_plugins.cmake (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: build-and-push
🔇 Additional comments (10)
sacco_mobile/macos/Flutter/GeneratedPluginRegistrant.swift (1)

9-9: Plugin integration for file selection looks good

The addition of the file_selector_macos plugin is correctly implemented with both the import statement and plugin registration. This aligns with the PR objectives to support document upload functionality.

Note that this is an auto-generated file as indicated by the comment at the top, so these changes would typically be generated by Flutter tooling when updating dependencies rather than manually edited.

Also applies to: 15-15

sacco_mobile/linux/flutter/generated_plugins.cmake (1)

6-6: Plugin integration for Linux platform looks good

The file_selector_linux plugin has been correctly added to the Flutter plugin list for Linux. This addition supports the document upload functionality described in the PR objectives.

Note that this is an auto-generated file as indicated by the comment at the top, so these changes would typically be generated by Flutter tooling when updating dependencies.

sacco_mobile/windows/flutter/generated_plugin_registrant.cc (1)

10-10: Windows plugin registration looks good

The file_selector_windows plugin is correctly included and registered for the Windows platform. The implementation follows the same pattern as other plugins in this file.

This change properly supports the document upload functionality mentioned in the PR objectives.

Also applies to: 16-17

sacco_mobile/windows/flutter/generated_plugins.cmake (1)

7-7: Windows plugin configuration looks good

The file_selector_windows plugin has been correctly added to the Flutter plugin list for Windows. This addition ensures proper build integration of the file selector functionality.

This change completes the cross-platform implementation of file selection required for the document upload feature described in the PR objectives.

sacco_mobile/lib/features/savings/viewmodels/transaction_viewmodel.dart (1)

41-62: No support for transaction‑type filter yet.

UI passes a transactionType intention (see screen), but the view‑model and repository lack such parameter. Align the layers to prevent divergent behaviour.

sacco_mobile/pubspec.yaml (1)

43-44:

Details

✅ Verification successful

Dependency additions look good, but consider verifying latest versions.

The addition of photo_view and image_picker packages is appropriate for the new document viewing and image selection functionality. Both packages are popular and well-maintained in the Flutter ecosystem.

Let's verify if these are the most recent stable versions and check for any security advisories:


🏁 Script executed:

#!/bin/bash
# Check for latest versions of the new dependencies

echo "Checking latest versions on pub.dev..."
echo "photo_view current: ^0.15.0"
curl -s "https://pub.dev/api/packages/photo_view" | jq -r '.latest.version'

echo "image_picker current: ^1.1.2"
curl -s "https://pub.dev/api/packages/image_picker" | jq -r '.latest.version'

# Check if these packages have any reported security issues
echo "Checking for security advisories..."
curl -s "https://pub.dev/api/packages/photo_view/advisories" | jq -r '.advisoriesMetadata | length'
curl -s "https://pub.dev/api/packages/image_picker/advisories" | jq -r '.advisoriesMetadata | length'

Length of output: 668


Dependencies are up-to-date – no updates or advisories found

Verified that [email protected] and [email protected] are the latest published versions on pub.dev and no security advisories are reported for either package.

sacco_mobile/lib/features/profile/views/next_of_kin_form_screen.dart (3)

23-78: Form initialization and lifecycle management implemented well.

The state initialization and controller disposal are properly handled, preventing memory leaks. The code correctly populates form fields for edit mode and handles nullability appropriately.


97-104: Good use of loading state with appropriate visual feedback.

The implementation correctly shows a loading indicator during API calls, enhancing user experience by providing clear feedback on the operation's progress.


301-360: Well-implemented reusable dropdown field with proper validation.

The custom dropdown field implementation with validation support is clean and reusable. Good job handling error states and styling consistently.

sacco_mobile/lib/features/profile/views/edit_profile_screen.dart (1)

23-71: Proper initialization and cleanup of controller resources.

The code correctly initializes TextEditingControllers with existing data and properly disposes of them to prevent memory leaks.

Comment on lines +50 to +57
void initState() {
super.initState();
// Initialize with default values if needed
WidgetsBinding.instance.addPostFrameCallback((_) {
final viewModel = context.read<LoanApplicationViewModel>();
viewModel.resetState();
});
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

context.read is invoked before a LoanApplicationViewModel exists in the widget tree

LoanApplicationScreen inserts its ChangeNotifierProvider inside build.
At the moment this initState callback executes, the provider has not yet been inserted, therefore the call to

final viewModel = context.read<LoanApplicationViewModel>();

will throw a ProviderNotFoundException.

Diff showing two possible remedies – pick one:

@@   void initState() {
-    WidgetsBinding.instance.addPostFrameCallback((_) {
-      final viewModel = context.read<LoanApplicationViewModel>();
-      viewModel.resetState();
-    });
+    // Option A – fetch the VM via getIt directly
+    WidgetsBinding.instance.addPostFrameCallback((_) {
+      getIt<LoanApplicationViewModel>().resetState();
+    });
+
+    // Option B – lift the provider *above* this widget (preferred).  
+    // Wrap `LoanApplicationScreen` from the parent with
+    // ChangeNotifierProvider(create: (_) => getIt<LoanApplicationViewModel>(), child: LoanApplicationScreen())
+    // and delete the provider created in `build`.

Comment on lines +70 to +75
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => getIt<LoanApplicationViewModel>(),
child: Consumer<LoanApplicationViewModel>(
builder: (context, viewModel, child) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

ChangeNotifierProvider recreated on every rebuild

Placing the provider inside build means a new ViewModel instance is created whenever Flutter rebuilds the screen, losing form‑state and listeners.
After moving the provider outside (see previous comment) you can simplify to:

return Consumer<LoanApplicationViewModel>(
  builder: (context, viewModel, child) { ... }
);

If you must keep the current structure, wrap the provider with ChangeNotifierProvider.value and pass an already‑created instance so it isn’t re‑instantiated.

Comment on lines +81 to +85
// First check if loan is already in the list
final existingLoan = _loans.firstWhere(
(loan) => loan.id == loanId,
orElse: () => null as Loan,
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

firstWhere fallback will throw at runtime

final existingLoan = _loans.firstWhere(
  (loan) => loan.id == loanId,
  orElse: () => null as Loan,
);

orElse must return a non‑nullable Loan; casting null circumvents the type‑system and produces a TypeError the moment the branch is taken.

Replace with a safe lookup:

- final existingLoan = _loans.firstWhere(
-   (loan) => loan.id == loanId,
-   orElse: () => null as Loan,
- );
+ final existingLoan = _loans.cast<Loan?>()
+     .firstWhere((loan) => loan?.id == loanId, orElse: () => null);

or, with collection package:

final existingLoan = _loans.firstWhereOrNull((loan) => loan.id == loanId);

Then adjust the null‑check below.
This prevents unpredictable crashes when the ID isn’t present.

Comment on lines +33 to +49
@override
void initState() {
super.initState();
// Load loan data when screen loads
WidgetsBinding.instance.addPostFrameCallback((_) async {
final viewModel = context.read<LoanListViewModel>();
if (viewModel.state != LoanListState.success) {
await viewModel.loadLoans();
}
final loan = await viewModel.getLoanById(widget.loanId);
if (mounted) {
setState(() {
_loan = loan;
});
}
});
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

Same provider‑initialisation race as in LoanApplicationScreen

initState tries to access LoanListViewModel via context.read before the ChangeNotifierProvider (created in build) is part of the widget tree, causing ProviderNotFoundException on first launch.

Adopt the same fix: lift the provider above this screen or obtain the instance from getIt inside the callback.

WidgetsBinding.instance.addPostFrameCallback((_) async {
-  final viewModel = context.read<LoanListViewModel>();
+  final viewModel = getIt<LoanListViewModel>();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@override
void initState() {
super.initState();
// Load loan data when screen loads
WidgetsBinding.instance.addPostFrameCallback((_) async {
final viewModel = context.read<LoanListViewModel>();
if (viewModel.state != LoanListState.success) {
await viewModel.loadLoans();
}
final loan = await viewModel.getLoanById(widget.loanId);
if (mounted) {
setState(() {
_loan = loan;
});
}
});
}
@override
void initState() {
super.initState();
// Load loan data when screen loads
WidgetsBinding.instance.addPostFrameCallback((_) async {
final viewModel = getIt<LoanListViewModel>();
if (viewModel.state != LoanListState.success) {
await viewModel.loadLoans();
}
final loan = await viewModel.getLoanById(widget.loanId);
if (mounted) {
setState(() {
_loan = loan;
});
}
});
}

Comment on lines +67 to +92
if (_selectedLoan != null) {
// Calculate new outstanding balance
final newBalance = _selectedLoan!.outstandingBalance - amount;

// Create a new loan object with updated balance
// Note: In a real app, you'd fetch the updated loan from the API
_selectedLoan = Loan(
id: _selectedLoan!.id,
reference: _selectedLoan!.reference,
memberId: _selectedLoan!.memberId,
loanType: _selectedLoan!.loanType,
amount: _selectedLoan!.amount,
interestRate: _selectedLoan!.interestRate,
termMonths: _selectedLoan!.termMonths,
status: _selectedLoan!.status,
applicationDate: _selectedLoan!.applicationDate,
approvalDate: _selectedLoan!.approvalDate,
disbursementDate: _selectedLoan!.disbursementDate,
totalAmountPayable: _selectedLoan!.totalAmountPayable,
totalInterest: _selectedLoan!.totalInterest,
outstandingBalance: newBalance,
nextPaymentDate: _selectedLoan!.nextPaymentDate,
lastPaymentDate: DateTime.now(), // Update last payment date
missedPaymentsCount: _selectedLoan!.missedPaymentsCount,
);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Refactor loan object creation for better maintainability.

The current approach manually copies all properties from the original loan to create an updated version. This is verbose and fragile as any changes to the Loan class would require updates here as well.

Consider adding a copyWith method to the Loan class:

// Add this to the Loan class
Loan copyWith({
  double? outstandingBalance,
  DateTime? lastPaymentDate,
  // other properties that might need updating
}) {
  return Loan(
    id: id,
    reference: reference,
    memberId: memberId,
    loanType: loanType,
    amount: amount,
    interestRate: interestRate,
    termMonths: termMonths,
    status: status,
    applicationDate: applicationDate,
    approvalDate: approvalDate,
    disbursementDate: disbursementDate,
    totalAmountPayable: totalAmountPayable,
    totalInterest: totalInterest,
    outstandingBalance: outstandingBalance ?? this.outstandingBalance,
    nextPaymentDate: nextPaymentDate,
    lastPaymentDate: lastPaymentDate ?? this.lastPaymentDate,
    missedPaymentsCount: missedPaymentsCount,
  );
}

Then simplify this code to:

- _selectedLoan = Loan(
-   id: _selectedLoan!.id,
-   reference: _selectedLoan!.reference,
-   memberId: _selectedLoan!.memberId,
-   loanType: _selectedLoan!.loanType,
-   amount: _selectedLoan!.amount,
-   interestRate: _selectedLoan!.interestRate,
-   termMonths: _selectedLoan!.termMonths,
-   status: _selectedLoan!.status,
-   applicationDate: _selectedLoan!.applicationDate,
-   approvalDate: _selectedLoan!.approvalDate,
-   disbursementDate: _selectedLoan!.disbursementDate,
-   totalAmountPayable: _selectedLoan!.totalAmountPayable,
-   totalInterest: _selectedLoan!.totalInterest,
-   outstandingBalance: newBalance,
-   nextPaymentDate: _selectedLoan!.nextPaymentDate,
-   lastPaymentDate: DateTime.now(), // Update last payment date
-   missedPaymentsCount: _selectedLoan!.missedPaymentsCount,
- );
+ _selectedLoan = _selectedLoan!.copyWith(
+   outstandingBalance: newBalance,
+   lastPaymentDate: DateTime.now(),
+ );

Comment on lines +81 to +118
Widget _buildImageViewer() {
return PhotoView(
imageProvider: NetworkImage(documentUrl),
loadingBuilder: (context, event) => const Center(
child: LoadingIndicator(
message: 'Loading image...',
),
),
errorBuilder: (context, error, stackTrace) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.broken_image,
size: 64,
color: Colors.red,
),
const SizedBox(height: 16),
const Text(
'Failed to load image',
style: TextStyle(fontSize: 16),
),
const SizedBox(height: 8),
Text(
error.toString(),
style: const TextStyle(fontSize: 12, color: Colors.grey),
textAlign: TextAlign.center,
),
],
),
),
backgroundDecoration: const BoxDecoration(
color: Colors.black,
),
minScale: PhotoViewComputedScale.contained,
maxScale: PhotoViewComputedScale.covered * 2,
);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Error handling exposes raw error message to users.

The error handling in the image viewer displays the raw error message to users, which could expose implementation details and isn't user-friendly.

- Text(
-   error.toString(),
-   style: const TextStyle(fontSize: 12, color: Colors.grey),
-   textAlign: TextAlign.center,
- ),
+ const Text(
+   'Unable to load the image. Please try again later.',
+   style: TextStyle(fontSize: 12, color: Colors.grey),
+   textAlign: TextAlign.center,
+ ),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Widget _buildImageViewer() {
return PhotoView(
imageProvider: NetworkImage(documentUrl),
loadingBuilder: (context, event) => const Center(
child: LoadingIndicator(
message: 'Loading image...',
),
),
errorBuilder: (context, error, stackTrace) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.broken_image,
size: 64,
color: Colors.red,
),
const SizedBox(height: 16),
const Text(
'Failed to load image',
style: TextStyle(fontSize: 16),
),
const SizedBox(height: 8),
Text(
error.toString(),
style: const TextStyle(fontSize: 12, color: Colors.grey),
textAlign: TextAlign.center,
),
],
),
),
backgroundDecoration: const BoxDecoration(
color: Colors.black,
),
minScale: PhotoViewComputedScale.contained,
maxScale: PhotoViewComputedScale.covered * 2,
);
}
Widget _buildImageViewer() {
return PhotoView(
imageProvider: NetworkImage(documentUrl),
loadingBuilder: (context, event) => const Center(
child: LoadingIndicator(
message: 'Loading image...',
),
),
errorBuilder: (context, error, stackTrace) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.broken_image,
size: 64,
color: Colors.red,
),
const SizedBox(height: 16),
const Text(
'Failed to load image',
style: TextStyle(fontSize: 16),
),
const SizedBox(height: 8),
const Text(
'Unable to load the image. Please try again later.',
style: TextStyle(fontSize: 12, color: Colors.grey),
textAlign: TextAlign.center,
),
],
),
),
backgroundDecoration: const BoxDecoration(
color: Colors.black,
),
minScale: PhotoViewComputedScale.contained,
maxScale: PhotoViewComputedScale.covered * 2,
);
}

Comment on lines +341 to +369
Future<void> _saveProfile() async {
if (_formKey.currentState?.validate() ?? false) {
// Get form values
final profileData = {
'marital_status': _selectedMaritalStatus,
'employment_status': _selectedEmploymentStatus,
'occupation': _occupationController.text,
'monthly_income': double.parse(_monthlyIncomeController.text),
'physical_address': _physicalAddressController.text,
'postal_address': _postalAddressController.text,
'city': _cityController.text,
'district': _districtController.text,
};

// Save profile
final viewModel = context.read<ProfileViewModel>();
final success = await viewModel.updateMemberProfile(profileData);

if (success && mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Profile updated successfully'),
backgroundColor: Colors.green,
),
);
Navigator.of(context).pop();
}
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Ensure proper validation checks for dropdown fields in form submission.

The code doesn't verify if the dropdown values are selected before submitting the form. Update the _saveProfile method to check these values.

  Future<void> _saveProfile() async {
    if (_formKey.currentState?.validate() ?? false) {
+     // Verify dropdown selections
+     if (_selectedMaritalStatus == null || _selectedEmploymentStatus == null) {
+       ScaffoldMessenger.of(context).showSnackBar(
+         const SnackBar(
+           content: Text('Please select both marital status and employment status'),
+           backgroundColor: Colors.red,
+         ),
+       );
+       return;
+     }
      
      // Get form values
      final profileData = {
        'marital_status': _selectedMaritalStatus,
        'employment_status': _selectedEmploymentStatus,
        'occupation': _occupationController.text,
        'monthly_income': double.parse(_monthlyIncomeController.text),
        'physical_address': _physicalAddressController.text,
        'postal_address': _postalAddressController.text,
        'city': _cityController.text,
        'district': _districtController.text,
      };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Future<void> _saveProfile() async {
if (_formKey.currentState?.validate() ?? false) {
// Get form values
final profileData = {
'marital_status': _selectedMaritalStatus,
'employment_status': _selectedEmploymentStatus,
'occupation': _occupationController.text,
'monthly_income': double.parse(_monthlyIncomeController.text),
'physical_address': _physicalAddressController.text,
'postal_address': _postalAddressController.text,
'city': _cityController.text,
'district': _districtController.text,
};
// Save profile
final viewModel = context.read<ProfileViewModel>();
final success = await viewModel.updateMemberProfile(profileData);
if (success && mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Profile updated successfully'),
backgroundColor: Colors.green,
),
);
Navigator.of(context).pop();
}
}
}
Future<void> _saveProfile() async {
if (_formKey.currentState?.validate() ?? false) {
// Verify dropdown selections
if (_selectedMaritalStatus == null || _selectedEmploymentStatus == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Please select both marital status and employment status'),
backgroundColor: Colors.red,
),
);
return;
}
// Get form values
final profileData = {
'marital_status': _selectedMaritalStatus,
'employment_status': _selectedEmploymentStatus,
'occupation': _occupationController.text,
'monthly_income': double.parse(_monthlyIncomeController.text),
'physical_address': _physicalAddressController.text,
'postal_address': _postalAddressController.text,
'city': _cityController.text,
'district': _districtController.text,
};
// Save profile
final viewModel = context.read<ProfileViewModel>();
final success = await viewModel.updateMemberProfile(profileData);
if (success && mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Profile updated successfully'),
backgroundColor: Colors.green,
),
);
Navigator.of(context).pop();
}
}
}

Comment on lines +300 to +339
Widget _buildDropdownField({
required String label,
required String? value,
required List<Map<String, String>> items,
required Function(String?) onChanged,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
fontSize: 14,
color: Colors.grey[700],
),
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey[400]!),
borderRadius: BorderRadius.circular(8),
),
child: DropdownButton<String>(
value: value,
isExpanded: true,
underline: const SizedBox(),
hint: Text('Select $label'),
items: items.map((item) {
return DropdownMenuItem<String>(
value: item['value'],
child: Text(item['label']!),
);
}).toList(),
onChanged: onChanged,
),
),
],
);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add validation support to dropdown fields.

Unlike the next of kin form screen, the dropdown fields here don't support validation. Consider adding validation to ensure these fields are not left empty.

  Widget _buildDropdownField({
    required String label,
    required String? value,
    required List<Map<String, String>> items,
    required Function(String?) onChanged,
+   String? Function(String?)? validator,
  }) {
-   return Column(
+   return FormField<String>(
+     initialValue: value,
+     validator: validator ?? (val) => val == null || val.isEmpty ? 'Please select $label' : null,
+     builder: (FormFieldState<String> state) {
+       return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          label,
          style: TextStyle(
            fontSize: 14,
            color: Colors.grey[700],
          ),
        ),
        const SizedBox(height: 8),
        Container(
          padding: const EdgeInsets.symmetric(horizontal: 12),
          decoration: BoxDecoration(
-           border: Border.all(color: Colors.grey[400]!),
+           border: Border.all(color: state.hasError ? Colors.red[300]! : Colors.grey[400]!),
            borderRadius: BorderRadius.circular(8),
          ),
          child: DropdownButton<String>(
            value: value,
            isExpanded: true,
            underline: const SizedBox(),
            hint: Text('Select $label'),
            items: items.map((item) {
              return DropdownMenuItem<String>(
                value: item['value'],
                child: Text(item['label']!),
              );
            }).toList(),
-           onChanged: onChanged,
+           onChanged: (newValue) {
+             onChanged(newValue);
+             state.didChange(newValue);
+           },
          ),
        ),
+       if (state.hasError)
+         Padding(
+           padding: const EdgeInsets.only(top: 8.0, left: 12.0),
+           child: Text(
+             state.errorText!,
+             style: TextStyle(color: Colors.red[700], fontSize: 12),
+           ),
+         ),
      ],
-   );
+     );
+   });
  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Widget _buildDropdownField({
required String label,
required String? value,
required List<Map<String, String>> items,
required Function(String?) onChanged,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
fontSize: 14,
color: Colors.grey[700],
),
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey[400]!),
borderRadius: BorderRadius.circular(8),
),
child: DropdownButton<String>(
value: value,
isExpanded: true,
underline: const SizedBox(),
hint: Text('Select $label'),
items: items.map((item) {
return DropdownMenuItem<String>(
value: item['value'],
child: Text(item['label']!),
);
}).toList(),
onChanged: onChanged,
),
),
],
);
}
Widget _buildDropdownField({
required String label,
required String? value,
required List<Map<String, String>> items,
required Function(String?) onChanged,
String? Function(String?)? validator,
}) {
return FormField<String>(
initialValue: value,
validator: validator ?? (val) => val == null || val.isEmpty ? 'Please select $label' : null,
builder: (FormFieldState<String> state) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
fontSize: 14,
color: Colors.grey[700],
),
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
border: Border.all(
color: state.hasError ? Colors.red[300]! : Colors.grey[400]!,
),
borderRadius: BorderRadius.circular(8),
),
child: DropdownButton<String>(
value: value,
isExpanded: true,
underline: const SizedBox(),
hint: Text('Select $label'),
items: items.map((item) {
return DropdownMenuItem<String>(
value: item['value'],
child: Text(item['label']!),
);
}).toList(),
onChanged: (newValue) {
onChanged(newValue);
state.didChange(newValue);
},
),
),
if (state.hasError)
Padding(
padding: const EdgeInsets.only(top: 8.0, left: 12.0),
child: Text(
state.errorText!,
style: TextStyle(color: Colors.red[700], fontSize: 12),
),
),
],
);
},
);
}

Comment on lines +30 to +43
return ChangeNotifierProvider(
create: (_) => getIt<ProfileViewModel>(),
child: Consumer<ProfileViewModel>(
builder: (context, viewModel, child) {
return Scaffold(
appBar: AppBar(
title: Text('Upload ${widget.documentTitle}'),
),
body: _buildBody(viewModel),
);
},
),
);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

ChangeNotifierProvider is recreated on every rebuild – cache the instance.

Placing ChangeNotifierProvider inside build means a brand‑new ProfileViewModel is instantiated any time Flutter decides to rebuild the widget tree (theme change, orientation change, setState, etc.).
The user could lose progress or see the loading spinner reappear mid‑interaction.

A minimal fix:

-class _DocumentUploadScreenState extends State<DocumentUploadScreen> {
+class _DocumentUploadScreenState extends State<DocumentUploadScreen> {
+  late final ProfileViewModel _viewModel = getIt<ProfileViewModel>();
@@
-    return ChangeNotifierProvider(
-      create: (_) => getIt<ProfileViewModel>(),
+    return ChangeNotifierProvider.value(
+      value: _viewModel,

This keeps one ViewModel for the lifetime of the screen, improving UX and avoiding unintended network calls.

Comment on lines +248 to +268
Future<void> _uploadDocument(ProfileViewModel viewModel) async {
if (_selectedFile == null) return;

bool success;

// Upload based on document type
if (widget.documentType == 'PHOTO') {
success = await viewModel.uploadProfilePhoto(_selectedFile!);
} else {
success = await viewModel.uploadIdDocument(_selectedFile!);
}

if (success && mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('${widget.documentTitle} uploaded successfully'),
backgroundColor: Colors.green,
),
);
Navigator.of(context).pop();
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Handle failed uploads – surface feedback to the user.

If success is false, the UI stays on the same screen but gives no indication of what went wrong.
Consider:

  1. Clearing _selectedFile only on success (so the user can retry).
  2. Showing a SnackBar or displaying viewModel.errorMessage.
  3. Resetting focus or scrolling to the error.

Example patch:

-if (success && mounted) {
+if (success && mounted) {
   ScaffoldMessenger.of(context).showSnackBar(
@@
   Navigator.of(context).pop();
+} else if (mounted) {
+  ScaffoldMessenger.of(context).showSnackBar(
+    SnackBar(
+      content: Text(viewModel.errorMessage ?? 'Upload failed'),
+      backgroundColor: Colors.red,
+    ),
+  );
 }

Better error visibility improves recoverability and user trust.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Future<void> _uploadDocument(ProfileViewModel viewModel) async {
if (_selectedFile == null) return;
bool success;
// Upload based on document type
if (widget.documentType == 'PHOTO') {
success = await viewModel.uploadProfilePhoto(_selectedFile!);
} else {
success = await viewModel.uploadIdDocument(_selectedFile!);
}
if (success && mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('${widget.documentTitle} uploaded successfully'),
backgroundColor: Colors.green,
),
);
Navigator.of(context).pop();
}
Future<void> _uploadDocument(ProfileViewModel viewModel) async {
if (_selectedFile == null) return;
bool success;
// Upload based on document type
if (widget.documentType == 'PHOTO') {
success = await viewModel.uploadProfilePhoto(_selectedFile!);
} else {
success = await viewModel.uploadIdDocument(_selectedFile!);
}
if (success && mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('${widget.documentTitle} uploaded successfully'),
backgroundColor: Colors.green,
),
);
Navigator.of(context).pop();
} else if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(viewModel.errorMessage ?? 'Upload failed'),
backgroundColor: Colors.red,
),
);
}
}

@kallyas
Copy link
Owner Author

kallyas commented Apr 17, 2025

@coderabbitai create a PR with suggested changes

@coderabbitai
Copy link

coderabbitai bot commented Apr 17, 2025

Tip

For best results, initiate chat on the files or code changes.

@kallyas I'd be happy to create a PR with suggested improvements for your sacco_mobile application. Based on the code I've reviewed, I'll plan out some enhancements that could improve documentation, error handling, and user experience.

graph TB
    A["Sacco Mobile Improvements"]
    B["Documentation"]
    C["Error Handling"]
    D["User Experience"]
    E["Code Optimization"]

    A --> B
    A --> C
    A --> D
    A --> E

    B1["Add docstrings to viewmodels"]
    B2["Create README for new features"]
    B --> B1
    B --> B2

    C1["Enhance loan application error feedback"]
    C2["Improve transaction error handling"]
    C --> C1
    C --> C2

    D1["Add loading indicators"]
    D2["Enhance form validation feedback"]
    D --> D1
    D --> D2

    E1["Refactor duplicate code"]
    E2["Optimize UI rebuilds"]
    E --> E1
    E --> E2
Loading

Another agent will come along to implement these changes for you in a new pull request. The plan includes:

  1. Documentation improvements: Adding comprehensive docstrings to the viewmodels and creating a README for the loan features to help users and developers understand the functionality.

  2. Enhanced error handling: Improving error feedback in the loan application and transaction processes with user-friendly messages and better recovery options.

  3. User experience upgrades: Adding better loading indicators to the document upload process and enhancing form validation with real-time feedback.

  4. Code optimizations: Refactoring duplicate code into a shared utility class and optimizing UI rebuilds to improve app performance.

Would you like me to modify this plan or focus on particular areas of improvement?


⏳ Preparing changes...

@coderabbitai
Copy link

coderabbitai bot commented Apr 17, 2025

A summary of the edits CodeRabbit can apply:

We will update sacco_mobile/lib/features/loans/viewmodels/loan_application_viewmodel.dart to provide formatted, user‑friendly error messages in submitLoanApplication.

We will insert a private helper method in sacco_mobile/lib/features/loans/viewmodels/loan_application_viewmodel.dart to format AppError into user‑friendly messages.

Will add a new README.md in sacco_mobile/lib/features/loans describing the loan management features.

Will enhance error feedback in loan_application_viewmodel.dart by formatting AppError codes to user‐friendly messages.

Will improve the submission error UI in loan_application_screen.dart to use an icon, border, and contextual guidance.

Will improve form validation feedback in loan_application_screen.dart with real-time validation and error mapping.

Will enhance transaction error handling in savings_account_viewmodel.dart with optimistic updates and detailed error messages.

Will improve loading indicators in document_upload_screen.dart by disabling controls during upload and adding AnimatedSwitcher and overlays.

Will refactor duplicate formatting code by introducing TransactionUtils and updating DepositScreen to use it.

Will update DepositScreen in sacco_mobile/lib/features/savings/views/deposit_screen.dart to import and use TransactionUtils.

Will optimize UI rebuilds in loan_list_screen.dart by using Builder and granular Consumers around the refresh button and body.

Will create the Loan Management README at sacco_mobile/lib/features/loans/README.md

Will enhance error handling in loan_application_viewmodel.dart by formatting messages and adding a helper method.

Will improve error UI and form validation in loan_application_screen.dart.

Will enhance transaction error handling and add optimistic update in savings_account_viewmodel.dart.

Will introduce animated loading indicators and disable UI during upload in document_upload_screen.dart.

Will add the TransactionUtils utility class under core/utils.

Will replace inline currency formatting with TransactionUtils in deposit_screen.dart.

Will optimize UI rebuilds in loan_list_screen.dart by using granular Consumers and IndexedStack.

LoanApplicationScreen.dart will be updated to return detailed field‑specific errors and use inline validation helpers.

LoanApplicationScreen.dart will include two new helper widgets for inline field error display.

I will refactor loan_detail_screen.dart to use TransactionUtils for currency formatting and remove the manual Intl import and field.

We will optimize UI rebuilds in sacco_mobile/lib/features/loans/views/loan_list_screen.dart by replacing the broad Consumer with a Builder and targeted Consumer widgets for the refresh button and body.

A README for the loan management features will be created at sacco_mobile/lib/features/loans/README.md.

Error handling in submitLoanApplication will be improved in loan_application_viewmodel.dart.

The error display UI will be enhanced in loan_application_screen.dart.

Transaction error handling will be enhanced in savings_account_viewmodel.dart.

Loading indicators and disabled states will be improved in document_upload_screen.dart.

Form validation feedback will be converted to inline errors in loan_application_screen.dart.

A shared TransactionUtils helper will be created at core/utils/transaction_utils.dart.

DepositScreen will use TransactionUtils.formatCurrency instead of inline NumberFormat.

UI rebuilds in LoanListScreen will be optimized by using granular Consumers.

We will create a new README.md for loan management features under sacco_mobile/lib/features/loans.

We will enhance error feedback handling in loan_application_viewmodel.dart.

We will update the error display UI in loan_application_screen.dart to improve styling and guidance.

We will enhance SavingsAccountViewModel.makeDeposit with optimistic update and user‑friendly error messages.

We will improve loading indicators and button states in document_upload_screen.dart.

We will improve form validation feedback in loan_application_screen.dart.

We will add a shared utility class TransactionUtils in core/utils for formatting and validating transactions.

We will refactor deposit_screen.dart to use TransactionUtils for currency formatting and validation.

We will optimize UI rebuilds in loan_list_screen.dart using granular Consumers and an IndexedStack.

Will create a README.md for the loan management features in sacco_mobile/lib/features/loans.

Will enhance error handling in loan_application_viewmodel.dart by formatting error messages.

Will enhance error display in loan_application_screen.dart with icon, border, and guidance text.

Will improve form validation feedback in loan_application_screen.dart by returning a map of errors and adding inline error widgets.

Will enhance transaction error handling and add optimistic UI updates in savings_account_viewmodel.dart.

Will enhance loading indicators and disable controls during upload in document_upload_screen.dart.

Will add a new TransactionUtils utility class under core/utils for formatting and validation.

Will refactor deposit_screen.dart to use TransactionUtils.formatCurrency and validateAmount helper.

Will optimize UI rebuilds in loan_list_screen.dart by scoping Consumer widgets.

A new README file will be created in sacco_mobile/lib/features/loans to document loan management features.

LoanApplicationViewModel will be updated to format error messages more user-friendly and handle generic exceptions.

LoanApplicationScreen's submission error UI will be updated to a styled container with icon and guidance.

SavingsAccountViewModel will be enhanced with optimistic UI update and detailed transaction error messages.

DocumentUploadScreen will be updated with AnimatedSwitcher loading button and overlay on image preview during upload.

Loan application form validation will be updated to return error maps and use inline field validation.

A new TransactionUtils class will be created under sacco_mobile/lib/features/core/utils for shared transaction formatting and validation.

DepositScreen will be refactored to use TransactionUtils for currency formatting and validation.

LoanListScreen will be optimized to reduce unnecessary rebuilds by splitting Consumers and using IndexedStack.

A plan of each step that CodeRabbit will take in order to execute the edits for each file (with inline rough draft snippets):

✏️ sacco_mobile/lib/features/loans/viewmodels/loan_application_viewmodel.dart

Replace the AppError catch block so that errorMessage uses the new helper. Change lines 107–111 from:

} on AppError catch (e) {
  _state = LoanApplicationState.error;
  _errorMessage = e.userFriendlyMessage;
  notifyListeners();
  return false;
}

to:

} on AppError catch (e) {
  _state = LoanApplicationState.error;
  _errorMessage = _formatErrorMessage(e);
  notifyListeners();
  return false;
}

Update the generic catch block on lines 112–116. Change:

} catch (e) {
  _state = LoanApplicationState.error;
  _errorMessage = 'An unexpected error occurred. Please try again.';
  notifyListeners();
  return false;
}

to:

} catch (e) {
  _state = LoanApplicationState.error;
  _errorMessage = 'We couldn\'t process your loan application right now. Please check your connection and try again in a few moments.';
  notifyListeners();
  return false;
}
➕ sacco_mobile/lib/features/loans/viewmodels/loan_application_viewmodel.dart

Immediately before the final closing brace of the class (after resetState), insert:

  /// Formats error messages to be more user-friendly based on error type
  String _formatErrorMessage(AppError error) {
    switch (error.code) {
      case 'validation_error':
        return 'Please check your application details: ${error.message}';
      case 'insufficient_income':
        return 'Your application was declined due to insufficient income. The minimum required monthly income for this loan type is ${error.details?['required_income'] ?? 'higher than provided'}.';
      case 'existing_loans':
        return 'You have existing loans that need to be paid down before applying for a new loan.';
      case 'server_error':
        return 'Our system is currently unavailable. Please try again later.';
      case 'network_error':
        return 'Please check your internet connection and try again.';
      default:
        return error.message;
    }
  }
➕ sacco_mobile/lib/features/loans/README.md

Create a new file named README.md under sacco_mobile/lib/features/loans with the following content:

# Loan Management Features

This module provides comprehensive loan management capabilities for the Sacco Mobile application, including loan application, loan listing, and loan repayment.

## Features

### Loan Application
- Multi-step loan application form
- Different loan types with varying terms and interest rates
- Instant calculation of monthly payments and total interest
- Loan purpose, collateral, and employment information collection

### Loan Listing
- View all loans with filtering options
- Active loans tab for quick access to current loans
- Loan history to track past applications
- Detailed loan status with progress indicators

### Loan Details
- Comprehensive loan information display
- Payment schedule and outstanding balance
- Due date alerts and missed payment notifications
- Quick access to make payments

### Loan Repayment
- Simple payment submission process
- Multiple payment method options
- Transaction reference tracking
- Penalty calculation for late payments

## How to Use

1. **Apply for a Loan**:
   - Navigate to the Loan screen
   - Tap on "Apply for Loan" button
   - Follow the multi-step application process
   - Review and submit your application

2. **View Your Loans**:
   - Navigate to the Loan screen
   - Browse the tabs to see Active, All, or History
   - Tap on any loan to view details

3. **Make a Loan Repayment**:
   - Open a loan from the loan list
   - Tap on "Make Payment" button
   - Enter payment amount and select payment method
   - Confirm payment details

4. **Track Loan Status**:
   - Use progress bars on loan cards to see repayment progress
   - Check payment due dates and outstanding amounts
   - View complete loan activity timeline in loan details

## Technical Implementation

The loan features use a ViewModel pattern to separate business logic from UI:
- LoanApplicationViewModel: Manages loan application state and submission
- LoanListViewModel: Handles loan listings and filtering
- LoanRepaymentViewModel: Controls loan repayment process

These view models connect to the LoanRepository which handles data operations with the backend services.
✏️ sacco_mobile/lib/features/loans/viewmodels/loan_application_viewmodel.dart

Open the file and locate the submitLoanApplication method. In the AppError catch block, replace _errorMessage = e.message; with

_errorMessage = _formatErrorMessage(e);

Then in the generic catch, replace the hardcoded message with:

_errorMessage = 'We couldn\'t process your loan application right now. Please check your connection and try again in a few moments.';

Immediately after the closing brace of submitLoanApplication, insert the helper method to map error codes to custom messages:

/// Formats error messages to be more user-friendly based on error type
String _formatErrorMessage(AppError error) {
  switch (error.code) {
    case 'validation_error':
      return 'Please check your application details: ${error.message}';
    case 'insufficient_income':
      return 'Your application was declined due to insufficient income. The minimum required monthly income for this loan type is ${error.details?['required_income'] ?? 'higher than provided'}.';
    case 'existing_loans':
      return 'You have existing loans that need to be paid down before applying for a new loan.';
    case 'server_error':
      return 'Our system is currently unavailable. Please try again later.';
    case 'network_error':
      return 'Please check your internet connection and try again.';
    default:
      return error.message;
  }
}
✏️ sacco_mobile/lib/features/loans/views/loan_application_screen.dart

Find the _buildSubmissionError widget. In its Container decoration, change the color and add a border:

decoration: BoxDecoration(
  color: Colors.red[50],
  borderRadius: BorderRadius.circular(8),
  border: Border.all(color: Colors.red[300]!),
),

Replace the plain Text widgets with a Row including an error icon and heading, followed by the error message and conditional guidance:

Row(
  children: [
    Icon(Icons.error_outline, color: Colors.red[700], size: 18),
    SizedBox(width: 8),
    Text(
      'Application Error',
      style: TextStyle(
        fontWeight: FontWeight.bold,
        color: Colors.red[900],
      ),
    ),
  ],
),
SizedBox(height: 8),
Text(
  errorMessage,
  style: TextStyle(color: Colors.red[900]),
),
SizedBox(height: 8),
if (errorMessage.contains('income') || errorMessage.contains('existing loans'))
  Text(
    'Consider speaking with a Sacco representative for guidance.',
    style: TextStyle(
      fontStyle: FontStyle.italic,
      fontSize: 12,
      color: Colors.red[800],
    ),
),
✏️ sacco_mobile/lib/features/loans/views/loan_application_screen.dart

Convert the existing _validateForm method to return a Map<String, String?> of field errors. At the top of the file, replace:

bool _validateForm() { … }

with

Map<String, String?> _validateForm() {
  final errors = <String, String?>{};
  // existing switch blocks, replacing each ScaffoldMessenger call with errors['fieldKey'] = 'message';
  // At end:
  if (errors.isNotEmpty) {
    final firstError = errors.values.firstWhere((e) => e != null)!;
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text(firstError)),
    );
  }
  return errors;
}

Update _nextPage to handle the new return type:

final errors = _validateForm();
if (errors.isEmpty) {
  // proceed to next page or submit
}

Add helper widgets at the bottom of the file for inline field error display:

Widget _buildTextField({ … String? error, … }) { … }
Widget _buildDropdown<T>({ … String? error, … }) { … }

Use these in place of existing TextField and DropdownButtonFormField usages to bind errorText to each field.


</details>

<details>
<summary>✏️ sacco_mobile/lib/features/savings/viewmodels/savings_account_viewmodel.dart</summary>

In `makeDeposit`, at the top, if `_savingsAccount` is null, set:  
```dart
_state = SavingsAccountState.error;
_errorMessage = 'Please load your savings account first.';
notifyListeners();
return false;

After the repository call succeeds, add an optimistic update:

final updated = _savingsAccount!.copyWith(
  balance: _savingsAccount!.balance + amount,
  lastTransactionDate: DateTime.now(),
);
_savingsAccount = updated;
notifyListeners();

before calling loadSavingsAccount();


In the on AppError catch block, replace _errorMessage = e.message; with:

_errorMessage = _getTransactionErrorMessage(e, isDeposit: true);

and in the generic catch, update to:

_errorMessage = 'Your deposit couldn\'t be processed. Please check your connection and try again. If the issue persists, contact support.';

Below makeDeposit, insert the helper method:

/// Returns user-friendly error messages for transaction errors
String _getTransactionErrorMessage(AppError error, {bool isDeposit = true}) {
  final type = isDeposit ? 'deposit' : 'withdrawal';
  switch (error.code) {
    case 'network_error':
      return 'Network issue: your $type may have succeeded but we couldn\'t confirm. Please check your balance before retrying.';
    case 'insufficient_funds':
      return 'Insufficient funds for this withdrawal.';
    case 'invalid_amount':
      return 'The amount entered is invalid. Please check and try again.';
    case 'account_locked':
      return 'Your account is locked. Please contact support.';
    case 'service_unavailable':
      return 'Service unavailable. Please try again later.';
    case 'duplicate_transaction':
      return 'Possible duplicate $type detected. Check recent transactions.';
    default:
      return error.message;
  }
}
✏️ sacco_mobile/lib/features/profile/views/document_upload_screen.dart

Update the file preview call to pass isUploading:

_selectedFile != null 
  ? _buildFilePreview(isUploading) 
  : _buildEmptyPreview()

Wrap the upload button’s child in AnimatedSwitcher and disable onPressed when isUploading:

child: AnimatedSwitcher(
  duration: Duration(milliseconds: 300),
  child: isUploading
    ? Row(
        key: ValueKey('loading'),
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Container(
            width: 18,
            height: 18,
            child: CircularProgressIndicator(
              strokeWidth: 2.5,
              valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
            ),
          ),
          SizedBox(width: 12),
          Text('Uploading... Please wait'),
        ],
      )
    : Row(
        key: ValueKey('upload'),
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Icon(Icons.upload_file),
          SizedBox(width: 8),
          Text('Upload ${widget.documentTitle}'),
        ],
      ),
),
onPressed: _selectedFile == null || isUploading 
  ? null 
  : () => _uploadDocument(viewModel),

Extend _buildFilePreview to accept bool isUploading, dim the preview with Opacity when uploading, disable the close icon’s tap, and overlay a full-screen “Uploading…” indicator:

Widget _buildFilePreview(bool isUploading) {
  return Stack(
    children: [
      Opacity(
        opacity: isUploading ? 0.7 : 1.0,
        child: /* existing Image preview code */,
      ),
      Positioned(
        top: 8,
        right: 8,
        child: InkWell(
          onTap: isUploading ? null : () => setState(() => _selectedFile = null),
          child: Opacity(
            opacity: isUploading ? 0.5 : 1.0,
            child: Container(/* close icon container */),
          ),
        ),
      ),
      if (isUploading)
        Positioned.fill(
          child: Center(
            child: Container(
              padding: EdgeInsets.symmetric(vertical: 12, horizontal: 20),
              decoration: BoxDecoration(color: Colors.black54, borderRadius: BorderRadius.circular(8)),
              child: Column(mainAxisSize: MainAxisSize.min, children: [
                SizedBox(
                  width: 28,
                  height: 28,
                  child: CircularProgressIndicator(strokeWidth: 3, valueColor: AlwaysStoppedAnimation<Color>(Colors.white)),
                ),
                SizedBox(height: 12),
                Text('Uploading...', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
              ]),
            ),
          ),
        ),
    ],
  );
}
✏️ sacco_mobile/lib/features/core/utils/transaction_utils.dart

Create the file transaction_utils.dart under sacco_mobile/lib/features/core/utils/ with exactly the following content:

import 'package:intl/intl.dart';

/// Utility class for formatting and validating transaction-related data
class TransactionUtils {
  static final NumberFormat currencyFormatter = NumberFormat.currency(
    locale: 'en_UG',
    symbol: 'UGX ',
    decimalDigits: 0,
  );
  static final DateFormat dateFormatter = DateFormat('dd MMM yyyy');
  static final DateFormat timeFormatter = DateFormat('HH:mm');
  static final DateFormat dateTimeFormatter = DateFormat('dd MMM yyyy, HH:mm');

  static String formatCurrency(double amount) => currencyFormatter.format(amount);
  static String formatDate(DateTime date) => dateFormatter.format(date);
  static String formatTime(DateTime time) => timeFormatter.format(time);
  static String formatDateTime(DateTime dateTime) => dateTimeFormatter.format(dateTime);

  static int daysBetween(DateTime from, DateTime to) {
    from = DateTime(from.year, from.month, from.day);
    to = DateTime(to.year, to.month, to.day);
    return (to.difference(from).inHours / 24).round();
  }
  static bool isPaymentDueSoon(DateTime dueDate) {
    final now = DateTime.now();
    final days = daysBetween(now, dueDate);
    return days >= 0 && days <= 3;
  }
  static bool isPaymentOverdue(DateTime dueDate) => DateTime.now().isAfter(dueDate);

  static int getAmountColor(String transactionType, {bool isDark = false}) {
    final type = transactionType.toLowerCase();
    if (type.contains('deposit') || type.contains('credit')) {
      return isDark ? 0xFF81C784 : 0xFF388E3C;
    } else if (type.contains('withdrawal') || type.contains('debit')) {
      return isDark ? 0xFFE57373 : 0xFFD32F2F;
    }
    return isDark ? 0xFFFFB74D : 0xFFF57C00;
  }

  static String? validateAmount(String amount, double maxAmount) {
    if (amount.isEmpty) return 'Amount is required';
    final parsed = double.tryParse(amount.replaceAll(',', ''));
    if (parsed == null) return 'Please enter a valid amount';
    if (parsed <= 0) return 'Amount must be greater than zero';
    if (parsed > maxAmount) return 'Amount cannot exceed ${formatCurrency(maxAmount)}';
    return null;
  }
}
✏️ sacco_mobile/lib/features/savings/views/deposit_screen.dart

Add at the top:

import '../../../core/utils/transaction_utils.dart';

In the _buildBody method, replace any NumberFormat.currency(...) use with:

TransactionUtils.formatCurrency(viewModel.savingsAccount?.balance ?? 0)

for the current balance display.


Replace the local _validateAmount implementation with:

String? _validateAmount(String value) {
  return TransactionUtils.validateAmount(value, double.infinity);
}
✏️ sacco_mobile/lib/features/loans/views/loan_list_screen.dart

Replace the top-level Consumer<LoanListViewModel> child with a Builder that retrieves the viewModel with listen: false. Wrap the refresh IconButton in its own Consumer<LoanListViewModel> to rebuild only the button when state changes.


Wrap the body in a separate Consumer<LoanListViewModel> so only _buildBody(viewModel) re-renders. Replace the TabBarView with an IndexedStack and Visibility widgets keyed to _tabController.index. In initState, add listeners to _tabController to call setState on index changes and trigger viewModel.loadLoans() in a post-frame callback.

➕ sacco_mobile/lib/features/loans/README.md

Insert the following content into README.md:

# Loan Management Features

This module provides comprehensive loan management capabilities for the Sacco Mobile application, including loan application, loan listing, and loan repayment.

## Features

### Loan Application
- Multi-step loan application form
- Different loan types with varying terms and interest rates
- Instant calculation of monthly payments and total interest
- Loan purpose, collateral, and employment information collection

### Loan Listing
- View all loans with filtering options
- Active loans tab for quick access to current loans
- Loan history to track past applications
- Detailed loan status with progress indicators

### Loan Details
- Comprehensive loan information display
- Payment schedule and outstanding balance
- Due date alerts and missed payment notifications
- Quick access to make payments

### Loan Repayment
- Simple payment submission process
- Multiple payment method options
- Transaction reference tracking
- Penalty calculation for late payments

## How to Use

1. **Apply for a Loan**  
   - Navigate to the Loan screen  
   - Tap on "Apply for Loan" button  
   - Follow the multi-step application process  
   - Review and submit your application  

2. **View Your Loans**  
   - Navigate to the Loan screen  
   - Browse the tabs to see Active, All, or History  
   - Tap on any loan to view details  

3. **Make a Loan Repayment**  
   - Open a loan from the loan list  
   - Tap on "Make Payment" button  
   - Enter payment amount and select payment method  
   - Confirm payment details  

4. **Track Loan Status**  
   - Use progress bars on loan cards to see repayment progress  
   - Check payment due dates and outstanding amounts  
   - View complete loan activity timeline in loan details  

## Technical Implementation

The loan features use a ViewModel pattern to separate business logic from UI:
- LoanApplicationViewModel: Manages loan application state and submission  
- LoanListViewModel: Handles loan listings and filtering  
- LoanRepaymentViewModel: Controls loan repayment process  

These view models connect to the LoanRepository which handles data operations with the backend services.
✏️ sacco_mobile/lib/features/loans/viewmodels/loan_application_viewmodel.dart

Replace the AppError catch and general catch blocks in submitLoanApplication with:

 on AppError catch (e) {
   _state = LoanApplicationState.error;
-  _errorMessage = e.message;
+  _errorMessage = _formatErrorMessage(e);
   notifyListeners();
   return false;
 } catch (e) {
   _state = LoanApplicationState.error;
-  _errorMessage = 'Failed to submit loan application. Please try again.';
+  _errorMessage = 'We couldn\'t process your loan application right now. Please check your connection and try again in a few moments.';
   notifyListeners();
   return false;
 }

After the closing brace of submitLoanApplication, add:

/// Formats error messages to be more user-friendly based on error type
String _formatErrorMessage(AppError error) {
  switch (error.code) {
    case 'validation_error':
      return 'Please check your application details: ${error.message}';
    case 'insufficient_income':
      return 'Your application was declined due to insufficient income. The minimum required monthly income for this loan type is ${error.details?['required_income'] ?? 'higher than provided'}.';
    case 'existing_loans':
      return 'You have existing loans that need to be paid down before applying for a new loan.';
    case 'server_error':
      return 'Our system is currently unavailable. Please try again later.';
    case 'network_error':
      return 'Please check your internet connection and try again.';
    default:
      return error.message;
  }
}
✏️ sacco_mobile/lib/features/loans/views/loan_application_screen.dart

Update _buildSubmissionError decoration and content:

 decoration: BoxDecoration(
-  color: Colors.red[100],
-  borderRadius: BorderRadius.circular(8),
+  color: Colors.red[50],
+  borderRadius: BorderRadius.circular(8),
+  border: Border.all(color: Colors.red[300]!),
 ),
 child: Column(
- Text('Error', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.red[900])),
- SizedBox(height: 4),
- Text(errorMessage, style: TextStyle(color: Colors.red[900])),
+ Row(
+   children: [
+     Icon(Icons.error_outline, color: Colors.red[700], size: 18),
+     SizedBox(width: 8),
+     Text('Application Error', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.red[900])),
+   ],
+ ),
+ SizedBox(height: 8),
+ Text(errorMessage, style: TextStyle(color: Colors.red[900])),
+ SizedBox(height: 8),
+ if (errorMessage.contains('income') || errorMessage.contains('existing loans'))
+   Text(
+     'Consider speaking with a Sacco representative for guidance.',
+     style: TextStyle(fontStyle: FontStyle.italic, fontSize: 12, color: Colors.red[800]),
+   ),

Replace the boolean _validateForm with a Map-based version and add inline helpers:

- bool _validateForm() {
+ Map<String, String?> _validateForm() {
   Map<String, String?> errors = {};
   switch (_currentPage) {
     // existing validation, populating errors['field']
   }
-  return true;
+  if (errors.isNotEmpty) {
+    final firstError = errors.values.firstWhere((e) => e != null)!;
+    ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(firstError)));
+  }
+  return errors;
 }
 
 void _nextPage() {
-  if (_validateForm()) {
+  final errors = _validateForm();
+  if (errors.isEmpty) {
     // navigate or submit
   }
 }
 
+// Add these helper methods for inline error display:
+Widget _buildTextField({required TextEditingController controller, required String label, String? error, ...}) { ... }
+Widget _buildDropdown<T>({required String label, required T? value, required List<DropdownMenuItem<T>> items, required void Function(T?) onChanged, String? error}) { ... }
✏️ sacco_mobile/lib/features/savings/viewmodels/savings_account_viewmodel.dart

Insert an initial error state when no account is loaded:

if (_savingsAccount == null) {
+  _state = SavingsAccountState.error;
+  _errorMessage = 'Please load your savings account first.';
+  notifyListeners();
   return false;
}

Replace catch blocks with formatted messages:

on AppError catch (e) {
-  _errorMessage = e.message;
+  _errorMessage = _getTransactionErrorMessage(e, isDeposit: true);
  notifyListeners();
  return false;
} catch (e) {
-  _errorMessage = 'Failed to make deposit. Please try again.';
+  _errorMessage = 'Your deposit couldn\'t be processed. Please check your connection and try again. If the issue persists, contact support.';
  notifyListeners();
  return false;
}

Before reloading the account, add an optimistic update:

await _savingsRepository.makeDeposit(...);
// Add optimistic update
 final updatedAccount = _savingsAccount!.copyWith(
   balance: _savingsAccount!.balance + amount,
   lastTransactionDate: DateTime.now(),
 );
 _savingsAccount = updatedAccount;
 notifyListeners();

Add helper method for transaction error messages below makeDeposit:

/// Returns user-friendly error messages for transaction errors
String _getTransactionErrorMessage(AppError error, {bool isDeposit = true}) {
  final type = isDeposit ? 'deposit' : 'withdrawal';
  switch (error.code) {
    case 'network_error':
      return 'Network connection issue. Your $type may have succeeded but we couldn\'t confirm it. Please check your account balance before trying again.';
    case 'insufficient_funds':
      return 'Insufficient funds for this withdrawal.';
    case 'invalid_amount':
      return 'The amount entered is invalid. Please check and try again.';
    case 'account_locked':
      return 'Your account is temporarily locked. Please contact customer support.';
    case 'service_unavailable':
      return 'The service is currently unavailable. Please try again later.';
    case 'duplicate_transaction':
      return 'This appears to be a duplicate $type. Please check your recent transactions before trying again.';
    default:
      return error.message;
  }
}
✏️ sacco_mobile/lib/features/profile/views/document_upload_screen.dart

Refactor the Upload button to use AnimatedSwitcher and key-based children:

onPressed: _selectedFile == null ? null : isUploading ? null : () => _uploadDocument(viewModel),
-child: isUploading
-  ? Row(...)
-  : Text('Upload ${widget.documentTitle}'),
+child: AnimatedSwitcher(
+  duration: Duration(milliseconds: 300),
+  child: isUploading
+    ? Row(key: ValueKey('loading'), mainAxisAlignment: MainAxisAlignment.center, children: [
+        SizedBox(width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2.5, valueColor: AlwaysStoppedAnimation<Color>(Colors.white))),
+        SizedBox(width: 12),
+        Text('Uploading... Please wait'),
+      ])
+    : Row(key: ValueKey('upload'), mainAxisAlignment: MainAxisAlignment.center, children: [
+        Icon(Icons.upload_file),
+        SizedBox(width: 8),
+        Text('Upload ${widget.documentTitle}'),
+      ]),
+),
 style: ElevatedButton.styleFrom(
   padding: EdgeInsets.symmetric(vertical: 14),
   backgroundColor: Theme.of(context).primaryColor,
+  disabledBackgroundColor: isUploading ? Theme.of(context).primaryColor.withOpacity(0.7) : null,
 ),

Update preview widgets to accept isUploading and overlay a dimmed spinner:

-Widget _buildFilePreview() {
+Widget _buildFilePreview(bool isUploading) {
   return Stack(children: [
+    Opacity(opacity: isUploading ? 0.7 : 1.0, child: /* existing preview code */),
+    if (isUploading)
+      Positioned.fill(child: Center(child: Column(mainAxisSize: MainAxisSize.min, children: [
+        SizedBox(width: 28, height: 28, child: CircularProgressIndicator(strokeWidth: 3, valueColor: AlwaysStoppedAnimation<Color>(Colors.white))),
+        SizedBox(height: 12),
+        Text('Uploading...', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
+      ]))),
   ]);
}
-Widget _buildEmptyPreview() {
+Widget _buildEmptyPreview() {
   // existing code...
+  SizedBox(height: 8),
+  Text('Tap Gallery or Camera below', style: TextStyle(color: Colors.grey[500], fontSize: 12)),
 }
➕ sacco_mobile/lib/features/core/utils/transaction_utils.dart

Create the file with:

import 'package:intl/intl.dart';

/// Utility class for formatting and validating transaction-related data
class TransactionUtils {
  static final NumberFormat currencyFormatter = NumberFormat.currency(
    locale: 'en_UG',
    symbol: 'UGX ',
    decimalDigits: 0,
  );
  static final DateFormat dateFormatter = DateFormat('dd MMM yyyy');
  static final DateFormat timeFormatter = DateFormat('HH:mm');
  static final DateFormat dateTimeFormatter = DateFormat('dd MMM yyyy, HH:mm');

  static String formatCurrency(double amount) => currencyFormatter.format(amount);
  static String formatDate(DateTime date) => dateFormatter.format(date);
  static String formatTime(DateTime time) => timeFormatter.format(time);
  static String formatDateTime(DateTime dateTime) => dateTimeFormatter.format(dateTime);

  static int daysBetween(DateTime from, DateTime to) {
    from = DateTime(from.year, from.month, from.day);
    to = DateTime(to.year, to.month, to.day);
    return (to.difference(from).inHours / 24).round();
  }
  static bool isPaymentDueSoon(DateTime dueDate) {
    final days = daysBetween(DateTime.now(), dueDate);
    return days >= 0 && days <= 3;
  }
  static bool isPaymentOverdue(DateTime dueDate) => DateTime.now().isAfter(dueDate);

  static int getAmountColor(String transactionType, {bool isDark = false}) {
    final type = transactionType.toLowerCase();
    if (type.contains('deposit') || type.contains('credit')) return isDark ? 0xFF81C784 : 0xFF388E3C;
    if (type.contains('withdrawal') || type.contains('debit')) return isDark ? 0xFFE57373 : 0xFFD32F2F;
    return isDark ? 0xFFFFB74D : 0xFFF57C00;
  }

  static String? validateAmount(String amount, double maxAmount) {
    if (amount.isEmpty) return 'Amount is required';
    final parsed = double.tryParse(amount.replaceAll(',', ''));
    if (parsed == null) return 'Please enter a valid amount';
    if (parsed <= 0) return 'Amount must be greater than zero';
    if (parsed > maxAmount) return 'Amount cannot exceed ${formatCurrency(maxAmount)}';
    return null;
  }
}
✏️ sacco_mobile/lib/features/savings/views/deposit_screen.dart

Add at the top:

import '../../../core/utils/transaction_utils.dart';

Update the balance display:

- 'Current Balance: ${NumberFormat.currency(locale: 'en_UG', symbol: 'UGX ', decimalDigits: 0).format(viewModel.savingsAccount?.balance ?? 0)}',
+ 'Current Balance: ${TransactionUtils.formatCurrency(viewModel.savingsAccount?.balance ?? 0)}',
✏️ sacco_mobile/lib/features/loans/views/loan_list_screen.dart

Replace the top-level Consumer with Builder and granular Consumers for appBar action and body:

-return ChangeNotifierProvider(
-  child: Consumer<LoanListViewModel>(
-    builder: (context, viewModel, _) {
+return ChangeNotifierProvider(
+  child: Builder(
+    builder: (context) {
+      final viewModel = Provider.of<LoanListViewModel>(context, listen: false);
+      return Scaffold(
         appBar: AppBar(
           title: Text('Loans'),
           actions: [
-            IconButton(icon: Icon(Icons.refresh), onPressed: viewModel.state == LoanListState.loading ? null : () => viewModel.loadLoans()),
+            Consumer<LoanListViewModel>(
+              builder: (_, vm, __) => IconButton(
+                icon: Icon(Icons.refresh),
+                onPressed: vm.state == LoanListState.loading ? null : () => vm.loadLoans(),
+              ),
+            ),
           ],
         ),
-        body: _buildBody(viewModel),
+        body: Consumer<LoanListViewModel>(builder: (_, vm, __) => _buildBody(vm)),

Replace TabBarView with IndexedStack and add initState:

-        TabBarView(controller: _tabController, children: [...]),
+        IndexedStack(
+          index: _tabController.index,
+          children: [
+            Visibility(visible: _tabController.index == 0, maintainState: true, child: _buildActiveLoansTab(viewModel)),
+            Visibility(visible: _tabController.index == 1, maintainState: true, child: _buildAllLoansTab(viewModel)),
+            Visibility(visible: _tabController.index == 2, maintainState: true, child: _buildLoanHistoryTab(viewModel)),
+          ],
+        ),
@@
  void initState() {
    super.initState();
+   _tabController = TabController(length: 3, vsync: this);
+   _tabController.addListener(() {
+     if (_tabController.indexIsChanging) setState(() {});
+   });
+   WidgetsBinding.instance.addPostFrameCallback((_) {
+     Provider.of<LoanListViewModel>(context, listen: false).loadLoans();
+   });
  }
✏️ sacco_mobile/lib/features/loans/views/loan_application_screen.dart

Replace the existing _validateForm method signature and body. Remove the bool _validateForm() declaration and its implementation, then insert:

Map<String, String?> _validateForm() {
  final Map<String, String?> errors = {};

  switch (_currentPage) {
    case 0:
      if (_selectedLoanType == null) {
        errors['loanType'] = 'Please select a loan type';
      }
      if (_amountController.text.isEmpty) {
        errors['amount'] = 'Please enter loan amount';
      } else {
        final amount = double.tryParse(_amountController.text.replaceAll(',', ''));
        if (amount == null || amount <= 0) {
          errors['amount'] = 'Please enter a valid amount';
        } else if (amount < 50000) {
          errors['amount'] = 'Minimum loan amount is UGX 50,000';
        } else if (amount > 10000000) {
          errors['amount'] = 'Maximum loan amount is UGX 10,000,000';
        }
      }
      if (_termMonths == null) {
        errors['loanTerm'] = 'Please select a loan term';
      }
      break;
    case 1:
      if (_purposeController.text.isEmpty) {
        errors['purpose'] = 'Please enter loan purpose';
      } else if (_purposeController.text.length < 10) {
        errors['purpose'] = 'Please provide more details about the purpose';
      }
      if (_collateralController.text.isEmpty) {
        errors['collateral'] = 'Please enter collateral information';
      }
      break;
    case 2:
      if (_employmentDetailsController.text.isEmpty) {
        errors['employmentStatus'] = 'Please select employment status';
      }
      if (_employmentDetailsController.text.isEmpty) {
        errors['employer'] = 'Please enter employer name';
      }
      if (_monthlyIncomeController.text.isEmpty) {
        errors['monthlyIncome'] = 'Please enter monthly income';
      } else {
        final income = double.tryParse(_monthlyIncomeController.text.replaceAll(',', ''));
        if (income == null || income <= 0) {
          errors['monthlyIncome'] = 'Please enter a valid income amount';
        }
      }
      break;
  }

  if (errors.isNotEmpty) {
    final firstError = errors.values.firstWhere((e) => e != null);
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text(firstError!))
    );
  }

  return errors;
}

Update _nextPage so it checks for an empty error map instead of a boolean. Find the call:

if (_validateForm()) {
  // ...
}

and replace it with:

final errors = _validateForm();
if (errors.isEmpty) {
  // existing page‑advance or submission logic
}

Remove any inline ScaffoldMessenger.of(context).showSnackBar(...) calls inside the old _validateForm cases, since the new implementation aggregates and shows the first error only once at the end.

➕ sacco_mobile/lib/features/loans/views/loan_application_screen.dart

After the last widget‑building method and before the final closing brace of _LoanApplicationScreenState, insert the following helper methods:

// Helper to build a text field with inline error
Widget _buildTextField({
  required TextEditingController controller,
  required String label,
  String? error,
  TextInputType keyboardType = TextInputType.text,
  List<TextInputFormatter>? inputFormatters,
  String? hintText,
}) {
  return Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      Text(label, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
      SizedBox(height: 8),
      TextField(
        controller: controller,
        decoration: InputDecoration(
          hintText: hintText,
          errorText: error,
          border: OutlineInputBorder(),
          contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 16),
        ),
        keyboardType: keyboardType,
        inputFormatters: inputFormatters,
      ),
    ],
  );
}

// Helper to build a dropdown with inline error
Widget _buildDropdown<T>({
  required String label,
  required T? value,
  required List<DropdownMenuItem<T>> items,
  required void Function(T?) onChanged,
  String? error,
}) {
  return Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      Text(label, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
      SizedBox(height: 8),
      DropdownButtonFormField<T>(
        value: value,
        items: items,
        onChanged: onChanged,
        decoration: InputDecoration(
          errorText: error,
          border: OutlineInputBorder(),
          contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 16),
        ),
        isExpanded: true,
      ),
    ],
  );
}
✏️ sacco_mobile/lib/features/loans/views/loan_detail_screen.dart

At the top of the file, remove the import of Intl and add the TransactionUtils import. Replace:

import 'package:intl/intl.dart';

with:

import 'package:sacco_mobile/core/utils/transaction_utils.dart';

Delete the manual currencyFormat field. Remove lines:

final currencyFormat = NumberFormat.currency(
  symbol: 'UGX ',
  decimalDigits: 0,
);

Replace every occurrence of currencyFormat.format(value) with TransactionUtils.formatCurrency(value). For example, change:

currencyFormat.format(loan.amount)

to

TransactionUtils.formatCurrency(loan.amount)

Apply this to all calls in the summary card, payment details card, activity item, and bottom bar.


After removing the Intl import and currencyFormat field, run a Dart analyzer or linter to ensure there are no unused imports. If any remain (e.g., the Intl package), delete them.


Build and run the app. Navigate to the Loan Detail screen and verify that all monetary values display correctly formatted (e.g., “UGX 1,000,000”) and that no runtime errors occur due to missing imports or methods.

✏️ sacco_mobile/lib/features/loans/views/loan_list_screen.dart

Replace the existing Consumer block (lines 48–88) with a Builder that retrieves the viewModel once (listen: false), and uses two granular Consumers: one for the refresh IconButton and one for the body. The replacement code is:

      child: Builder(
        builder: (context) {
          final viewModel = Provider.of<LoanListViewModel>(context, listen: false);
          return Scaffold(
            appBar: AppBar(
              title: const Text('My Loans'),
              actions: [
                Consumer<LoanListViewModel>(
                  builder: (context, viewModel, _) {
                    return IconButton(
                      icon: const Icon(Icons.refresh),
                      onPressed: viewModel.state == LoanListState.loading
                          ? null
                          : () => viewModel.refreshLoans(),
                    );
                  },
                ),
              ],
              bottom: TabBar(
                controller: _tabController,
                tabs: const [
                  Tab(text: 'Active'),
                  Tab(text: 'All Loans'),
                  Tab(text: 'History'),
                ],
              ),
            ),
            body: Consumer<LoanListViewModel>(
              builder: (context, viewModel, _) => _buildBody(viewModel),
            ),
            floatingActionButton: FloatingActionButton.extended(
              onPressed: () {
                Navigator.of(context).push(
                  MaterialPageRoute(
                    builder: (context) => const LoanApplicationScreen(),
                  ),
                );
              },
              label: const Text('Apply for Loan'),
              icon: const Icon(Icons.add),
              backgroundColor: AppTheme.primaryColor,
            ),
          );
        },
      ),
➕ sacco_mobile/lib/features/loans/README.md

Add the following content exactly as shown to the new file:

# Loan Management Features

This module provides comprehensive loan management capabilities for the Sacco Mobile application, including loan application, loan listing, and loan repayment.

## Features

### Loan Application
- Multi-step loan application form
- Different loan types with varying terms and interest rates
- Instant calculation of monthly payments and total interest
- Loan purpose, collateral, and employment information collection

### Loan Listing
- View all loans with filtering options
- Active loans tab for quick access to current loans
- Loan history to track past applications
- Detailed loan status with progress indicators

### Loan Details
- Comprehensive loan information display
- Payment schedule and outstanding balance
- Due date alerts and missed payment notifications
- Quick access to make payments

### Loan Repayment
- Simple payment submission process
- Multiple payment method options
- Transaction reference tracking
- Penalty calculation for late payments

## How to Use

1. **Apply for a Loan**:
   - Navigate to the Loan screen
   - Tap on "Apply for Loan" button
   - Follow the multi-step application process
   - Review and submit your application

2. **View Your Loans**:
   - Navigate to the Loan screen
   - Browse the tabs to see Active, All, or History
   - Tap on any loan to view details

3. **Make a Loan Repayment**:
   - Open a loan from the loan list
   - Tap on "Make Payment" button
   - Enter payment amount and select payment method
   - Confirm payment details

4. **Track Loan Status**:
   - Use progress bars on loan cards to see repayment progress
   - Check payment due dates and outstanding amounts
   - View complete loan activity timeline in loan details

## Technical Implementation

The loan features use a ViewModel pattern to separate business logic from UI:
- `LoanApplicationViewModel`: Manages loan application state and submission
- `LoanListViewModel`: Handles loan listings and filtering
- `LoanRepaymentViewModel`: Controls loan repayment process

These view models connect to the `LoanRepository` which handles data operations with the backend services.
✏️ sacco_mobile/lib/features/loans/viewmodels/loan_application_viewmodel.dart

In the on AppError catch (e) block, replace _errorMessage = e.message; with _errorMessage = _formatErrorMessage(e);


In the generic catch (e) block, update _errorMessage to:

We couldn't process your loan application right now. Please check your connection and try again in a few moments.

Below the closing brace of submitLoanApplication, add the helper method:

/// Formats error messages to be more user‑friendly based on error type
String _formatErrorMessage(AppError error) {
  switch (error.code) {
    case 'validation_error':
      return 'Please check your application details: ${error.message}';
    case 'insufficient_income':
      return 'Your application was declined due to insufficient income. The minimum required monthly income for this loan type is ${error.details?['required_income'] ?? 'higher than provided'}.';
    case 'existing_loans':
      return 'You have existing loans that need to be paid down before applying for a new loan.';
    case 'server_error':
      return 'Our system is currently unavailable. Please try again later.';
    case 'network_error':
      return 'Please check your internet connection and try again.';
    default:
      return error.message;
  }
}
✏️ sacco_mobile/lib/features/loans/views/loan_application_screen.dart

Modify the Container decoration to include a light border:

- decoration: BoxDecoration(
-   color: Colors.red[100],
-   borderRadius: BorderRadius.circular(8),
- ),
+ decoration: BoxDecoration(
+   color: Colors.red[50],
+   borderRadius: BorderRadius.circular(8),
+   border: Border.all(color: Colors.red[300]!),
+ ),

Replace the title Text and add an icon and contextual advice:

Row(
  children: [
    Icon(Icons.error_outline, color: Colors.red[700], size: 18),
    SizedBox(width: 8),
    Text(
      'Application Error',
      style: TextStyle(
        fontWeight: FontWeight.bold,
        color: Colors.red[900],
      ),
    ),
  ],
),
SizedBox(height: 8),
Text(errorMessage, style: TextStyle(color: Colors.red[900])),
SizedBox(height: 8),
if (errorMessage.contains('income') || errorMessage.contains('existing loans'))
  Text(
    'Consider speaking with a Sacco representative for guidance.',
    style: TextStyle(
      fontStyle: FontStyle.italic,
      fontSize: 12,
      color: Colors.red[800],
    ),
  ),
✏️ sacco_mobile/lib/features/savings/viewmodels/savings_account_viewmodel.dart

Before returning false when _savingsAccount is null, set:

_state = SavingsAccountState.error;
_errorMessage = 'Please load your savings account first.';
notifyListeners();

After the repository call succeeds, apply an optimistic update:

final updatedAccount = _savingsAccount!.copyWith(
  balance: _savingsAccount!.balance + amount,
  lastTransactionDate: DateTime.now(),
);
_savingsAccount = updatedAccount;
notifyListeners();

In the on AppError catch (e) block, replace _errorMessage = e.message; with _errorMessage = _getTransactionErrorMessage(e, isDeposit: true);


In the generic catch (e) block, update _errorMessage to:

Your deposit couldn't be processed. Please check your connection and try again. If the issue persists, contact support.

Below the class, add the helper:

/// Returns user‑friendly error messages for transaction errors
String _getTransactionErrorMessage(AppError error, {bool isDeposit = true}) {
  final transactionType = isDeposit ? 'deposit' : 'withdrawal';
  switch (error.code) {
    case 'network_error':
      return 'Network connection issue. Your $transactionType may have succeeded but we couldn\'t confirm it. Please check your account balance before trying again.';
    case 'insufficient_funds':
      return 'Insufficient funds for this withdrawal.';
    case 'invalid_amount':
      return 'The amount entered is invalid. Please check and try again.';
    case 'account_locked':
      return 'Your account is temporarily locked. Please contact customer support.';
    case 'service_unavailable':
      return 'The service is currently unavailable. Please try again later.';
    case 'duplicate_transaction':
      return 'This appears to be a duplicate $transactionType. Please check your recent transactions before trying again.';
    default:
      return error.message;
  }
}
✏️ sacco_mobile/lib/features/profile/views/document_upload_screen.dart

Introduce at the top of _buildBody:

final bool isUploading = viewModel.state == ProfileState.uploading;

Pass isUploading to _buildFilePreview(isUploading) and _buildEmptyPreview(isUploading) so the preview dims during upload.


Wrap the upload AppButton in an AnimatedSwitcher:

AnimatedSwitcher(
  duration: Duration(milliseconds: 300),
  child: isUploading
    ? Row(
        key: ValueKey('loading'),
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          LoadingIndicator(message: 'Uploading...'),
          SizedBox(width: 8),
          Text('Please wait'),
        ],
      )
    : Text('Upload'),
)

In both _buildEmptyPreview and _buildFilePreview, apply an Opacity or overlay when isUploading is true to visually indicate the upload in progress and disable clear actions.

✏️ sacco_mobile/lib/features/loans/views/loan_application_screen.dart

Change the signature of _validateForm from returning bool to returning Map<String, String?>, accumulating field‑specific errors instead of showing SnackBars immediately.


At the end of _validateForm, if any errors exist, show only the first one via SnackBar and return the map. In _nextPage, proceed only when the returned map is empty.


Add helper widgets _buildTextField and _buildDropdown at the bottom of the class to render inline errorText on fields using the errors map.

➕ sacco_mobile/lib/features/core/utils/transaction_utils.dart

Add the following Dart class:

import 'package:intl/intl.dart';

/// Utility class for formatting and validating transaction-related data
class TransactionUtils {
  static final NumberFormat currencyFormatter = NumberFormat.currency(
    locale: 'en_UG', symbol: 'UGX ', decimalDigits: 0,
  );
  static final DateFormat dateFormatter = DateFormat('dd MMM yyyy');
  static final DateFormat timeFormatter = DateFormat('HH:mm');
  static final DateFormat dateTimeFormatter = DateFormat('dd MMM yyyy, HH:mm');

  static String formatCurrency(double amount) => currencyFormatter.format(amount);
  static String formatDate(DateTime date) => dateFormatter.format(date);
  static String formatTime(DateTime time) => timeFormatter.format(time);
  static String formatDateTime(DateTime dateTime) => dateTimeFormatter.format(dateTime);

  static int daysBetween(DateTime from, DateTime to) {
    from = DateTime(from.year, from.month, from.day);
    to = DateTime(to.year, to.month, to.day);
    return (to.difference(from).inHours / 24).round();
  }
  static bool isPaymentDueSoon(DateTime dueDate) {
    final days = daysBetween(DateTime.now(), dueDate);
    return days >= 0 && days <= 3;
  }
  static bool isPaymentOverdue(DateTime dueDate) => DateTime.now().isAfter(dueDate);

  static int getAmountColor(String type, {bool isDark = false}) {
    final lower = type.toLowerCase();
    if (lower.contains('deposit') || lower.contains('credit')) {
      return isDark ? 0xFF81C784 : 0xFF388E3C;
    } else if (lower.contains('withdrawal') || lower.contains('debit')) {
      return isDark ? 0xFFE57373 : 0xFFD32F2F;
    } else {
      return isDark ? 0xFFFFB74D : 0xFFF57C00;
    }
  }

  static String? validateAmount(String amount, double maxAmount) {
    if (amount.isEmpty) return 'Amount is required';
    final parsed = double.tryParse(amount.replaceAll(',', ''));
    if (parsed == null) return 'Please enter a valid amount';
    if (parsed <= 0) return 'Amount must be greater than zero';
    if (parsed > maxAmount) return 'Amount cannot exceed ${formatCurrency(maxAmount)}';
    return null;
  }
}
✏️ sacco_mobile/lib/features/savings/views/deposit_screen.dart

Add import at the top:

import '../../../core/utils/transaction_utils.dart';

Replace occurrences of:

NumberFormat.currency(
  locale: 'en_UG', symbol: 'UGX ', decimalDigits: 0,
).format(...)

with

TransactionUtils.formatCurrency(...)

Modify _validateAmount to delegate to:

return TransactionUtils.validateAmount(value, double.infinity);
✏️ sacco_mobile/lib/features/loans/views/loan_list_screen.dart

Replace the top‑level Consumer<LoanListViewModel> with a Builder that calls Provider.of<LoanListViewModel>(context, listen: false) for static parts, then wrap only the refresh button and the body in separate Consumer<LoanListViewModel> widgets.


Change the TabBarView to an IndexedStack with Visibility widgets to maintain offscreen tab state and prevent unnecessary rebuilds.


In initState, attach a listener to _tabController to call setState on tab changes, and trigger loadLoans() in a PostFrameCallback.

➕ sacco_mobile/lib/features/loans/README.md

Copy the following content exactly into the new file sacco_mobile/lib/features/loans/README.md:

# Loan Management Features

This module provides comprehensive loan management capabilities for the Sacco Mobile application, including loan application, loan listing, and loan repayment.

## Features

### Loan Application
- Multi-step loan application form
- Different loan types with varying terms and interest rates
- Instant calculation of monthly payments and total interest
- Loan purpose, collateral, and employment information collection

### Loan Listing
- View all loans with filtering options
- Active loans tab for quick access to current loans
- Loan history to track past applications
- Detailed loan status with progress indicators

### Loan Details
- Comprehensive loan information display
- Payment schedule and outstanding balance
- Due date alerts and missed payment notifications
- Quick access to make payments

### Loan Repayment
- Simple payment submission process
- Multiple payment method options
- Transaction reference tracking
- Penalty calculation for late payments

## How to Use

1. **Apply for a Loan**:
   - Navigate to the Loan screen
   - Tap on "Apply for Loan" button
   - Follow the multi-step application process
   - Review and submit your application

2. **View Your Loans**:
   - Navigate to the Loan screen
   - Browse the tabs to see Active, All, or History
   - Tap on any loan to view details

3. **Make a Loan Repayment**:
   - Open a loan from the loan list
   - Tap on "Make Payment" button
   - Enter payment amount and select payment method
   - Confirm payment details

4. **Track Loan Status**:
   - Use progress bars on loan cards to see repayment progress
   - Check payment due dates and outstanding amounts
   - View complete loan activity timeline in loan details

## Technical Implementation

The loan features use a ViewModel pattern to separate business logic from UI:
- `LoanApplicationViewModel`: Manages loan application state and submission
- `LoanListViewModel`: Handles loan listings and filtering
- `LoanRepaymentViewModel`: Controls loan repayment process

These view models connect to the `LoanRepository` which handles data operations with the backend services.
✏️ sacco_mobile/lib/features/loans/viewmodels/loan_application_viewmodel.dart

In submitLoanApplication's AppError catch block, replace _errorMessage = e.message; with _errorMessage = _formatErrorMessage(e); so errors are more descriptive.


In the general catch block, replace the default message with:

_errorMessage = 'We couldn\'t process your loan application right now. Please check your connection and try again in a few moments.';

Immediately after the closing brace of submitLoanApplication, add the private helper method:

/// Formats error messages to be more user-friendly based on error type
String _formatErrorMessage(AppError error) {
  switch (error.code) {
    case 'validation_error':
      return 'Please check your application details: ${error.message}';
    case 'insufficient_income':
      return 'Your application was declined due to insufficient income. The minimum required monthly income for this loan type is ${error.details?['required_income'] ?? 'higher than provided'}.';
    case 'existing_loans':
      return 'You have existing loans that need to be paid down before applying for a new loan.';
    case 'server_error':
      return 'Our system is currently unavailable. Please try again later.';
    case 'network_error':
      return 'Please check your internet connection and try again.';
    default:
      return error.message;
  }
}
✏️ sacco_mobile/lib/features/loans/views/loan_application_screen.dart

Replace the BoxDecoration block with:

decoration: BoxDecoration(
  color: Colors.red[50],
  borderRadius: BorderRadius.circular(8),
  border: Border.all(color: Colors.red[300]!),
),

Replace the static 'Error' header and its Text widget with:

Row(
  children: [
    Icon(Icons.error_outline, color: Colors.red[700], size: 18),
    SizedBox(width: 8),
    Text(
      'Application Error',
      style: TextStyle(
        fontWeight: FontWeight.bold,
        color: Colors.red[900],
      ),
    ),
  ],
),

Add below the errorMessage Text:

if (errorMessage.contains('income') || errorMessage.contains('existing loans'))
  Text(
    'Consider speaking with a Sacco representative for guidance.',
    style: TextStyle(
      fontStyle: FontStyle.italic,
      fontSize: 12,
      color: Colors.red[800],
    ),
  ),
✏️ sacco_mobile/lib/features/savings/viewmodels/savings_account_viewmodel.dart

At the top of makeDeposit, handle null account:

if (_savingsAccount == null) {
  _state = SavingsAccountState.error;
  _errorMessage = 'Please load your savings account first.';
  notifyListeners();
  return false;
}

After a successful repository call, add an optimistic update:

final updatedAccount = _savingsAccount!.copyWith(
  balance: _savingsAccount!.balance + amount,
  lastTransactionDate: DateTime.now(),
);
_savingsAccount = updatedAccount;
notifyListeners();

In the AppError catch, change _errorMessage = e.message; to:

_errorMessage = _getTransactionErrorMessage(e, isDeposit: true);

In the general catch, replace the message with:

_errorMessage = 'Your deposit couldn\'t be processed. Please check your connection and try again. If the issue persists, contact support.';

After makeDeposit, add the helper method:

/// Returns user-friendly error messages for transaction errors
String _getTransactionErrorMessage(AppError error, {bool isDeposit = true}) {
  final transactionType = isDeposit ? 'deposit' : 'withdrawal';
  switch (error.code) {
    case 'network_error':
      return 'Network connection issue. Your $transactionType may have succeeded but we couldn\'t confirm it. Please check your account balance before trying again.';
    case 'insufficient_funds':
      return 'Insufficient funds for this withdrawal.';
    case 'invalid_amount':
      return 'The amount entered is invalid. Please check and try again.';
    case 'account_locked':
      return 'Your account is temporarily locked. Please contact customer support.';
    case 'service_unavailable':
      return 'The service is currently unavailable. Please try again later.';
    case 'duplicate_transaction':
      return 'This appears to be a duplicate $transactionType. Please check your recent transactions before trying again.';
    default:
      return error.message;
  }
}
✏️ sacco_mobile/lib/features/profile/views/document_upload_screen.dart

Change the file preview invocation to pass isUploading:

_selectedFile != null ? _buildFilePreview(isUploading) : _buildEmptyPreview(),

Wrap the Upload button child in an AnimatedSwitcher and switch between loading and upload states as per requirement:


Update the empty and file preview builders to show guidance and overlay a spinner when isUploading is true.

✏️ sacco_mobile/lib/features/loans/views/loan_application_screen.dart

Change _validateForm signature from bool _validateForm() to Map<String, String?> _validateForm() and build an errors map instead of showing SnackBars directly.


In _nextPage, replace the boolean check with Map<String, String?> errors = _validateForm(); if (errors.isEmpty) { ... }.


Add helper methods _buildTextField and _buildDropdown at the bottom of the class for inline error display, matching the requirement block exactly.

➕ sacco_mobile/lib/features/core/utils/transaction_utils.dart

Create the file and insert the TransactionUtils class exactly as specified, including imports from intl and all static methods.

✏️ sacco_mobile/lib/features/savings/views/deposit_screen.dart

At the top of the file, add:

import '../../../core/utils/transaction_utils.dart';

In _buildBody, replace the inline NumberFormat.currency(...) usage with TransactionUtils.formatCurrency(viewModel.savingsAccount?.balance ?? 0).


In the _validateAmount method, return TransactionUtils.validateAmount(value, double.infinity); instead of manual logic.

✏️ sacco_mobile/lib/features/loans/views/loan_list_screen.dart

Replace the top-level Consumer with a Builder and use Provider.of(..., listen: false) for static UI parts.


Wrap the refresh IconButton in its own Consumer so only it rebuilds on state changes.


Wrap the body in a separate Consumer and replace TabBarView with an IndexedStack of Visibility widgets to maintain inactive tabs without rebuilding them.


In initState, set up the TabController listener and load loans in a post‑frame callback.

➕ sacco_mobile/lib/features/loans/README.md

Insert the following content into README.md at the top of the file:

# Loan Management Features

This module provides comprehensive loan management capabilities for the Sacco Mobile application, including loan application, loan listing, and loan repayment.

## Features

### Loan Application
- Multi-step loan application form
- Different loan types with varying terms and interest rates
- Instant calculation of monthly payments and total interest
- Loan purpose, collateral, and employment information collection

### Loan Listing
- View all loans with filtering options
- Active loans tab for quick access to current loans
- Loan history to track past applications
- Detailed loan status with progress indicators

### Loan Details
- Comprehensive loan information display
- Payment schedule and outstanding balance
- Due date alerts and missed payment notifications
- Quick access to make payments

### Loan Repayment
- Simple payment submission process
- Multiple payment method options
- Transaction reference tracking
- Penalty calculation for late payments

## How to Use

1. **Apply for a Loan**:
   - Navigate to the Loan screen
   - Tap on "Apply for Loan" button
   - Follow the multi-step application process
   - Review and submit your application

2. **View Your Loans**:
   - Navigate to the Loan screen
   - Browse the tabs to see Active, All, or History
   - Tap on any loan to view details

3. **Make a Loan Repayment**:
   - Open a loan from the loan list
   - Tap on "Make Payment" button
   - Enter payment amount and select payment method
   - Confirm payment details

4. **Track Loan Status**:
   - Use progress bars on loan cards to see repayment progress
   - Check payment due dates and outstanding amounts
   - View complete loan activity timeline in loan details

## Technical Implementation

The loan features use a ViewModel pattern to separate business logic from UI:
- `LoanApplicationViewModel`: Manages loan application state and submission
- `LoanListViewModel`: Handles loan listings and filtering
- `LoanRepaymentViewModel`: Controls loan repayment process

These view models connect to the `LoanRepository` which handles data operations with the backend services.
✏️ sacco_mobile/lib/features/loans/viewmodels/loan_application_viewmodel.dart

In the AppError catch block inside submitLoanApplication, replace
_errorMessage = e.message;
with
_errorMessage = _formatErrorMessage(e);.


In the generic catch block, update the default message to:
We couldn't process your loan application right now. Please check your connection and try again in a few moments.


After the submitLoanApplication method (before the end of the class), add the following helper method:

/// Formats error messages to be more user-friendly based on error type
String _formatErrorMessage(AppError error) {
  switch (error.code) {
    case 'validation_error':
      return 'Please check your application details: ${error.message}';
    case 'insufficient_income':
      return 'Your application was declined due to insufficient income. The minimum required monthly income for this loan type is ${error.details?['required_income'] ?? 'higher than provided'}.';
    case 'existing_loans':
      return 'You have existing loans that need to be paid down before applying for a new loan.';
    case 'server_error':
      return 'Our system is currently unavailable. Please try again later.';
    case 'network_error':
      return 'Please check your internet connection and try again.';
    default:
      return error.message;
  }
}
✏️ sacco_mobile/lib/features/loans/views/loan_application_screen.dart

Update the Container decoration to:

decoration: BoxDecoration(
  color: Colors.red[50],
  borderRadius: BorderRadius.circular(8),
  border: Border.all(color: Colors.red[300]!),
),

Replace the existing Text widget labeled ‘Error’ and the message Text with:

Row(
  children: [
    Icon(Icons.error_outline, color: Colors.red[700], size: 18),
    SizedBox(width: 8),
    Text(
      'Application Error',
      style: TextStyle(
        fontWeight: FontWeight.bold,
        color: Colors.red[900],
      ),
    ),
  ],
),
SizedBox(height: 8),
Text(
  errorMessage,
  style: TextStyle(color: Colors.red[900]),
),
SizedBox(height: 8),
if (errorMessage.contains('income') || errorMessage.contains('existing loans'))
  Text(
    'Consider speaking with a Sacco representative for guidance.',
    style: TextStyle(
      fontStyle: FontStyle.italic,
      fontSize: 12,
      color: Colors.red[800],
    ),
  ),
✏️ sacco_mobile/lib/features/loans/views/loan_application_screen.dart

Change the signature of _validateForm from bool _validateForm() to Map<String, String?> _validateForm(), initialize errors, and replace each ScaffoldMessenger call with populating errors keyed by field name.


At the end of _validateForm, if errors is not empty, show the first error via:

final firstError = errors.values.where((e) => e != null).first;
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(firstError!)));
return errors;

Update the call in _nextPage to:

final errors = _validateForm();
if (errors.isEmpty) { ... }

Add helper methods _buildTextField and _buildDropdown at the bottom of the class to render inline error messages for fields, using the provided prototypes.

✏️ sacco_mobile/lib/features/savings/viewmodels/savings_account_viewmodel.dart

In makeDeposit, before returning false when _savingsAccount is null, set:

_state = SavingsAccountState.error;
_errorMessage = 'Please load your savings account first.';
notifyListeners();

After the repository call, add optimistic update:

final updatedAccount = _savingsAccount!.copyWith(
  balance: _savingsAccount!.balance + amount,
  lastTransactionDate: DateTime.now(),
);
_savingsAccount = updatedAccount;
notifyListeners();

In the AppError catch, replace _errorMessage = e.message; with _errorMessage = _getTransactionErrorMessage(e, isDeposit: true);.


In the generic catch, update the error message to:

'Your deposit couldn\'t be processed. Please check your connection and try again. If the issue persists, contact support.'

At the end of the class, add the helper method:

/// Returns user-friendly error messages for transaction errors
String _getTransactionErrorMessage(AppError error, {bool isDeposit = true}) {
  final transactionType = isDeposit ? 'deposit' : 'withdrawal';
  switch (error.code) {
    case 'network_error':
      return 'Network connection issue. Your $transactionType may have succeeded but we couldn\'t confirm it. Please check your account balance before trying again.';
    case 'insufficient_funds':
      return 'Insufficient funds for this withdrawal.';
    case 'invalid_amount':
      return 'The amount entered is invalid. Please check and try again.';
    case 'account_locked':
      return 'Your account is temporarily locked. Please contact customer support.';
    case 'service_unavailable':
      return 'The service is currently unavailable. Please try again later.';
    case 'duplicate_transaction':
      return 'This appears to be a duplicate $transactionType. Please check your recent transactions before trying again.';
    default:
      return error.message;
  }
}
✏️ sacco_mobile/lib/features/profile/views/document_upload_screen.dart

Change the call to _buildFilePreview to pass isUploading and update _buildFilePreview signature to accept that flag and adjust opacity of the image and close button accordingly.


Wrap the upload button’s child in AnimatedSwitcher, adding 'loading' and 'upload' ValueKey rows with a smaller CircularProgressIndicator and updated text.


In _buildEmptyPreview, add a hint line:

SizedBox(height: 8),
Text(
  'Tap Gallery or Camera below',
  style: TextStyle(
    color: Colors.grey[500],
    fontSize: 12,
  ),
),

In _buildFilePreview, overlay a full-screen semi-transparent loading box with a CircularProgressIndicator and 'Uploading...' text when isUploading is true.

➕ sacco_mobile/lib/features/core/utils/transaction_utils.dart

Create transaction_utils.dart with the following content:

import 'package:intl/intl.dart';

/// Utility class for formatting and validating transaction-related data
class TransactionUtils {
  /// Currency formatter for Ugandan Shillings
  static final NumberFormat currencyFormatter = NumberFormat.currency(
    locale: 'en_UG',
    symbol: 'UGX ',
    decimalDigits: 0,
  );
  
  /// Date formatter for displaying dates
  static final DateFormat dateFormatter = DateFormat('dd MMM yyyy');
  
  /// Time formatter for displaying times
  static final DateFormat timeFormatter = DateFormat('HH:mm');
  
  /// Date and time formatter
  static final DateFormat dateTimeFormatter = DateFormat('dd MMM yyyy, HH:mm');
  
  /// Format currency amount to UGX string
  static String formatCurrency(double amount) => currencyFormatter.format(amount);
  
  /// Format date to readable string
  static String formatDate(DateTime date) => dateFormatter.format(date);
  
  /// Format time to readable string
  static String formatTime(DateTime time) => timeFormatter.format(time);
  
  /// Format date and time to readable string
  static String formatDateTime(DateTime dateTime) => dateTimeFormatter.format(dateTime);
  
  /// Calculate days difference between two dates
  static int daysBetween(DateTime from, DateTime to) {
    from = DateTime(from.year, from.month, from.day);
    to = DateTime(to.year, to.month, to.day);
    return (to.difference(from).inHours / 24).round();
  }
  
  /// Check if payment is due soon (within 3 days)
  static bool isPaymentDueSoon(DateTime dueDate) {
    final now = DateTime.now();
    final days = daysBetween(now, dueDate);
    return days >= 0 && days <= 3;
  }
  
  /// Check if payment is overdue
  static bool isPaymentOverdue(DateTime dueDate) => DateTime.now().isAfter(dueDate);
  
  /// Get appropriate color for transaction amount based on type
  static int getAmountColor(String transactionType, {bool isDark = false}) {
    final t = transactionType.toLowerCase();
    if (t.contains('deposit') || t.contains('credit')) {
      return isDark ? 0xFF81C784 : 0xFF388E3C;
    } else if (t.contains('withdrawal') || t.contains('debit')) {
      return isDark ? 0xFFE57373 : 0xFFD32F2F;
    } else {
      return isDark ? 0xFFFFB74D : 0xFFF57C00;
    }
  }
  
  /// Validate transaction amount
  static String? validateAmount(String amount, double maxAmount) {
    if (amount.isEmpty) return 'Amount is required';
    final parsed = double.tryParse(amount.replaceAll(',', ''));
    if (parsed == null) return 'Please enter a valid amount';
    if (parsed <= 0) return 'Amount must be greater than zero';
    if (parsed > maxAmount) return 'Amount cannot exceed ${formatCurrency(maxAmount)}';
    return null;
  }
}
✏️ sacco_mobile/lib/features/savings/views/deposit_screen.dart

Add import at the top:

import '../../../core/utils/transaction_utils.dart';

Replace the inline NumberFormat usage for current balance with:

'Current Balance: ${TransactionUtils.formatCurrency(viewModel.savingsAccount?.balance ?? 0)}'

Refactor the _validateAmount method to:

String? _validateAmount(String value) {
  return TransactionUtils.validateAmount(value, double.infinity);
}
✏️ sacco_mobile/lib/features/loans/views/loan_list_screen.dart

Replace the top-level Consumer with a Builder that obtains viewModel once (listen: false) and uses nested Consumers for the refresh button and body.


Wrap IconButton in a Consumer to rebuild only when state changes, and wrap _buildBody(viewModel) in its own Consumer.


Replace TabBarView with an IndexedStack keyed on _tabController.index and wrap each tab child in Visibility(maintainState: true).


In initState, add a listener to _tabController to call setState on index change, and load loans post-frame.

➕ sacco_mobile/lib/features/loans/README.md

Create a new file README.md under sacco_mobile/lib/features/loans with the following content:

# Loan Management Features

This module provides comprehensive loan management capabilities for the Sacco Mobile application, including loan application, loan listing, and loan repayment.

## Features

### Loan Application
- Multi-step loan application form
- Different loan types with varying terms and interest rates
- Instant calculation of monthly payments and total interest
- Loan purpose, collateral, and employment information collection

### Loan Listing
- View all loans with filtering options
- Active loans tab for quick access to current loans
- Loan history to track past applications
- Detailed loan status with progress indicators

### Loan Details
- Comprehensive loan information display
- Payment schedule and outstanding balance
- Due date alerts and missed payment notifications
- Quick access to make payments

### Loan Repayment
- Simple payment submission process
- Multiple payment method options
- Transaction reference tracking
- Penalty calculation for late payments

## How to Use

1. **Apply for a Loan**:
   - Navigate to the Loan screen
   - Tap on "Apply for Loan" button
   - Follow the multi-step application process
   - Review and submit your application

2. **View Your Loans**:
   - Navigate to the Loan screen
   - Browse the tabs to see Active, All, or History
   - Tap on any loan to view details

3. **Make a Loan Repayment**:
   - Open a loan from the loan list
   - Tap on "Make Payment" button
   - Enter payment amount and select payment method
   - Confirm payment details

4. **Track Loan Status**:
   - Use progress bars on loan cards to see repayment progress
   - Check payment due dates and outstanding amounts
   - View complete loan activity timeline in loan details

## Technical Implementation

The loan features use a ViewModel pattern to separate business logic from UI:
- `LoanApplicationViewModel`: Manages loan application state and submission
- `LoanListViewModel`: Handles loan listings and filtering
- `LoanRepaymentViewModel`: Controls loan repayment process

These view models connect to the `LoanRepository` which handles data operations with the backend services.
✏️ sacco_mobile/lib/features/loans/viewmodels/loan_application_viewmodel.dart

In the AppError catch block, replace _errorMessage = e.message; with _errorMessage = _formatErrorMessage(e);


In the generic catch block, replace the message with:

We couldn't process your loan application right now. Please check your connection and try again in a few moments.

After the closing brace of submitLoanApplication, add the helper method:

  /// Formats error messages to be more user-friendly
  String _formatErrorMessage(AppError error) {
    switch (error.code) {
      case 'validation_error':
        return 'Please check your application details: ${error.message}';
      case 'insufficient_income':
        return 'Your application was declined due to insufficient income. The minimum required monthly income for this loan type is ${error.details?['required_income'] ?? 'higher than provided'}.';
      case 'existing_loans':
        return 'You have existing loans that need to be paid down before applying for a new loan.';
      case 'server_error':
        return 'Our system is currently unavailable. Please try again later.';
      case 'network_error':
        return 'Please check your internet connection and try again.';
      default:
        return error.message;
    }
  }
✏️ sacco_mobile/lib/features/loans/views/loan_application_screen.dart

Update BoxDecoration to use color: Colors.red[50], add border: Border.all(color: Colors.red[300]!)


Replace the Text('Error') widget with:

Row(
  children: [
    Icon(Icons.error_outline, color: Colors.red[700], size: 18),
    SizedBox(width: 8),
    Text(
      'Application Error',
      style: TextStyle(fontWeight: FontWeight.bold, color: Colors.red[900]),
    ),
  ],
),

Add a conditional guidance Text below the error message:

if (errorMessage.contains('income') || errorMessage.contains('existing loans'))
  Text(
    'Consider speaking with a Sacco representative for guidance.',
    style: TextStyle(fontStyle: FontStyle.italic, fontSize: 12, color: Colors.red[800]),
  ),
✏️ sacco_mobile/lib/features/savings/viewmodels/savings_account_viewmodel.dart

At the top of makeDeposit, check _savingsAccount == null, set _state = SavingsAccountState.error, _errorMessage = 'Please load your savings account first.', call notifyListeners(), then return false;


After await _savingsRepository.makeDeposit(...), perform optimistic update:

final updatedAccount = _savingsAccount!.copyWith(
  balance: _savingsAccount!.balance + amount,
  lastTransactionDate: DateTime.now(),
);
_savingsAccount = updatedAccount;
notifyListeners();

In the AppError catch block, replace _errorMessage = e.message; with _errorMessage = _getTransactionErrorMessage(e, isDeposit: true);


In the generic catch block, set _errorMessage = 'Your deposit couldn\'t be processed. Please check your connection and try again. If the issue persists, contact support.';


After makeDeposit, add helper:

String _getTransactionErrorMessage(AppError error, {bool isDeposit = true}) {
  final transactionType = isDeposit ? 'deposit' : 'withdrawal';
  switch (error.code) {
    case 'network_error':
      return 'Network connection issue. Your $transactionType may have succeeded but we couldn\'t confirm it. Please check your account balance before trying again.';
    case 'insufficient_funds':
      return 'Insufficient funds for this withdrawal.';
    case 'invalid_amount':
      return 'The amount entered is invalid. Please check and try again.';
    case 'account_locked':
      return 'Your account is temporarily locked. Please contact customer support.';
    case 'service_unavailable':
      return 'The service is currently unavailable. Please try again later.';
    case 'duplicate_transaction':
      return 'This appears to be a duplicate $transactionType. Please check your recent transactions before trying again.';
    default:
      return error.message;
  }
}
✏️ sacco_mobile/lib/features/profile/views/document_upload_screen.dart

Replace the ElevatedButton child with:

AnimatedSwitcher(
  duration: Duration(milliseconds: 300),
  child: isUploading 
    ? Row(
        key: ValueKey('loading'),
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Container(
            width: 18,
            height: 18,
            child: CircularProgressIndicator(
              strokeWidth: 2.5,
              valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
            ),
          ),
          SizedBox(width: 12),
          Text('Uploading... Please wait'),
        ],
      )
    : Row(
        key: ValueKey('upload'),
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Icon(Icons.upload_file),
          SizedBox(width: 8),
          Text('Upload ${widget.documentTitle}'),
        ],
      ),
)

Adjust onPressed to onPressed: _selectedFile == null ? null : isUploading ? null : () => _uploadDocument(viewModel) and set disabledBackgroundColor accordingly.


Change _buildFilePreview() signature to _buildFilePreview(bool isUploading), wrap the preview in Opacity(opacity: isUploading ? 0.7 : 1.0), disable the close button when uploading, and overlay a centered uploading indicator using Positioned.fill.


Enhance _buildEmptyPreview() to include:

SizedBox(height: 8),
Text(
  'Tap Gallery or Camera below',
  style: TextStyle(color: Colors.grey[500], fontSize: 12),
),
✏️ sacco_mobile/lib/features/loans/views/loan_application_screen.dart

Change _validateForm() signature from bool to Map<String, String?> and initialize Map<String, String?> errors = {}; instead of immediate returns.


Replace each ScaffoldMessenger.of(context).showSnackBar(...) and return false; with errors['fieldName'] = 'Error message'; and remove early returns.


After the switch, if errors.isNotEmpty, show the first error in a SnackBar and return errors; otherwise return an empty map.


Update _nextPage() to:

Map<String, String?> errors = _validateForm();
if (errors.isEmpty) {
  // proceed to next page or submit
}

At the end of the class, add helper widgets _buildTextField and _buildDropdown that accept an error parameter and render inline errorText on the field.

➕ sacco_mobile/lib/features/core/utils/transaction_utils.dart

Create a new file transaction_utils.dart with the following class:

import 'package:intl/intl.dart';

/// Utility class for formatting and validating transaction-related data
class TransactionUtils {
  static final NumberFormat currencyFormatter = NumberFormat.currency(locale: 'en_UG', symbol: 'UGX ', decimalDigits: 0);
  static final DateFormat dateFormatter = DateFormat('dd MMM yyyy');
  static final DateFormat timeFormatter = DateFormat('HH:mm');
  static final DateFormat dateTimeFormatter = DateFormat('dd MMM yyyy, HH:mm');

  static String formatCurrency(double amount) => currencyFormatter.format(amount);
  static String formatDate(DateTime date) => dateFormatter.format(date);
  static String formatTime(DateTime time) => timeFormatter.format(time);
  static String formatDateTime(DateTime dateTime) => dateTimeFormatter.format(dateTime);

  static int daysBetween(DateTime from, DateTime to) {
    from = DateTime(from.year, from.month, from.day);
    to = DateTime(to.year, to.month, to.day);
    return (to.difference(from).inHours / 24).round();
  }

  static bool isPaymentDueSoon(DateTime dueDate) {
    final days = daysBetween(DateTime.now(), dueDate);
    return days >= 0 && days <= 3;
  }

  static bool isPaymentOverdue(DateTime dueDate) => DateTime.now().isAfter(dueDate);

  static int getAmountColor(String transactionType, {bool isDark = false}) {
    final type = transactionType.toLowerCase();
    if (type.contains('deposit') || type.contains('credit')) {
      return isDark ? 0xFF81C784 : 0xFF388E3C;
    } else if (type.contains('withdrawal') || type.contains('debit')) {
      return isDark ? 0xFFE57373 : 0xFFD32F2F;
    } else {
      return isDark ? 0xFFFFB74D : 0xFFF57C00;
    }
  }

  static String? validateAmount(String amount, double maxAmount) {
    if (amount.isEmpty) return 'Amount is required';
    final parsed = double.tryParse(amount.replaceAll(',', ''));
    if (parsed == null) return 'Please enter a valid amount';
    if (parsed <= 0) return 'Amount must be greater than zero';
    if (parsed > maxAmount) return 'Amount cannot exceed ${formatCurrency(maxAmount)}';
    return null;
  }
}
✏️ sacco_mobile/lib/features/savings/views/deposit_screen.dart

At the top of the file, add:

import '../../../core/utils/transaction_utils.dart';

Replace the inline NumberFormat.currency usage in the balance display with:

TransactionUtils.formatCurrency(viewModel.savingsAccount?.balance ?? 0)

Update the _validateAmount method to:

String? _validateAmount(String value) {
  return TransactionUtils.validateAmount(value, double.infinity);
}
✏️ sacco_mobile/lib/features/loans/views/loan_list_screen.dart

Replace the top-level Consumer wrapping the Scaffold with a Builder that gets the viewModel once via Provider.of<LoanListViewModel>(context, listen: false) and uses separate Consumer widgets for the refresh button and the body.


Replace the TabBarView with:

IndexedStack(
  index: _tabController.index,
  children: [
    Visibility(visible: _tabController.index == 0, maintainState: true, child: _buildActiveLoansTab(viewModel)),
    Visibility(visible: _tabController.index == 1, maintainState: true, child: _buildAllLoansTab(viewModel)),
    Visibility(visible: _tabController.index == 2, maintainState: true, child: _buildLoanHistoryTab(viewModel)),
  ],
)

In initState(), initialize _tabController, add a listener to call setState() on index changes, and schedule viewModel.loadLoans() in a post-frame callback.

A summary of the context that CodeRabbit is considering across the codebase:

We verified that AppError defines the fields we rely on (code, message, userFriendlyMessage, details) for formatting error messages.

We located the submitLoanApplication method to identify the existing catch blocks that need enhancement.

Locate the definition of AppError to understand its fields (code, message, details, userFriendlyMessage) for enhanced error formatting.

Find the LoanRepository interface to confirm its methods for the README’s Technical Implementation section.

Locate the SavingsAccountViewModel to apply transaction error handling enhancements.

Locate DocumentUploadScreen to insert improved loading indicators in the _buildBody method.

Locate LoanApplicationScreen to modify submission error UI and upgrade form validation logic.

Locate DepositScreen in the savings feature to integrate the newly created TransactionUtils utility class.

Locate LoanListScreen to apply more granular Consumer and Builder widgets for optimized UI rebuilds.

We need to confirm the existence of the README.md in the loans feature to avoid overwriting existing documentation.

Verify the AppError implementation to ensure our proposed error formatting uses available fields.

Ensure the intl package is available for our new TransactionUtils class.

Locate the submitLoanApplication method in LoanApplicationViewModel to apply enhanced error handling.

Identify current error UI and validation logic in LoanApplicationScreen to upgrade both.

Locate the makeDeposit implementation in SavingsAccountViewModel for optimistic updates and custom messages.

Locate the body builder in DocumentUploadScreen to integrate animated loading and UI disablement.

Find inline currency formatting in DepositScreen to replace with TransactionUtils.

Locate the initial Consumer usage in LoanListScreen to optimize rebuilds.

Locate the existing _validateForm method in LoanApplicationScreen.dart to determine where to apply the updated signature and logic.

We need to locate the shared TransactionUtils class to confirm its path and available formatting methods.

We must find every occurrence of the in‑file currencyFormat field and its usages to know where to apply the refactor.

We need to locate the LoanListViewModel implementation to confirm that it defines both loadLoans and refreshLoans methods, ensuring our UI rebuild optimization aligns with the existing API.

We inspect loan_list_viewmodel.dart to verify the names of the methods for loading and refreshing loans.

Verify whether a README already exists for the loan management feature so we know if we need to create it.

Locate the submitLoanApplication method to enhance its error handling logic.

Find the widget that displays submission errors in the loan application screen to improve its UI and messaging.

Locate makeDeposit in the SavingsAccountViewModel to improve transaction error handling.

Inspect DocumentUploadScreen to integrate enhanced loading indicators around the preview and actions.

Find the form validation logic to change it from instant SnackBars to inline field errors.

Ensure transaction utilities don’t already exist before adding a new shared helper.

Locate usage of NumberFormat.currency in deposit_screen.dart to swap in the new helper.

Identify the top‑level Consumer in loan_list_screen.dart to convert it to more granular Consumers.

We search for the AppError class definition to inspect its fields (code, message, details) so we can correctly format user‑friendly messages.

We verify the ProfileState enum values to confirm that 'uploading' and 'error' states exist for enhancing the document upload UI.

Locate the submitLoanApplication method in LoanApplicationViewModel to apply the error‑handling diff.

Locate the _buildSubmissionError widget in LoanApplicationScreen to update its UI styling and messaging.

Locate the makeDeposit method in SavingsAccountViewModel to introduce optimistic updates and improved error messages.

Locate the _buildBody method in DocumentUploadScreen to insert enhanced loading indicators.

Locate the form validation logic in LoanApplicationScreen to convert to map‑based errors and add inline feedback helpers.

Confirm that the intl package is declared so NumberFormat and DateFormat are available for our utility class.

Locate DepositScreen in savings/views to refactor its currency formatting and validation to use TransactionUtils.

Locate the build method in LoanListScreen to replace a single Consumer with granular Consumers and an IndexedStack.

List all files under the loans feature to prepare for creating a README and identifying files to modify.

Verify whether a README.md already exists in the loans feature.

Locate the definition and length of LoanApplicationViewModel to scope modifications for enhanced error feedback.

Locate submit error UI in loan_application_screen.dart to apply styling and feedback enhancements.

Locate the form validation logic in loan_application_screen.dart to implement real-time field errors and helper methods.

Locate SavingsAccountViewModel to improve transaction error handling and optimistic updates.

Locate DocumentUploadScreen to add better loading indicators and disable interactions during upload.

Check for existing transaction utils to avoid duplicate implementations.

Locate DepositScreen to use the new TransactionUtils.formatCurrency and validation.

Locate LoanListScreen to apply more granular Consumers and prevent unnecessary rebuilds.

Check if a README for loan features already exists to avoid duplicating documentation.

Locate the submitLoanApplication method in LoanApplicationViewModel to apply enhanced error formatting.

Find the _buildSubmissionError widget to enhance its styling and messaging.

Locate makeDeposit in SavingsAccountViewModel to implement optimistic updates and detailed error messages.

Find the document upload body to integrate animated loading indicators and disable interactions while uploading.

Locate the form validation logic in LoanApplicationScreen to convert snackbars to inline error tracking and real-time validation.

Check whether TransactionUtils already exists to avoid duplicate utility creation.

Locate DepositScreen to replace inline NumberFormat usage and validation logic.

Locate LoanListScreen build method to apply UI rebuild optimizations.

  • Error while executing edits - (🔄 Check again to try again)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant