-
Notifications
You must be signed in to change notification settings - Fork 7
Develop #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
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.
WalkthroughThis 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 Changes
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)
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
Poem
Tip ⚡💬 Agentic Chat (Pro Plan, General Availability)
✨ Finishing Touches
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
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)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this 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 suggestionAvoid recreating
ProfileViewModelevery frame – lift it out ofbuild.
ChangeNotifierProvider(create: …)insidebuildleads to a fresh ViewModel each rebuild, wiping state (memberProfile, tab data, errors).
Move it toinitStateor 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
FileSelectorPluginare 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 getto 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 calculationIf
termMonthswere ever0(e.g., corrupt data), this would raise aIntegerDivisionByZeroException.- 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
_buildLoanCardand_buildHistoryLoanCardmethods are quite large and could be extracted into their own widget classes to improve maintainability and testability.Create separate files for these components:
loan_card.dartfor the active loan cardhistory_loan_card.dartfor the loan history cardThis 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
LoanProgressIndicatorcould 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
_getStatusColormethod 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.digitsOnlyblocks “12.50” or “1000.75”, yet the back‑end accepts fractional values (double.parseis 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: 0will 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
_submitWithdrawalawaitsmakeWithdrawalafter showing the confirmation dialog, the user can spam the button beforestateswitches toloading. Disable the button immediately after the dialog is accepted to guard against duplicate requests, or track an internal_isSubmittingflag.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 expensiveNumberFormat/DateFormatobjects every build.
intlformatters 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/Expandedwidgets or useLayoutBuilderto 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 % 7makes Sunday the first day; most locales consider Monday the first weekday.
Prefer theDateTime.mondayconstant or theintlpackage’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 returnnullwhen the primary swatch is overridden.
Either keep the nullable type and provide a fallback or useColorSchemefrom the app theme.sacco_mobile/lib/features/savings/viewmodels/transaction_viewmodel.dart (1)
67-70: Setting state toloadingon every pagination request causes UI flicker.When
_page > 1, consider a separateloadingMoreflag or keep_stateassuccessand expose a booleanisPaginatingto 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.11sacco_mobile/lib/features/profile/views/document_upload_screen.dart (2)
11-18: Prefer a strongly‑typed enum over string literals fordocumentType.Using the raw strings
'ID'/'PHOTO'ties correctness to spelling and letter‑case.
A small typo later ('Photo') would silently bypass yourifin_uploadDocumentand 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 != nullandstate == loading.
During an upload,_selectedFileis 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 typedDocumentTyperather than raw strings when navigating.As noted on the upload screen, using string literals like
'PHOTO'/'ID'is error‑prone.
After introducing theDocumentTypeenum, 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 usingIntrinsicWidthorFlexwithExpanded(flex: …)instead of a fixed pixel width, and wrap the whole row inDirectionalityto respect RTL.Not urgent, but improves accessibility and internationalisation.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
sacco_mobile/pubspec.lockis 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 goodThe addition of the
file_selector_macosplugin 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 goodThe
file_selector_linuxplugin 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 goodThe
file_selector_windowsplugin 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 goodThe
file_selector_windowsplugin 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
transactionTypeintention (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_viewandimage_pickerpackages 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.
| void initState() { | ||
| super.initState(); | ||
| // Initialize with default values if needed | ||
| WidgetsBinding.instance.addPostFrameCallback((_) { | ||
| final viewModel = context.read<LoanApplicationViewModel>(); | ||
| viewModel.resetState(); | ||
| }); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
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`.| @override | ||
| Widget build(BuildContext context) { | ||
| return ChangeNotifierProvider( | ||
| create: (_) => getIt<LoanApplicationViewModel>(), | ||
| child: Consumer<LoanApplicationViewModel>( | ||
| builder: (context, viewModel, child) { |
There was a problem hiding this comment.
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.
| // First check if loan is already in the list | ||
| final existingLoan = _loans.firstWhere( | ||
| (loan) => loan.id == loanId, | ||
| orElse: () => null as Loan, | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| @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; | ||
| }); | ||
| } | ||
| }); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
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.
| @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; | |
| }); | |
| } | |
| }); | |
| } |
| 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, | ||
| ); | ||
| } |
There was a problem hiding this comment.
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(),
+ );| 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, | ||
| ); | ||
| } |
There was a problem hiding this comment.
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.
| 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, | |
| ); | |
| } |
| 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(); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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(); | |
| } | |
| } | |
| } |
| 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, | ||
| ), | ||
| ), | ||
| ], | ||
| ); | ||
| } |
There was a problem hiding this comment.
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.
| 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), | |
| ), | |
| ), | |
| ], | |
| ); | |
| }, | |
| ); | |
| } |
| return ChangeNotifierProvider( | ||
| create: (_) => getIt<ProfileViewModel>(), | ||
| child: Consumer<ProfileViewModel>( | ||
| builder: (context, viewModel, child) { | ||
| return Scaffold( | ||
| appBar: AppBar( | ||
| title: Text('Upload ${widget.documentTitle}'), | ||
| ), | ||
| body: _buildBody(viewModel), | ||
| ); | ||
| }, | ||
| ), | ||
| ); | ||
| } |
There was a problem hiding this comment.
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.
| 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(); | ||
| } |
There was a problem hiding this comment.
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:
- Clearing
_selectedFileonly on success (so the user can retry). - Showing a SnackBar or displaying
viewModel.errorMessage. - 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.
| 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, | |
| ), | |
| ); | |
| } | |
| } |
|
@coderabbitai create a PR with suggested changes |
|
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
Another agent will come along to implement these changes for you in a new pull request. The plan includes:
Would you like me to modify this plan or focus on particular areas of improvement? ⏳ Preparing changes... |
|
A summary of the edits CodeRabbit can apply:
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.dartReplace 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.dartImmediately 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.mdCreate 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.dartOpen the file and locate the submitLoanApplication method. In the AppError catch block, replace _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.dartFind 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.dartConvert the existing 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 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. 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 In the _errorMessage = _getTransactionErrorMessage(e, isDeposit: true);and in the generic _errorMessage = 'Your deposit couldn\'t be processed. Please check your connection and try again. If the issue persists, contact support.';Below /// 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.dartUpdate the file preview call to pass _selectedFile != null
? _buildFilePreview(isUploading)
: _buildEmptyPreview()Wrap the upload button’s child in 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 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.dartCreate the file 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.dartAdd at the top: import '../../../core/utils/transaction_utils.dart';In the TransactionUtils.formatCurrency(viewModel.savingsAccount?.balance ?? 0)for the current balance display. Replace the local String? _validateAmount(String value) {
return TransactionUtils.validateAmount(value, double.infinity);
}✏️ sacco_mobile/lib/features/loans/views/loan_list_screen.dartReplace the top-level Wrap the ➕ sacco_mobile/lib/features/loans/README.mdInsert 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.dartReplace 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.dartUpdate _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.dartInsert 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.dartRefactor 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.dartCreate 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.dartAdd 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.dartReplace 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.dartReplace the existing 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 if (_validateForm()) {
// ...
}and replace it with: final errors = _validateForm();
if (errors.isEmpty) {
// existing page‑advance or submission logic
}Remove any inline ➕ sacco_mobile/lib/features/loans/views/loan_application_screen.dartAfter the last widget‑building method and before the final closing brace of // 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.dartAt 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.dartReplace 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.mdAdd 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.dartIn the In the generic 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 /// 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.dartModify the - 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 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.dartBefore returning false when _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 In the generic 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.dartIntroduce at the top of final bool isUploading = viewModel.state == ProfileState.uploading;Pass 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 ✏️ sacco_mobile/lib/features/loans/views/loan_application_screen.dartChange the signature of At the end of Add helper widgets ➕ sacco_mobile/lib/features/core/utils/transaction_utils.dartAdd 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.dartAdd 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 return TransactionUtils.validateAmount(value, double.infinity);✏️ sacco_mobile/lib/features/loans/views/loan_list_screen.dartReplace the top‑level Change the In ➕ sacco_mobile/lib/features/loans/README.mdCopy 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.dartIn submitLoanApplication's AppError catch block, replace In the general catch block, replace the default message with: 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.dartReplace 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.dartAt 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 = _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.dartChange 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.dartChange In Add helper methods ➕ sacco_mobile/lib/features/core/utils/transaction_utils.dartCreate 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.dartAt the top of the file, add: import '../../../core/utils/transaction_utils.dart';In _buildBody, replace the inline NumberFormat.currency(...) usage with In the ✏️ sacco_mobile/lib/features/loans/views/loan_list_screen.dartReplace 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.mdInsert 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.dartIn the AppError catch block inside submitLoanApplication, replace In the generic catch block, update the default message to: 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.dartUpdate 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.dartChange the signature of _validateForm from 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 ✏️ sacco_mobile/lib/features/savings/viewmodels/savings_account_viewmodel.dartIn makeDeposit, before returning false when _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 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.dartChange the call to _buildFilePreview to pass 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.dartCreate 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.dartAdd 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 String? _validateAmount(String value) {
return TransactionUtils.validateAmount(value, double.infinity);
}✏️ sacco_mobile/lib/features/loans/views/loan_list_screen.dartReplace 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.mdCreate a new file README.md under sacco_mobile/lib/features/loans with the following content: ✏️ sacco_mobile/lib/features/loans/viewmodels/loan_application_viewmodel.dartIn the AppError catch block, replace In the generic catch block, replace the message with: After the closing brace of submitLoanApplication, add the helper method: ✏️ sacco_mobile/lib/features/loans/views/loan_application_screen.dartUpdate BoxDecoration to use color: Colors.red[50], add border: Border.all(color: Colors.red[300]!) Replace the Text('Error') widget with: Add a conditional guidance Text below the error message: ✏️ sacco_mobile/lib/features/savings/viewmodels/savings_account_viewmodel.dartAt the top of makeDeposit, check After In the AppError catch block, replace In the generic catch block, set After makeDeposit, add helper: ✏️ sacco_mobile/lib/features/profile/views/document_upload_screen.dartReplace the ElevatedButton child with: Adjust Change Enhance ✏️ sacco_mobile/lib/features/loans/views/loan_application_screen.dartChange Replace each After the switch, if Update At the end of the class, add helper widgets ➕ sacco_mobile/lib/features/core/utils/transaction_utils.dartCreate a new file transaction_utils.dart with the following class: ✏️ sacco_mobile/lib/features/savings/views/deposit_screen.dartAt the top of the file, add: Replace the inline NumberFormat.currency usage in the balance display with: Update the ✏️ sacco_mobile/lib/features/loans/views/loan_list_screen.dartReplace the top-level Consumer wrapping the Scaffold with a Builder that gets the viewModel once via Replace the TabBarView with: In A summary of the context that CodeRabbit is considering across the codebase:
|
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_mobileapplication. 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
LoanApplicationViewModelto manage loan application states, fetch loan types, submit applications, calculate payments, and handle errors. It includes utility methods for monthly payment and total interest calculations. (loan_application_viewmodel.dart, sacco_mobile/lib/features/loans/viewmodels/loan_application_viewmodel.dartR1-R186)LoanApplicationScreenwith a multi-step form for loan applications, including loan type selection, purpose, collateral, and employment details. It integrates with the view model for state management and submission logic. (loan_application_screen.dart, sacco_mobile/lib/features/loans/views/loan_application_screen.dartR1-R161)Loan List Management
LoanListViewModelto handle the retrieval and filtering of loans, including active loans, and manage states like loading, success, and error. (loan_list_viewmodel.dart, sacco_mobile/lib/features/loans/viewmodels/loan_list_viewmodel.dartR1-R102)Loan Repayment Management
LoanRepaymentViewModelto manage loan repayment processes, including payment submission, balance updates, and penalty calculations. It also tracks repayment state and errors. (loan_repayment_viewmodel.dart, sacco_mobile/lib/features/loans/viewmodels/loan_repayment_viewmodel.dartR1-R128)Document Upload
DocumentUploadScreenfor uploading user documents (e.g., ID or profile photo) with image preview, selection (from gallery or camera), and upload functionality. It integrates withProfileViewModelfor state management and file upload. (document_upload_screen.dart, sacco_mobile/lib/features/profile/views/document_upload_screen.dartR1-R270)Summary by CodeRabbit
photo_viewandimage_picker.