diff --git a/apps/mobile/packages/domain/lib/krow_domain.dart b/apps/mobile/packages/domain/lib/krow_domain.dart index bbe513ae..d3b2ac2a 100644 --- a/apps/mobile/packages/domain/lib/krow_domain.dart +++ b/apps/mobile/packages/domain/lib/krow_domain.dart @@ -53,6 +53,10 @@ export 'src/entities/financial/invoice_item.dart'; export 'src/entities/financial/invoice_decline.dart'; export 'src/entities/financial/staff_payment.dart'; export 'src/entities/financial/payment_summary.dart'; +export 'src/entities/financial/bank_account/bank_account.dart'; +export 'src/entities/financial/bank_account/business_bank_account.dart'; +export 'src/entities/financial/bank_account/staff_bank_account.dart'; +export 'src/adapters/financial/bank_account/bank_account_adapter.dart'; // Profile export 'src/entities/profile/staff_document.dart'; @@ -68,7 +72,6 @@ export 'src/entities/ratings/business_staff_preference.dart'; // Staff Profile export 'src/entities/profile/emergency_contact.dart'; -export 'src/entities/profile/bank_account.dart'; export 'src/entities/profile/accessibility.dart'; export 'src/entities/profile/schedule.dart'; diff --git a/apps/mobile/packages/domain/lib/src/adapters/financial/bank_account/bank_account_adapter.dart b/apps/mobile/packages/domain/lib/src/adapters/financial/bank_account/bank_account_adapter.dart new file mode 100644 index 00000000..167d1126 --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/adapters/financial/bank_account/bank_account_adapter.dart @@ -0,0 +1,21 @@ +import '../../../entities/financial/bank_account/business_bank_account.dart'; + +/// Adapter for [BusinessBankAccount] to map data layer values to domain entity. +class BusinessBankAccountAdapter { + /// Maps primitive values to [BusinessBankAccount]. + static BusinessBankAccount fromPrimitives({ + required String id, + required String bank, + required String last4, + required bool isPrimary, + DateTime? expiryTime, + }) { + return BusinessBankAccount( + id: id, + bankName: bank, + last4: last4, + isPrimary: isPrimary, + expiryTime: expiryTime, + ); + } +} diff --git a/apps/mobile/packages/domain/lib/src/adapters/profile/bank_account_adapter.dart b/apps/mobile/packages/domain/lib/src/adapters/profile/bank_account_adapter.dart index 6b285b8a..133da163 100644 --- a/apps/mobile/packages/domain/lib/src/adapters/profile/bank_account_adapter.dart +++ b/apps/mobile/packages/domain/lib/src/adapters/profile/bank_account_adapter.dart @@ -1,9 +1,9 @@ -import '../../entities/profile/bank_account.dart'; +import '../../entities/financial/bank_account/staff_bank_account.dart'; -/// Adapter for [BankAccount] to map data layer values to domain entity. +/// Adapter for [StaffBankAccount] to map data layer values to domain entity. class BankAccountAdapter { - /// Maps primitive values to [BankAccount]. - static BankAccount fromPrimitives({ + /// Maps primitive values to [StaffBankAccount]. + static StaffBankAccount fromPrimitives({ required String id, required String userId, required String bankName, @@ -13,7 +13,7 @@ class BankAccountAdapter { String? sortCode, bool? isPrimary, }) { - return BankAccount( + return StaffBankAccount( id: id, userId: userId, bankName: bankName, @@ -26,25 +26,25 @@ class BankAccountAdapter { ); } - static BankAccountType _stringToType(String? value) { - if (value == null) return BankAccountType.checking; + static StaffBankAccountType _stringToType(String? value) { + if (value == null) return StaffBankAccountType.checking; try { // Assuming backend enum names match or are uppercase - return BankAccountType.values.firstWhere( - (e) => e.name.toLowerCase() == value.toLowerCase(), - orElse: () => BankAccountType.other, + return StaffBankAccountType.values.firstWhere( + (StaffBankAccountType e) => e.name.toLowerCase() == value.toLowerCase(), + orElse: () => StaffBankAccountType.other, ); } catch (_) { - return BankAccountType.other; + return StaffBankAccountType.other; } } /// Converts domain type to string for backend. - static String typeToString(BankAccountType type) { + static String typeToString(StaffBankAccountType type) { switch (type) { - case BankAccountType.checking: + case StaffBankAccountType.checking: return 'CHECKING'; - case BankAccountType.savings: + case StaffBankAccountType.savings: return 'SAVINGS'; default: return 'CHECKING'; diff --git a/apps/mobile/packages/domain/lib/src/entities/financial/bank_account/bank_account.dart b/apps/mobile/packages/domain/lib/src/entities/financial/bank_account/bank_account.dart new file mode 100644 index 00000000..04af8402 --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/financial/bank_account/bank_account.dart @@ -0,0 +1,27 @@ +import 'package:equatable/equatable.dart'; + +/// Abstract base class for all types of bank accounts. +abstract class BankAccount extends Equatable { + /// Creates a [BankAccount]. + const BankAccount({ + required this.id, + required this.bankName, + required this.isPrimary, + this.last4, + }); + + /// Unique identifier. + final String id; + + /// Name of the bank or provider. + final String bankName; + + /// Whether this is the primary payment method. + final bool isPrimary; + + /// Last 4 digits of the account/card. + final String? last4; + + @override + List get props => [id, bankName, isPrimary, last4]; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/financial/bank_account/business_bank_account.dart b/apps/mobile/packages/domain/lib/src/entities/financial/bank_account/business_bank_account.dart new file mode 100644 index 00000000..8ad3d48e --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/financial/bank_account/business_bank_account.dart @@ -0,0 +1,26 @@ +import 'bank_account.dart'; + +/// Domain model representing a business bank account or payment method. +class BusinessBankAccount extends BankAccount { + /// Creates a [BusinessBankAccount]. + const BusinessBankAccount({ + required super.id, + required super.bankName, + required String last4, + required super.isPrimary, + this.expiryTime, + }) : super(last4: last4); + + /// Expiration date if applicable. + final DateTime? expiryTime; + + @override + List get props => [ + ...super.props, + expiryTime, + ]; + + /// Getter for non-nullable last4 in Business context. + @override + String get last4 => super.last4!; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/financial/bank_account/staff_bank_account.dart b/apps/mobile/packages/domain/lib/src/entities/financial/bank_account/staff_bank_account.dart new file mode 100644 index 00000000..3f2f034e --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/financial/bank_account/staff_bank_account.dart @@ -0,0 +1,48 @@ +import 'bank_account.dart'; + +/// Type of staff bank account. +enum StaffBankAccountType { + /// Checking account. + checking, + + /// Savings account. + savings, + + /// Other type. + other, +} + +/// Domain entity representing a staff's bank account. +class StaffBankAccount extends BankAccount { + /// Creates a [StaffBankAccount]. + const StaffBankAccount({ + required super.id, + required this.userId, + required super.bankName, + required this.accountNumber, + required this.accountName, + required super.isPrimary, + super.last4, + this.sortCode, + this.type = StaffBankAccountType.checking, + }); + + /// User identifier. + final String userId; + + /// Full account number. + final String accountNumber; + + /// Name of the account holder. + final String accountName; + + /// Sort code (optional). + final String? sortCode; + + /// Account type. + final StaffBankAccountType type; + + @override + List get props => + [...super.props, userId, accountNumber, accountName, sortCode, type]; +} diff --git a/apps/mobile/packages/domain/lib/src/entities/profile/bank_account.dart b/apps/mobile/packages/domain/lib/src/entities/profile/bank_account.dart deleted file mode 100644 index deca9a28..00000000 --- a/apps/mobile/packages/domain/lib/src/entities/profile/bank_account.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'package:equatable/equatable.dart'; - -/// Account type (Checking, Savings, etc). -enum BankAccountType { - checking, - savings, - other, -} - -/// Represents bank account details for payroll. -class BankAccount extends Equatable { - - const BankAccount({ - required this.id, - required this.userId, - required this.bankName, - required this.accountNumber, - required this.accountName, - this.sortCode, - this.type = BankAccountType.checking, - this.isPrimary = false, - this.last4, - }); - /// Unique identifier. - final String id; - - /// The [User] owning the account. - final String userId; - - /// Name of the bank. - final String bankName; - - /// Account number. - final String accountNumber; - - /// Name on the account. - final String accountName; - - /// Sort code (if applicable). - final String? sortCode; - - /// Type of account. - final BankAccountType type; - - /// Whether this is the primary account. - final bool isPrimary; - - /// Last 4 digits. - final String? last4; - - @override - List get props => [id, userId, bankName, accountNumber, accountName, sortCode, type, isPrimary, last4]; -} \ No newline at end of file diff --git a/apps/mobile/packages/features/client/billing/lib/src/billing_module.dart b/apps/mobile/packages/features/client/billing/lib/src/billing_module.dart index 8c639cb3..1acdc69b 100644 --- a/apps/mobile/packages/features/client/billing/lib/src/billing_module.dart +++ b/apps/mobile/packages/features/client/billing/lib/src/billing_module.dart @@ -3,6 +3,7 @@ import 'package:krow_core/core.dart'; import 'data/repositories_impl/billing_repository_impl.dart'; import 'domain/repositories/billing_repository.dart'; +import 'domain/usecases/get_bank_accounts.dart'; import 'domain/usecases/get_current_bill_amount.dart'; import 'domain/usecases/get_invoice_history.dart'; import 'domain/usecases/get_pending_invoices.dart'; @@ -21,6 +22,7 @@ class BillingModule extends Module { i.addSingleton(BillingRepositoryImpl.new); // Use Cases + i.addSingleton(GetBankAccountsUseCase.new); i.addSingleton(GetCurrentBillAmountUseCase.new); i.addSingleton(GetSavingsAmountUseCase.new); i.addSingleton(GetPendingInvoicesUseCase.new); @@ -30,6 +32,7 @@ class BillingModule extends Module { // BLoCs i.addSingleton( () => BillingBloc( + getBankAccounts: i.get(), getCurrentBillAmount: i.get(), getSavingsAmount: i.get(), getPendingInvoices: i.get(), diff --git a/apps/mobile/packages/features/client/billing/lib/src/data/repositories_impl/billing_repository_impl.dart b/apps/mobile/packages/features/client/billing/lib/src/data/repositories_impl/billing_repository_impl.dart index d0441b26..95578127 100644 --- a/apps/mobile/packages/features/client/billing/lib/src/data/repositories_impl/billing_repository_impl.dart +++ b/apps/mobile/packages/features/client/billing/lib/src/data/repositories_impl/billing_repository_impl.dart @@ -16,6 +16,23 @@ class BillingRepositoryImpl implements BillingRepository { final data_connect.DataConnectService _service; + /// Fetches bank accounts associated with the business. + @override + Future> getBankAccounts() async { + return _service.run(() async { + final String businessId = await _service.getBusinessId(); + + final fdc.QueryResult< + data_connect.GetAccountsByOwnerIdData, + data_connect.GetAccountsByOwnerIdVariables> result = + await _service.connector + .getAccountsByOwnerId(ownerId: businessId) + .execute(); + + return result.data.accounts.map(_mapBankAccount).toList(); + }); + } + /// Fetches the current bill amount by aggregating open invoices. @override Future getCurrentBillAmount() async { @@ -182,6 +199,18 @@ class BillingRepositoryImpl implements BillingRepository { ); } + BusinessBankAccount _mapBankAccount( + data_connect.GetAccountsByOwnerIdAccounts account, + ) { + return BusinessBankAccountAdapter.fromPrimitives( + id: account.id, + bank: account.bank, + last4: account.last4, + isPrimary: account.isPrimary ?? false, + expiryTime: _service.toDateTime(account.expiryTime), + ); + } + InvoiceStatus _mapInvoiceStatus( data_connect.EnumValue status, ) { diff --git a/apps/mobile/packages/features/client/billing/lib/src/domain/repositories/billing_repository.dart b/apps/mobile/packages/features/client/billing/lib/src/domain/repositories/billing_repository.dart index 4a9300d3..d631a40b 100644 --- a/apps/mobile/packages/features/client/billing/lib/src/domain/repositories/billing_repository.dart +++ b/apps/mobile/packages/features/client/billing/lib/src/domain/repositories/billing_repository.dart @@ -7,6 +7,9 @@ import '../models/billing_period.dart'; /// acting as a boundary between the Domain and Data layers. /// It allows the Domain layer to remain independent of specific data sources. abstract class BillingRepository { + /// Fetches bank accounts associated with the business. + Future> getBankAccounts(); + /// Fetches invoices that are pending approval or payment. Future> getPendingInvoices(); diff --git a/apps/mobile/packages/features/client/billing/lib/src/domain/usecases/get_bank_accounts.dart b/apps/mobile/packages/features/client/billing/lib/src/domain/usecases/get_bank_accounts.dart new file mode 100644 index 00000000..23a52f38 --- /dev/null +++ b/apps/mobile/packages/features/client/billing/lib/src/domain/usecases/get_bank_accounts.dart @@ -0,0 +1,14 @@ +import 'package:krow_core/core.dart'; +import 'package:krow_domain/krow_domain.dart'; +import '../repositories/billing_repository.dart'; + +/// Use case for fetching the bank accounts associated with the business. +class GetBankAccountsUseCase extends NoInputUseCase> { + /// Creates a [GetBankAccountsUseCase]. + GetBankAccountsUseCase(this._repository); + + final BillingRepository _repository; + + @override + Future> call() => _repository.getBankAccounts(); +} diff --git a/apps/mobile/packages/features/client/billing/lib/src/presentation/blocs/billing_bloc.dart b/apps/mobile/packages/features/client/billing/lib/src/presentation/blocs/billing_bloc.dart index ccddda07..ee88ed63 100644 --- a/apps/mobile/packages/features/client/billing/lib/src/presentation/blocs/billing_bloc.dart +++ b/apps/mobile/packages/features/client/billing/lib/src/presentation/blocs/billing_bloc.dart @@ -1,6 +1,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:krow_core/core.dart'; import 'package:krow_domain/krow_domain.dart'; +import '../../domain/usecases/get_bank_accounts.dart'; import '../../domain/usecases/get_current_bill_amount.dart'; import '../../domain/usecases/get_invoice_history.dart'; import '../../domain/usecases/get_pending_invoices.dart'; @@ -16,12 +17,14 @@ class BillingBloc extends Bloc with BlocErrorHandler { /// Creates a [BillingBloc] with the given use cases. BillingBloc({ + required GetBankAccountsUseCase getBankAccounts, required GetCurrentBillAmountUseCase getCurrentBillAmount, required GetSavingsAmountUseCase getSavingsAmount, required GetPendingInvoicesUseCase getPendingInvoices, required GetInvoiceHistoryUseCase getInvoiceHistory, required GetSpendingBreakdownUseCase getSpendingBreakdown, - }) : _getCurrentBillAmount = getCurrentBillAmount, + }) : _getBankAccounts = getBankAccounts, + _getCurrentBillAmount = getCurrentBillAmount, _getSavingsAmount = getSavingsAmount, _getPendingInvoices = getPendingInvoices, _getInvoiceHistory = getInvoiceHistory, @@ -31,6 +34,7 @@ class BillingBloc extends Bloc on(_onPeriodChanged); } + final GetBankAccountsUseCase _getBankAccounts; final GetCurrentBillAmountUseCase _getCurrentBillAmount; final GetSavingsAmountUseCase _getSavingsAmount; final GetPendingInvoicesUseCase _getPendingInvoices; @@ -52,12 +56,15 @@ class BillingBloc extends Bloc _getPendingInvoices.call(), _getInvoiceHistory.call(), _getSpendingBreakdown.call(state.period), + _getBankAccounts.call(), ]); final double savings = results[1] as double; final List pendingInvoices = results[2] as List; final List invoiceHistory = results[3] as List; final List spendingItems = results[4] as List; + final List bankAccounts = + results[5] as List; // Map Domain Entities to Presentation Models final List uiPendingInvoices = @@ -79,6 +86,7 @@ class BillingBloc extends Bloc pendingInvoices: uiPendingInvoices, invoiceHistory: uiInvoiceHistory, spendingBreakdown: uiSpendingBreakdown, + bankAccounts: bankAccounts, ), ); }, diff --git a/apps/mobile/packages/features/client/billing/lib/src/presentation/blocs/billing_state.dart b/apps/mobile/packages/features/client/billing/lib/src/presentation/blocs/billing_state.dart index d983728d..ef3ba019 100644 --- a/apps/mobile/packages/features/client/billing/lib/src/presentation/blocs/billing_state.dart +++ b/apps/mobile/packages/features/client/billing/lib/src/presentation/blocs/billing_state.dart @@ -1,4 +1,5 @@ import 'package:equatable/equatable.dart'; +import 'package:krow_domain/krow_domain.dart'; import '../../domain/models/billing_period.dart'; import '../models/billing_invoice_model.dart'; import '../models/spending_breakdown_model.dart'; @@ -28,6 +29,7 @@ class BillingState extends Equatable { this.pendingInvoices = const [], this.invoiceHistory = const [], this.spendingBreakdown = const [], + this.bankAccounts = const [], this.period = BillingPeriod.week, this.errorMessage, }); @@ -50,6 +52,9 @@ class BillingState extends Equatable { /// Breakdown of spending by category. final List spendingBreakdown; + /// Bank accounts associated with the business. + final List bankAccounts; + /// Selected period for the breakdown. final BillingPeriod period; @@ -64,6 +69,7 @@ class BillingState extends Equatable { List? pendingInvoices, List? invoiceHistory, List? spendingBreakdown, + List? bankAccounts, BillingPeriod? period, String? errorMessage, }) { @@ -74,6 +80,7 @@ class BillingState extends Equatable { pendingInvoices: pendingInvoices ?? this.pendingInvoices, invoiceHistory: invoiceHistory ?? this.invoiceHistory, spendingBreakdown: spendingBreakdown ?? this.spendingBreakdown, + bankAccounts: bankAccounts ?? this.bankAccounts, period: period ?? this.period, errorMessage: errorMessage ?? this.errorMessage, ); @@ -87,6 +94,7 @@ class BillingState extends Equatable { pendingInvoices, invoiceHistory, spendingBreakdown, + bankAccounts, period, errorMessage, ]; diff --git a/apps/mobile/packages/features/client/billing/lib/src/presentation/pages/billing_page.dart b/apps/mobile/packages/features/client/billing/lib/src/presentation/pages/billing_page.dart index 6a1c2832..4771b744 100644 --- a/apps/mobile/packages/features/client/billing/lib/src/presentation/pages/billing_page.dart +++ b/apps/mobile/packages/features/client/billing/lib/src/presentation/pages/billing_page.dart @@ -71,19 +71,20 @@ class _BillingViewState extends State { @override Widget build(BuildContext context) { - return BlocConsumer( - listener: (BuildContext context, BillingState state) { - if (state.status == BillingStatus.failure && state.errorMessage != null) { - UiSnackbar.show( - context, - message: translateErrorKey(state.errorMessage!), - type: UiSnackbarType.error, - ); - } - }, - builder: (BuildContext context, BillingState state) { - return Scaffold( - body: CustomScrollView( + return Scaffold( + body: BlocConsumer( + listener: (BuildContext context, BillingState state) { + if (state.status == BillingStatus.failure && + state.errorMessage != null) { + UiSnackbar.show( + context, + message: translateErrorKey(state.errorMessage!), + type: UiSnackbarType.error, + ); + } + }, + builder: (BuildContext context, BillingState state) { + return CustomScrollView( controller: _scrollController, slivers: [ SliverAppBar( @@ -97,7 +98,7 @@ class _BillingViewState extends State { leading: Center( child: UiIconButton.secondary( icon: UiIcons.arrowLeft, - onTap: () => Modular.to.toClientHome() + onTap: () => Modular.to.toClientHome(), ), ), title: AnimatedSwitcher( @@ -132,8 +133,9 @@ class _BillingViewState extends State { const SizedBox(height: UiConstants.space1), Text( '\$${state.currentBill.toStringAsFixed(2)}', - style: UiTypography.display1b - .copyWith(color: UiColors.white), + style: UiTypography.display1b.copyWith( + color: UiColors.white, + ), ), const SizedBox(height: UiConstants.space2), Container( @@ -171,16 +173,14 @@ class _BillingViewState extends State { ), ), SliverList( - delegate: SliverChildListDelegate( - [ - _buildContent(context, state), - ], - ), + delegate: SliverChildListDelegate([ + _buildContent(context, state), + ]), ), ], - ), - ); - }, + ); + }, + ), ); } @@ -211,7 +211,9 @@ class _BillingViewState extends State { const SizedBox(height: UiConstants.space4), UiButton.secondary( text: 'Retry', - onPressed: () => BlocProvider.of(context).add(const BillingLoadStarted()), + onPressed: () => BlocProvider.of( + context, + ).add(const BillingLoadStarted()), ), ], ), @@ -230,8 +232,10 @@ class _BillingViewState extends State { ], const PaymentMethodCard(), const SpendingBreakdownCard(), - if (state.invoiceHistory.isEmpty) _buildEmptyState(context) - else InvoiceHistorySection(invoices: state.invoiceHistory), + if (state.invoiceHistory.isEmpty) + _buildEmptyState(context) + else + InvoiceHistorySection(invoices: state.invoiceHistory), const SizedBox(height: UiConstants.space32), ], diff --git a/apps/mobile/packages/features/client/billing/lib/src/presentation/widgets/payment_method_card.dart b/apps/mobile/packages/features/client/billing/lib/src/presentation/widgets/payment_method_card.dart index 4f1c569b..346380e7 100644 --- a/apps/mobile/packages/features/client/billing/lib/src/presentation/widgets/payment_method_card.dart +++ b/apps/mobile/packages/features/client/billing/lib/src/presentation/widgets/payment_method_card.dart @@ -1,166 +1,133 @@ import 'package:core_localization/core_localization.dart'; import 'package:design_system/design_system.dart'; -import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc; import 'package:flutter/material.dart'; -import 'package:krow_data_connect/krow_data_connect.dart' as dc; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:krow_domain/krow_domain.dart'; +import '../blocs/billing_bloc.dart'; +import '../blocs/billing_state.dart'; /// Card showing the current payment method. -class PaymentMethodCard extends StatefulWidget { +class PaymentMethodCard extends StatelessWidget { /// Creates a [PaymentMethodCard]. const PaymentMethodCard({super.key}); - @override - State createState() => _PaymentMethodCardState(); -} - -class _PaymentMethodCardState extends State { - late final Future _accountsFuture = - _loadAccounts(); - - Future _loadAccounts() async { - final String? businessId = - dc.ClientSessionStore.instance.session?.business?.id; - if (businessId == null || businessId.isEmpty) { - return null; - } - - final fdc.QueryResult< - dc.GetAccountsByOwnerIdData, - dc.GetAccountsByOwnerIdVariables - > - result = await dc.ExampleConnector.instance - .getAccountsByOwnerId(ownerId: businessId) - .execute(); - return result.data; - } - @override Widget build(BuildContext context) { - return FutureBuilder( - future: _accountsFuture, - builder: - ( - BuildContext context, - AsyncSnapshot snapshot, - ) { - final List accounts = - snapshot.data?.accounts ?? []; - final dc.GetAccountsByOwnerIdAccounts? account = accounts.isNotEmpty - ? accounts.first - : null; + return BlocBuilder( + builder: (BuildContext context, BillingState state) { + final List accounts = state.bankAccounts; + final BusinessBankAccount? account = + accounts.isNotEmpty ? accounts.first : null; - if (account == null) { - return const SizedBox.shrink(); - } + if (account == null) { + return const SizedBox.shrink(); + } - final String bankLabel = account.bank.isNotEmpty == true - ? account.bank - : '----'; - final String last4 = account.last4.isNotEmpty == true - ? account.last4 - : '----'; - final bool isPrimary = account.isPrimary ?? false; - final String expiryLabel = _formatExpiry(account.expiryTime); + final String bankLabel = + account.bankName.isNotEmpty == true ? account.bankName : '----'; + final String last4 = + account.last4.isNotEmpty == true ? account.last4 : '----'; + final bool isPrimary = account.isPrimary; + final String expiryLabel = _formatExpiry(account.expiryTime); - return Container( - padding: const EdgeInsets.all(UiConstants.space4), - decoration: BoxDecoration( - color: UiColors.white, - borderRadius: UiConstants.radiusLg, - border: Border.all(color: UiColors.border), - boxShadow: [ - BoxShadow( - color: UiColors.black.withValues(alpha: 0.04), - blurRadius: 8, - offset: const Offset(0, 2), - ), - ], + return Container( + padding: const EdgeInsets.all(UiConstants.space4), + decoration: BoxDecoration( + color: UiColors.white, + borderRadius: UiConstants.radiusLg, + border: Border.all(color: UiColors.border), + boxShadow: [ + BoxShadow( + color: UiColors.black.withValues(alpha: 0.04), + blurRadius: 8, + offset: const Offset(0, 2), ), - child: Column( + ], + ), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - t.client_billing.payment_method, - style: UiTypography.title2b.textPrimary, - ), - const SizedBox.shrink(), - ], - ), - const SizedBox(height: UiConstants.space3), - Container( - padding: const EdgeInsets.all(UiConstants.space3), - decoration: BoxDecoration( - color: UiColors.bgSecondary, - borderRadius: UiConstants.radiusMd, - ), - child: Row( - children: [ - Container( - width: UiConstants.space10, - height: UiConstants.space6 + 4, - decoration: BoxDecoration( - color: UiColors.primary, - borderRadius: UiConstants.radiusSm, - ), - child: Center( - child: Text( - bankLabel, - style: UiTypography.footnote2b.white, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - ), - const SizedBox(width: UiConstants.space3), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '•••• $last4', - style: UiTypography.body2b.textPrimary, - ), - Text( - t.client_billing.expires(date: expiryLabel), - style: UiTypography.footnote2r.textSecondary, - ), - ], - ), - ), - if (isPrimary) - Container( - padding: const EdgeInsets.symmetric( - horizontal: UiConstants.space2, - vertical: UiConstants.space1, - ), - decoration: BoxDecoration( - color: UiColors.accent, - borderRadius: UiConstants.radiusSm, - ), - child: Text( - t.client_billing.default_badge, - style: UiTypography.titleUppercase4b.textPrimary, - ), - ), - ], - ), + Text( + t.client_billing.payment_method, + style: UiTypography.title2b.textPrimary, ), + const SizedBox.shrink(), ], ), - ); - }, + const SizedBox(height: UiConstants.space3), + Container( + padding: const EdgeInsets.all(UiConstants.space3), + decoration: BoxDecoration( + color: UiColors.bgSecondary, + borderRadius: UiConstants.radiusMd, + ), + child: Row( + children: [ + Container( + width: UiConstants.space10, + height: UiConstants.space6 + 4, + decoration: BoxDecoration( + color: UiColors.primary, + borderRadius: UiConstants.radiusSm, + ), + child: Center( + child: Text( + bankLabel, + style: UiTypography.footnote2b.white, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ), + const SizedBox(width: UiConstants.space3), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '•••• $last4', + style: UiTypography.body2b.textPrimary, + ), + Text( + t.client_billing.expires(date: expiryLabel), + style: UiTypography.footnote2r.textSecondary, + ), + ], + ), + ), + if (isPrimary) + Container( + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space2, + vertical: UiConstants.space1, + ), + decoration: BoxDecoration( + color: UiColors.accent, + borderRadius: UiConstants.radiusSm, + ), + child: Text( + t.client_billing.default_badge, + style: UiTypography.titleUppercase4b.textPrimary, + ), + ), + ], + ), + ), + ], + ), + ); + }, ); } - String _formatExpiry(fdc.Timestamp? expiryTime) { + String _formatExpiry(DateTime? expiryTime) { if (expiryTime == null) { return 'N/A'; } - final DateTime date = expiryTime.toDateTime(); - final String month = date.month.toString().padLeft(2, '0'); - final String year = (date.year % 100).toString().padLeft(2, '0'); + final String month = expiryTime.month.toString().padLeft(2, '0'); + final String year = (expiryTime.year % 100).toString().padLeft(2, '0'); return '$month/$year'; } } diff --git a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/actions_widget.dart b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/actions_widget.dart index 04a420b7..3af93fc3 100644 --- a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/actions_widget.dart +++ b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/actions_widget.dart @@ -4,7 +4,6 @@ import 'package:flutter/material.dart'; /// A widget that displays quick actions for the client. class ActionsWidget extends StatelessWidget { - /// Creates an [ActionsWidget]. const ActionsWidget({ super.key, @@ -12,6 +11,7 @@ class ActionsWidget extends StatelessWidget { required this.onCreateOrderPressed, this.subtitle, }); + /// Callback when RAPID is pressed. final VoidCallback onRapidPressed; @@ -26,12 +26,9 @@ class ActionsWidget extends StatelessWidget { // Check if client_home exists in t final TranslationsClientHomeActionsEn i18n = t.client_home.actions; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, + return Row( + spacing: UiConstants.space4, children: [ - Row( - children: [ - /// TODO: FEATURE_NOT_YET_IMPLEMENTED Expanded( child: _ActionCard( title: i18n.rapid, @@ -46,7 +43,6 @@ class ActionsWidget extends StatelessWidget { onTap: onRapidPressed, ), ), - // const SizedBox(width: UiConstants.space2), Expanded( child: _ActionCard( title: i18n.create_order, @@ -62,14 +58,11 @@ class ActionsWidget extends StatelessWidget { ), ), ], - ), - ], ); } } class _ActionCard extends StatelessWidget { - const _ActionCard({ required this.title, required this.subtitle, diff --git a/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/data/repositories/bank_account_repository_impl.dart b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/data/repositories/bank_account_repository_impl.dart index 14614b66..b029f4ed 100644 --- a/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/data/repositories/bank_account_repository_impl.dart +++ b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/data/repositories/bank_account_repository_impl.dart @@ -14,13 +14,10 @@ class BankAccountRepositoryImpl implements BankAccountRepository { final DataConnectService _service; @override - Future> getAccounts() async { + Future> getAccounts() async { return _service.run(() async { final String staffId = await _service.getStaffId(); - var x = staffId; - - print(x); final QueryResult result = await _service.connector .getAccountsByOwnerId(ownerId: staffId) @@ -44,7 +41,7 @@ class BankAccountRepositoryImpl implements BankAccountRepository { } @override - Future addAccount(BankAccount account) async { + Future addAccount(StaffBankAccount account) async { return _service.run(() async { final String staffId = await _service.getStaffId(); diff --git a/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/domain/arguments/add_bank_account_params.dart b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/domain/arguments/add_bank_account_params.dart index ead4135d..4bce8605 100644 --- a/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/domain/arguments/add_bank_account_params.dart +++ b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/domain/arguments/add_bank_account_params.dart @@ -4,7 +4,7 @@ import 'package:krow_domain/krow_domain.dart'; /// Arguments for adding a bank account. class AddBankAccountParams extends UseCaseArgument with EquatableMixin { - final BankAccount account; + final StaffBankAccount account; const AddBankAccountParams({required this.account}); diff --git a/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/domain/repositories/bank_account_repository.dart b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/domain/repositories/bank_account_repository.dart index 3e701aba..51d72774 100644 --- a/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/domain/repositories/bank_account_repository.dart +++ b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/domain/repositories/bank_account_repository.dart @@ -3,8 +3,8 @@ import 'package:krow_domain/krow_domain.dart'; /// Repository interface for managing bank accounts. abstract class BankAccountRepository { /// Fetches the list of bank accounts for the current user. - Future> getAccounts(); + Future> getAccounts(); /// adds a new bank account. - Future addAccount(BankAccount account); + Future addAccount(StaffBankAccount account); } diff --git a/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/domain/usecases/get_bank_accounts_usecase.dart b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/domain/usecases/get_bank_accounts_usecase.dart index 2ee64df3..2de67941 100644 --- a/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/domain/usecases/get_bank_accounts_usecase.dart +++ b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/domain/usecases/get_bank_accounts_usecase.dart @@ -3,13 +3,13 @@ import 'package:krow_domain/krow_domain.dart'; import '../repositories/bank_account_repository.dart'; /// Use case to fetch bank accounts. -class GetBankAccountsUseCase implements NoInputUseCase> { +class GetBankAccountsUseCase implements NoInputUseCase> { final BankAccountRepository _repository; GetBankAccountsUseCase(this._repository); @override - Future> call() { + Future> call() { return _repository.getAccounts(); } } diff --git a/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/blocs/bank_account_cubit.dart b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/blocs/bank_account_cubit.dart index f159781e..afa3c888 100644 --- a/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/blocs/bank_account_cubit.dart +++ b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/blocs/bank_account_cubit.dart @@ -23,19 +23,15 @@ class BankAccountCubit extends Cubit await handleError( emit: emit, action: () async { - final List accounts = await _getBankAccountsUseCase(); + final List accounts = await _getBankAccountsUseCase(); emit( - state.copyWith( - status: BankAccountStatus.loaded, - accounts: accounts, - ), + state.copyWith(status: BankAccountStatus.loaded, accounts: accounts), ); }, - onError: - (String errorKey) => state.copyWith( - status: BankAccountStatus.error, - errorMessage: errorKey, - ), + onError: (String errorKey) => state.copyWith( + status: BankAccountStatus.error, + errorMessage: errorKey, + ), ); } @@ -52,21 +48,18 @@ class BankAccountCubit extends Cubit emit(state.copyWith(status: BankAccountStatus.loading)); // Create domain entity - final BankAccount newAccount = BankAccount( + final StaffBankAccount newAccount = StaffBankAccount( id: '', // Generated by server usually userId: '', // Handled by Repo/Auth bankName: bankName, - accountNumber: accountNumber, + accountNumber: accountNumber.length > 4 + ? accountNumber.substring(accountNumber.length - 4) + : accountNumber, accountName: '', sortCode: routingNumber, - type: - type == 'CHECKING' - ? BankAccountType.checking - : BankAccountType.savings, - last4: - accountNumber.length > 4 - ? accountNumber.substring(accountNumber.length - 4) - : accountNumber, + type: type == 'CHECKING' + ? StaffBankAccountType.checking + : StaffBankAccountType.savings, isPrimary: false, ); @@ -85,12 +78,10 @@ class BankAccountCubit extends Cubit ), ); }, - onError: - (String errorKey) => state.copyWith( - status: BankAccountStatus.error, - errorMessage: errorKey, - ), + onError: (String errorKey) => state.copyWith( + status: BankAccountStatus.error, + errorMessage: errorKey, + ), ); } } - diff --git a/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/blocs/bank_account_state.dart b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/blocs/bank_account_state.dart index 09038616..3073c78b 100644 --- a/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/blocs/bank_account_state.dart +++ b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/blocs/bank_account_state.dart @@ -5,7 +5,7 @@ enum BankAccountStatus { initial, loading, loaded, error, accountAdded } class BankAccountState extends Equatable { final BankAccountStatus status; - final List accounts; + final List accounts; final String? errorMessage; final bool showForm; @@ -18,7 +18,7 @@ class BankAccountState extends Equatable { BankAccountState copyWith({ BankAccountStatus? status, - List? accounts, + List? accounts, String? errorMessage, bool? showForm, }) { diff --git a/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/pages/bank_account_page.dart b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/pages/bank_account_page.dart index 53b92702..698cfb6b 100644 --- a/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/pages/bank_account_page.dart +++ b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/pages/bank_account_page.dart @@ -96,7 +96,7 @@ class BankAccountPage extends StatelessWidget { style: UiTypography.headline4m.copyWith(color: UiColors.textPrimary), ), const SizedBox(height: UiConstants.space3), - ...state.accounts.map((BankAccount a) => _buildAccountCard(a, strings)), // Added type + ...state.accounts.map((StaffBankAccount a) => _buildAccountCard(a, strings)), // Added type // Add extra padding at bottom const SizedBox(height: UiConstants.space20), @@ -183,7 +183,7 @@ class BankAccountPage extends StatelessWidget { ); } - Widget _buildAccountCard(BankAccount account, dynamic strings) { + Widget _buildAccountCard(StaffBankAccount account, dynamic strings) { final bool isPrimary = account.isPrimary; const Color primaryColor = UiColors.primary; diff --git a/apps/mobile/pubspec.lock b/apps/mobile/pubspec.lock index f30d02fc..25c3fd23 100644 --- a/apps/mobile/pubspec.lock +++ b/apps/mobile/pubspec.lock @@ -141,10 +141,10 @@ packages: dependency: transitive description: name: characters - sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.0" charcode: dependency: transitive description: @@ -741,6 +741,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" + js: + dependency: transitive + description: + name: js + sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" + url: "https://pub.dev" + source: hosted + version: "0.7.2" json_annotation: dependency: transitive description: @@ -809,18 +817,18 @@ packages: dependency: transitive description: name: matcher - sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.18" + version: "0.12.17" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.13.0" + version: "0.11.1" melos: dependency: "direct dev" description: @@ -1318,26 +1326,26 @@ packages: dependency: transitive description: name: test - sha256: "54c516bbb7cee2754d327ad4fca637f78abfc3cbcc5ace83b3eda117e42cd71a" + sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7" url: "https://pub.dev" source: hosted - version: "1.29.0" + version: "1.26.3" test_api: dependency: transitive description: name: test_api - sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.9" + version: "0.7.7" test_core: dependency: transitive description: name: test_core - sha256: "394f07d21f0f2255ec9e3989f21e54d3c7dc0e6e9dbce160e5a9c1a6be0e2943" + sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0" url: "https://pub.dev" source: hosted - version: "0.6.15" + version: "0.6.12" typed_data: dependency: transitive description: diff --git a/makefiles/mobile.mk b/makefiles/mobile.mk index f4d62624..43c3d618 100644 --- a/makefiles/mobile.mk +++ b/makefiles/mobile.mk @@ -1,6 +1,6 @@ # --- Mobile App Development --- -.PHONY: mobile-install mobile-info mobile-client-dev-android mobile-staff-dev-android mobile-client-build mobile-staff-build mobile-hot-reload mobile-hot-restart +.PHONY: mobile-install mobile-info mobile-analyze mobile-client-dev-android mobile-staff-dev-android mobile-client-build mobile-staff-build mobile-hot-reload mobile-hot-restart MOBILE_DIR := apps/mobile @@ -19,6 +19,10 @@ mobile-info: @echo "--> Fetching mobile command info..." @cd $(MOBILE_DIR) && melos run info +mobile-analyze: + @echo "--> Analyzing mobile workspace for compile-time errors..." + @cd $(MOBILE_DIR) && flutter analyze + # --- Hot Reload & Restart --- mobile-hot-reload: @echo "--> Triggering hot reload for running Flutter app..."