feat: Refactor bank account handling in billing and staff modules

- Introduced new bank account entities: BusinessBankAccount and StaffBankAccount.
- Updated bank account adapter to handle new entities.
- Removed legacy BankAccount entity and its adapter.
- Implemented use case for fetching bank accounts in billing repository.
- Updated BillingBloc and BillingState to include bank accounts.
- Refactored PaymentMethodCard to display bank account information.
- Adjusted actions widget layout for better UI consistency.
- Updated staff bank account repository and use cases to utilize new entity structure.
- Ensured all references to bank accounts in the codebase are updated to the new structure.
This commit is contained in:
Achintha Isuru
2026-02-17 12:05:24 -05:00
parent cccc8f35ed
commit 9e1af17328
25 changed files with 399 additions and 298 deletions

View File

@@ -53,6 +53,10 @@ export 'src/entities/financial/invoice_item.dart';
export 'src/entities/financial/invoice_decline.dart'; export 'src/entities/financial/invoice_decline.dart';
export 'src/entities/financial/staff_payment.dart'; export 'src/entities/financial/staff_payment.dart';
export 'src/entities/financial/payment_summary.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 // Profile
export 'src/entities/profile/staff_document.dart'; export 'src/entities/profile/staff_document.dart';
@@ -68,7 +72,6 @@ export 'src/entities/ratings/business_staff_preference.dart';
// Staff Profile // Staff Profile
export 'src/entities/profile/emergency_contact.dart'; export 'src/entities/profile/emergency_contact.dart';
export 'src/entities/profile/bank_account.dart';
export 'src/entities/profile/accessibility.dart'; export 'src/entities/profile/accessibility.dart';
export 'src/entities/profile/schedule.dart'; export 'src/entities/profile/schedule.dart';

View File

@@ -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,
);
}
}

View File

@@ -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 { class BankAccountAdapter {
/// Maps primitive values to [BankAccount]. /// Maps primitive values to [StaffBankAccount].
static BankAccount fromPrimitives({ static StaffBankAccount fromPrimitives({
required String id, required String id,
required String userId, required String userId,
required String bankName, required String bankName,
@@ -13,7 +13,7 @@ class BankAccountAdapter {
String? sortCode, String? sortCode,
bool? isPrimary, bool? isPrimary,
}) { }) {
return BankAccount( return StaffBankAccount(
id: id, id: id,
userId: userId, userId: userId,
bankName: bankName, bankName: bankName,
@@ -26,25 +26,25 @@ class BankAccountAdapter {
); );
} }
static BankAccountType _stringToType(String? value) { static StaffBankAccountType _stringToType(String? value) {
if (value == null) return BankAccountType.checking; if (value == null) return StaffBankAccountType.checking;
try { try {
// Assuming backend enum names match or are uppercase // Assuming backend enum names match or are uppercase
return BankAccountType.values.firstWhere( return StaffBankAccountType.values.firstWhere(
(e) => e.name.toLowerCase() == value.toLowerCase(), (StaffBankAccountType e) => e.name.toLowerCase() == value.toLowerCase(),
orElse: () => BankAccountType.other, orElse: () => StaffBankAccountType.other,
); );
} catch (_) { } catch (_) {
return BankAccountType.other; return StaffBankAccountType.other;
} }
} }
/// Converts domain type to string for backend. /// Converts domain type to string for backend.
static String typeToString(BankAccountType type) { static String typeToString(StaffBankAccountType type) {
switch (type) { switch (type) {
case BankAccountType.checking: case StaffBankAccountType.checking:
return 'CHECKING'; return 'CHECKING';
case BankAccountType.savings: case StaffBankAccountType.savings:
return 'SAVINGS'; return 'SAVINGS';
default: default:
return 'CHECKING'; return 'CHECKING';

View File

@@ -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<Object?> get props => <Object?>[id, bankName, isPrimary, last4];
}

View File

@@ -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<Object?> get props => <Object?>[
...super.props,
expiryTime,
];
/// Getter for non-nullable last4 in Business context.
@override
String get last4 => super.last4!;
}

View File

@@ -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<Object?> get props =>
<Object?>[...super.props, userId, accountNumber, accountName, sortCode, type];
}

View File

@@ -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<Object?> get props => <Object?>[id, userId, bankName, accountNumber, accountName, sortCode, type, isPrimary, last4];
}

View File

@@ -3,6 +3,7 @@ import 'package:krow_core/core.dart';
import 'data/repositories_impl/billing_repository_impl.dart'; import 'data/repositories_impl/billing_repository_impl.dart';
import 'domain/repositories/billing_repository.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_current_bill_amount.dart';
import 'domain/usecases/get_invoice_history.dart'; import 'domain/usecases/get_invoice_history.dart';
import 'domain/usecases/get_pending_invoices.dart'; import 'domain/usecases/get_pending_invoices.dart';
@@ -21,6 +22,7 @@ class BillingModule extends Module {
i.addSingleton<BillingRepository>(BillingRepositoryImpl.new); i.addSingleton<BillingRepository>(BillingRepositoryImpl.new);
// Use Cases // Use Cases
i.addSingleton(GetBankAccountsUseCase.new);
i.addSingleton(GetCurrentBillAmountUseCase.new); i.addSingleton(GetCurrentBillAmountUseCase.new);
i.addSingleton(GetSavingsAmountUseCase.new); i.addSingleton(GetSavingsAmountUseCase.new);
i.addSingleton(GetPendingInvoicesUseCase.new); i.addSingleton(GetPendingInvoicesUseCase.new);
@@ -30,6 +32,7 @@ class BillingModule extends Module {
// BLoCs // BLoCs
i.addSingleton<BillingBloc>( i.addSingleton<BillingBloc>(
() => BillingBloc( () => BillingBloc(
getBankAccounts: i.get<GetBankAccountsUseCase>(),
getCurrentBillAmount: i.get<GetCurrentBillAmountUseCase>(), getCurrentBillAmount: i.get<GetCurrentBillAmountUseCase>(),
getSavingsAmount: i.get<GetSavingsAmountUseCase>(), getSavingsAmount: i.get<GetSavingsAmountUseCase>(),
getPendingInvoices: i.get<GetPendingInvoicesUseCase>(), getPendingInvoices: i.get<GetPendingInvoicesUseCase>(),

View File

@@ -16,6 +16,23 @@ class BillingRepositoryImpl implements BillingRepository {
final data_connect.DataConnectService _service; final data_connect.DataConnectService _service;
/// Fetches bank accounts associated with the business.
@override
Future<List<BusinessBankAccount>> 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. /// Fetches the current bill amount by aggregating open invoices.
@override @override
Future<double> getCurrentBillAmount() async { Future<double> 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( InvoiceStatus _mapInvoiceStatus(
data_connect.EnumValue<data_connect.InvoiceStatus> status, data_connect.EnumValue<data_connect.InvoiceStatus> status,
) { ) {

View File

@@ -7,6 +7,9 @@ import '../models/billing_period.dart';
/// acting as a boundary between the Domain and Data layers. /// acting as a boundary between the Domain and Data layers.
/// It allows the Domain layer to remain independent of specific data sources. /// It allows the Domain layer to remain independent of specific data sources.
abstract class BillingRepository { abstract class BillingRepository {
/// Fetches bank accounts associated with the business.
Future<List<BusinessBankAccount>> getBankAccounts();
/// Fetches invoices that are pending approval or payment. /// Fetches invoices that are pending approval or payment.
Future<List<Invoice>> getPendingInvoices(); Future<List<Invoice>> getPendingInvoices();

View File

@@ -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<List<BusinessBankAccount>> {
/// Creates a [GetBankAccountsUseCase].
GetBankAccountsUseCase(this._repository);
final BillingRepository _repository;
@override
Future<List<BusinessBankAccount>> call() => _repository.getBankAccounts();
}

View File

@@ -1,6 +1,7 @@
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow_core/core.dart'; import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.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_current_bill_amount.dart';
import '../../domain/usecases/get_invoice_history.dart'; import '../../domain/usecases/get_invoice_history.dart';
import '../../domain/usecases/get_pending_invoices.dart'; import '../../domain/usecases/get_pending_invoices.dart';
@@ -16,12 +17,14 @@ class BillingBloc extends Bloc<BillingEvent, BillingState>
with BlocErrorHandler<BillingState> { with BlocErrorHandler<BillingState> {
/// Creates a [BillingBloc] with the given use cases. /// Creates a [BillingBloc] with the given use cases.
BillingBloc({ BillingBloc({
required GetBankAccountsUseCase getBankAccounts,
required GetCurrentBillAmountUseCase getCurrentBillAmount, required GetCurrentBillAmountUseCase getCurrentBillAmount,
required GetSavingsAmountUseCase getSavingsAmount, required GetSavingsAmountUseCase getSavingsAmount,
required GetPendingInvoicesUseCase getPendingInvoices, required GetPendingInvoicesUseCase getPendingInvoices,
required GetInvoiceHistoryUseCase getInvoiceHistory, required GetInvoiceHistoryUseCase getInvoiceHistory,
required GetSpendingBreakdownUseCase getSpendingBreakdown, required GetSpendingBreakdownUseCase getSpendingBreakdown,
}) : _getCurrentBillAmount = getCurrentBillAmount, }) : _getBankAccounts = getBankAccounts,
_getCurrentBillAmount = getCurrentBillAmount,
_getSavingsAmount = getSavingsAmount, _getSavingsAmount = getSavingsAmount,
_getPendingInvoices = getPendingInvoices, _getPendingInvoices = getPendingInvoices,
_getInvoiceHistory = getInvoiceHistory, _getInvoiceHistory = getInvoiceHistory,
@@ -31,6 +34,7 @@ class BillingBloc extends Bloc<BillingEvent, BillingState>
on<BillingPeriodChanged>(_onPeriodChanged); on<BillingPeriodChanged>(_onPeriodChanged);
} }
final GetBankAccountsUseCase _getBankAccounts;
final GetCurrentBillAmountUseCase _getCurrentBillAmount; final GetCurrentBillAmountUseCase _getCurrentBillAmount;
final GetSavingsAmountUseCase _getSavingsAmount; final GetSavingsAmountUseCase _getSavingsAmount;
final GetPendingInvoicesUseCase _getPendingInvoices; final GetPendingInvoicesUseCase _getPendingInvoices;
@@ -52,12 +56,15 @@ class BillingBloc extends Bloc<BillingEvent, BillingState>
_getPendingInvoices.call(), _getPendingInvoices.call(),
_getInvoiceHistory.call(), _getInvoiceHistory.call(),
_getSpendingBreakdown.call(state.period), _getSpendingBreakdown.call(state.period),
_getBankAccounts.call(),
]); ]);
final double savings = results[1] as double; final double savings = results[1] as double;
final List<Invoice> pendingInvoices = results[2] as List<Invoice>; final List<Invoice> pendingInvoices = results[2] as List<Invoice>;
final List<Invoice> invoiceHistory = results[3] as List<Invoice>; final List<Invoice> invoiceHistory = results[3] as List<Invoice>;
final List<InvoiceItem> spendingItems = results[4] as List<InvoiceItem>; final List<InvoiceItem> spendingItems = results[4] as List<InvoiceItem>;
final List<BusinessBankAccount> bankAccounts =
results[5] as List<BusinessBankAccount>;
// Map Domain Entities to Presentation Models // Map Domain Entities to Presentation Models
final List<BillingInvoice> uiPendingInvoices = final List<BillingInvoice> uiPendingInvoices =
@@ -79,6 +86,7 @@ class BillingBloc extends Bloc<BillingEvent, BillingState>
pendingInvoices: uiPendingInvoices, pendingInvoices: uiPendingInvoices,
invoiceHistory: uiInvoiceHistory, invoiceHistory: uiInvoiceHistory,
spendingBreakdown: uiSpendingBreakdown, spendingBreakdown: uiSpendingBreakdown,
bankAccounts: bankAccounts,
), ),
); );
}, },

View File

@@ -1,4 +1,5 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:krow_domain/krow_domain.dart';
import '../../domain/models/billing_period.dart'; import '../../domain/models/billing_period.dart';
import '../models/billing_invoice_model.dart'; import '../models/billing_invoice_model.dart';
import '../models/spending_breakdown_model.dart'; import '../models/spending_breakdown_model.dart';
@@ -28,6 +29,7 @@ class BillingState extends Equatable {
this.pendingInvoices = const <BillingInvoice>[], this.pendingInvoices = const <BillingInvoice>[],
this.invoiceHistory = const <BillingInvoice>[], this.invoiceHistory = const <BillingInvoice>[],
this.spendingBreakdown = const <SpendingBreakdownItem>[], this.spendingBreakdown = const <SpendingBreakdownItem>[],
this.bankAccounts = const <BusinessBankAccount>[],
this.period = BillingPeriod.week, this.period = BillingPeriod.week,
this.errorMessage, this.errorMessage,
}); });
@@ -50,6 +52,9 @@ class BillingState extends Equatable {
/// Breakdown of spending by category. /// Breakdown of spending by category.
final List<SpendingBreakdownItem> spendingBreakdown; final List<SpendingBreakdownItem> spendingBreakdown;
/// Bank accounts associated with the business.
final List<BusinessBankAccount> bankAccounts;
/// Selected period for the breakdown. /// Selected period for the breakdown.
final BillingPeriod period; final BillingPeriod period;
@@ -64,6 +69,7 @@ class BillingState extends Equatable {
List<BillingInvoice>? pendingInvoices, List<BillingInvoice>? pendingInvoices,
List<BillingInvoice>? invoiceHistory, List<BillingInvoice>? invoiceHistory,
List<SpendingBreakdownItem>? spendingBreakdown, List<SpendingBreakdownItem>? spendingBreakdown,
List<BusinessBankAccount>? bankAccounts,
BillingPeriod? period, BillingPeriod? period,
String? errorMessage, String? errorMessage,
}) { }) {
@@ -74,6 +80,7 @@ class BillingState extends Equatable {
pendingInvoices: pendingInvoices ?? this.pendingInvoices, pendingInvoices: pendingInvoices ?? this.pendingInvoices,
invoiceHistory: invoiceHistory ?? this.invoiceHistory, invoiceHistory: invoiceHistory ?? this.invoiceHistory,
spendingBreakdown: spendingBreakdown ?? this.spendingBreakdown, spendingBreakdown: spendingBreakdown ?? this.spendingBreakdown,
bankAccounts: bankAccounts ?? this.bankAccounts,
period: period ?? this.period, period: period ?? this.period,
errorMessage: errorMessage ?? this.errorMessage, errorMessage: errorMessage ?? this.errorMessage,
); );
@@ -87,6 +94,7 @@ class BillingState extends Equatable {
pendingInvoices, pendingInvoices,
invoiceHistory, invoiceHistory,
spendingBreakdown, spendingBreakdown,
bankAccounts,
period, period,
errorMessage, errorMessage,
]; ];

View File

@@ -71,19 +71,20 @@ class _BillingViewState extends State<BillingView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocConsumer<BillingBloc, BillingState>( return Scaffold(
listener: (BuildContext context, BillingState state) { body: BlocConsumer<BillingBloc, BillingState>(
if (state.status == BillingStatus.failure && state.errorMessage != null) { listener: (BuildContext context, BillingState state) {
UiSnackbar.show( if (state.status == BillingStatus.failure &&
context, state.errorMessage != null) {
message: translateErrorKey(state.errorMessage!), UiSnackbar.show(
type: UiSnackbarType.error, context,
); message: translateErrorKey(state.errorMessage!),
} type: UiSnackbarType.error,
}, );
builder: (BuildContext context, BillingState state) { }
return Scaffold( },
body: CustomScrollView( builder: (BuildContext context, BillingState state) {
return CustomScrollView(
controller: _scrollController, controller: _scrollController,
slivers: <Widget>[ slivers: <Widget>[
SliverAppBar( SliverAppBar(
@@ -97,7 +98,7 @@ class _BillingViewState extends State<BillingView> {
leading: Center( leading: Center(
child: UiIconButton.secondary( child: UiIconButton.secondary(
icon: UiIcons.arrowLeft, icon: UiIcons.arrowLeft,
onTap: () => Modular.to.toClientHome() onTap: () => Modular.to.toClientHome(),
), ),
), ),
title: AnimatedSwitcher( title: AnimatedSwitcher(
@@ -132,8 +133,9 @@ class _BillingViewState extends State<BillingView> {
const SizedBox(height: UiConstants.space1), const SizedBox(height: UiConstants.space1),
Text( Text(
'\$${state.currentBill.toStringAsFixed(2)}', '\$${state.currentBill.toStringAsFixed(2)}',
style: UiTypography.display1b style: UiTypography.display1b.copyWith(
.copyWith(color: UiColors.white), color: UiColors.white,
),
), ),
const SizedBox(height: UiConstants.space2), const SizedBox(height: UiConstants.space2),
Container( Container(
@@ -171,16 +173,14 @@ class _BillingViewState extends State<BillingView> {
), ),
), ),
SliverList( SliverList(
delegate: SliverChildListDelegate( delegate: SliverChildListDelegate(<Widget>[
<Widget>[ _buildContent(context, state),
_buildContent(context, state), ]),
],
),
), ),
], ],
), );
); },
}, ),
); );
} }
@@ -211,7 +211,9 @@ class _BillingViewState extends State<BillingView> {
const SizedBox(height: UiConstants.space4), const SizedBox(height: UiConstants.space4),
UiButton.secondary( UiButton.secondary(
text: 'Retry', text: 'Retry',
onPressed: () => BlocProvider.of<BillingBloc>(context).add(const BillingLoadStarted()), onPressed: () => BlocProvider.of<BillingBloc>(
context,
).add(const BillingLoadStarted()),
), ),
], ],
), ),
@@ -230,8 +232,10 @@ class _BillingViewState extends State<BillingView> {
], ],
const PaymentMethodCard(), const PaymentMethodCard(),
const SpendingBreakdownCard(), const SpendingBreakdownCard(),
if (state.invoiceHistory.isEmpty) _buildEmptyState(context) if (state.invoiceHistory.isEmpty)
else InvoiceHistorySection(invoices: state.invoiceHistory), _buildEmptyState(context)
else
InvoiceHistorySection(invoices: state.invoiceHistory),
const SizedBox(height: UiConstants.space32), const SizedBox(height: UiConstants.space32),
], ],

View File

@@ -1,166 +1,133 @@
import 'package:core_localization/core_localization.dart'; import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.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: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. /// Card showing the current payment method.
class PaymentMethodCard extends StatefulWidget { class PaymentMethodCard extends StatelessWidget {
/// Creates a [PaymentMethodCard]. /// Creates a [PaymentMethodCard].
const PaymentMethodCard({super.key}); const PaymentMethodCard({super.key});
@override
State<PaymentMethodCard> createState() => _PaymentMethodCardState();
}
class _PaymentMethodCardState extends State<PaymentMethodCard> {
late final Future<dc.GetAccountsByOwnerIdData?> _accountsFuture =
_loadAccounts();
Future<dc.GetAccountsByOwnerIdData?> _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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return FutureBuilder<dc.GetAccountsByOwnerIdData?>( return BlocBuilder<BillingBloc, BillingState>(
future: _accountsFuture, builder: (BuildContext context, BillingState state) {
builder: final List<BusinessBankAccount> accounts = state.bankAccounts;
( final BusinessBankAccount? account =
BuildContext context, accounts.isNotEmpty ? accounts.first : null;
AsyncSnapshot<dc.GetAccountsByOwnerIdData?> snapshot,
) {
final List<dc.GetAccountsByOwnerIdAccounts> accounts =
snapshot.data?.accounts ?? <dc.GetAccountsByOwnerIdAccounts>[];
final dc.GetAccountsByOwnerIdAccounts? account = accounts.isNotEmpty
? accounts.first
: null;
if (account == null) { if (account == null) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
final String bankLabel = account.bank.isNotEmpty == true final String bankLabel =
? account.bank account.bankName.isNotEmpty == true ? account.bankName : '----';
: '----'; final String last4 =
final String last4 = account.last4.isNotEmpty == true account.last4.isNotEmpty == true ? account.last4 : '----';
? account.last4 final bool isPrimary = account.isPrimary;
: '----'; final String expiryLabel = _formatExpiry(account.expiryTime);
final bool isPrimary = account.isPrimary ?? false;
final String expiryLabel = _formatExpiry(account.expiryTime);
return Container( return Container(
padding: const EdgeInsets.all(UiConstants.space4), padding: const EdgeInsets.all(UiConstants.space4),
decoration: BoxDecoration( decoration: BoxDecoration(
color: UiColors.white, color: UiColors.white,
borderRadius: UiConstants.radiusLg, borderRadius: UiConstants.radiusLg,
border: Border.all(color: UiColors.border), border: Border.all(color: UiColors.border),
boxShadow: <BoxShadow>[ boxShadow: <BoxShadow>[
BoxShadow( BoxShadow(
color: UiColors.black.withValues(alpha: 0.04), color: UiColors.black.withValues(alpha: 0.04),
blurRadius: 8, blurRadius: 8,
offset: const Offset(0, 2), offset: const Offset(0, 2),
),
],
), ),
child: Column( ],
),
child: Column(
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[ children: <Widget>[
Row( Text(
mainAxisAlignment: MainAxisAlignment.spaceBetween, t.client_billing.payment_method,
children: <Widget>[ style: UiTypography.title2b.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: <Widget>[
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: <Widget>[
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,
),
),
],
),
), ),
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: <Widget>[
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: <Widget>[
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) { if (expiryTime == null) {
return 'N/A'; return 'N/A';
} }
final DateTime date = expiryTime.toDateTime(); final String month = expiryTime.month.toString().padLeft(2, '0');
final String month = date.month.toString().padLeft(2, '0'); final String year = (expiryTime.year % 100).toString().padLeft(2, '0');
final String year = (date.year % 100).toString().padLeft(2, '0');
return '$month/$year'; return '$month/$year';
} }
} }

View File

@@ -4,7 +4,6 @@ import 'package:flutter/material.dart';
/// A widget that displays quick actions for the client. /// A widget that displays quick actions for the client.
class ActionsWidget extends StatelessWidget { class ActionsWidget extends StatelessWidget {
/// Creates an [ActionsWidget]. /// Creates an [ActionsWidget].
const ActionsWidget({ const ActionsWidget({
super.key, super.key,
@@ -12,6 +11,7 @@ class ActionsWidget extends StatelessWidget {
required this.onCreateOrderPressed, required this.onCreateOrderPressed,
this.subtitle, this.subtitle,
}); });
/// Callback when RAPID is pressed. /// Callback when RAPID is pressed.
final VoidCallback onRapidPressed; final VoidCallback onRapidPressed;
@@ -26,12 +26,9 @@ class ActionsWidget extends StatelessWidget {
// Check if client_home exists in t // Check if client_home exists in t
final TranslationsClientHomeActionsEn i18n = t.client_home.actions; final TranslationsClientHomeActionsEn i18n = t.client_home.actions;
return Column( return Row(
crossAxisAlignment: CrossAxisAlignment.start, spacing: UiConstants.space4,
children: <Widget>[ children: <Widget>[
Row(
children: <Widget>[
/// TODO: FEATURE_NOT_YET_IMPLEMENTED
Expanded( Expanded(
child: _ActionCard( child: _ActionCard(
title: i18n.rapid, title: i18n.rapid,
@@ -46,7 +43,6 @@ class ActionsWidget extends StatelessWidget {
onTap: onRapidPressed, onTap: onRapidPressed,
), ),
), ),
// const SizedBox(width: UiConstants.space2),
Expanded( Expanded(
child: _ActionCard( child: _ActionCard(
title: i18n.create_order, title: i18n.create_order,
@@ -62,14 +58,11 @@ class ActionsWidget extends StatelessWidget {
), ),
), ),
], ],
),
],
); );
} }
} }
class _ActionCard extends StatelessWidget { class _ActionCard extends StatelessWidget {
const _ActionCard({ const _ActionCard({
required this.title, required this.title,
required this.subtitle, required this.subtitle,

View File

@@ -14,13 +14,10 @@ class BankAccountRepositoryImpl implements BankAccountRepository {
final DataConnectService _service; final DataConnectService _service;
@override @override
Future<List<BankAccount>> getAccounts() async { Future<List<StaffBankAccount>> getAccounts() async {
return _service.run(() async { return _service.run(() async {
final String staffId = await _service.getStaffId(); final String staffId = await _service.getStaffId();
var x = staffId;
print(x);
final QueryResult<GetAccountsByOwnerIdData, GetAccountsByOwnerIdVariables> final QueryResult<GetAccountsByOwnerIdData, GetAccountsByOwnerIdVariables>
result = await _service.connector result = await _service.connector
.getAccountsByOwnerId(ownerId: staffId) .getAccountsByOwnerId(ownerId: staffId)
@@ -44,7 +41,7 @@ class BankAccountRepositoryImpl implements BankAccountRepository {
} }
@override @override
Future<void> addAccount(BankAccount account) async { Future<void> addAccount(StaffBankAccount account) async {
return _service.run(() async { return _service.run(() async {
final String staffId = await _service.getStaffId(); final String staffId = await _service.getStaffId();

View File

@@ -4,7 +4,7 @@ import 'package:krow_domain/krow_domain.dart';
/// Arguments for adding a bank account. /// Arguments for adding a bank account.
class AddBankAccountParams extends UseCaseArgument with EquatableMixin { class AddBankAccountParams extends UseCaseArgument with EquatableMixin {
final BankAccount account; final StaffBankAccount account;
const AddBankAccountParams({required this.account}); const AddBankAccountParams({required this.account});

View File

@@ -3,8 +3,8 @@ import 'package:krow_domain/krow_domain.dart';
/// Repository interface for managing bank accounts. /// Repository interface for managing bank accounts.
abstract class BankAccountRepository { abstract class BankAccountRepository {
/// Fetches the list of bank accounts for the current user. /// Fetches the list of bank accounts for the current user.
Future<List<BankAccount>> getAccounts(); Future<List<StaffBankAccount>> getAccounts();
/// adds a new bank account. /// adds a new bank account.
Future<void> addAccount(BankAccount account); Future<void> addAccount(StaffBankAccount account);
} }

View File

@@ -3,13 +3,13 @@ import 'package:krow_domain/krow_domain.dart';
import '../repositories/bank_account_repository.dart'; import '../repositories/bank_account_repository.dart';
/// Use case to fetch bank accounts. /// Use case to fetch bank accounts.
class GetBankAccountsUseCase implements NoInputUseCase<List<BankAccount>> { class GetBankAccountsUseCase implements NoInputUseCase<List<StaffBankAccount>> {
final BankAccountRepository _repository; final BankAccountRepository _repository;
GetBankAccountsUseCase(this._repository); GetBankAccountsUseCase(this._repository);
@override @override
Future<List<BankAccount>> call() { Future<List<StaffBankAccount>> call() {
return _repository.getAccounts(); return _repository.getAccounts();
} }
} }

View File

@@ -23,19 +23,15 @@ class BankAccountCubit extends Cubit<BankAccountState>
await handleError( await handleError(
emit: emit, emit: emit,
action: () async { action: () async {
final List<BankAccount> accounts = await _getBankAccountsUseCase(); final List<StaffBankAccount> accounts = await _getBankAccountsUseCase();
emit( emit(
state.copyWith( state.copyWith(status: BankAccountStatus.loaded, accounts: accounts),
status: BankAccountStatus.loaded,
accounts: accounts,
),
); );
}, },
onError: onError: (String errorKey) => state.copyWith(
(String errorKey) => state.copyWith( status: BankAccountStatus.error,
status: BankAccountStatus.error, errorMessage: errorKey,
errorMessage: errorKey, ),
),
); );
} }
@@ -52,21 +48,18 @@ class BankAccountCubit extends Cubit<BankAccountState>
emit(state.copyWith(status: BankAccountStatus.loading)); emit(state.copyWith(status: BankAccountStatus.loading));
// Create domain entity // Create domain entity
final BankAccount newAccount = BankAccount( final StaffBankAccount newAccount = StaffBankAccount(
id: '', // Generated by server usually id: '', // Generated by server usually
userId: '', // Handled by Repo/Auth userId: '', // Handled by Repo/Auth
bankName: bankName, bankName: bankName,
accountNumber: accountNumber, accountNumber: accountNumber.length > 4
? accountNumber.substring(accountNumber.length - 4)
: accountNumber,
accountName: '', accountName: '',
sortCode: routingNumber, sortCode: routingNumber,
type: type: type == 'CHECKING'
type == 'CHECKING' ? StaffBankAccountType.checking
? BankAccountType.checking : StaffBankAccountType.savings,
: BankAccountType.savings,
last4:
accountNumber.length > 4
? accountNumber.substring(accountNumber.length - 4)
: accountNumber,
isPrimary: false, isPrimary: false,
); );
@@ -85,12 +78,10 @@ class BankAccountCubit extends Cubit<BankAccountState>
), ),
); );
}, },
onError: onError: (String errorKey) => state.copyWith(
(String errorKey) => state.copyWith( status: BankAccountStatus.error,
status: BankAccountStatus.error, errorMessage: errorKey,
errorMessage: errorKey, ),
),
); );
} }
} }

View File

@@ -5,7 +5,7 @@ enum BankAccountStatus { initial, loading, loaded, error, accountAdded }
class BankAccountState extends Equatable { class BankAccountState extends Equatable {
final BankAccountStatus status; final BankAccountStatus status;
final List<BankAccount> accounts; final List<StaffBankAccount> accounts;
final String? errorMessage; final String? errorMessage;
final bool showForm; final bool showForm;
@@ -18,7 +18,7 @@ class BankAccountState extends Equatable {
BankAccountState copyWith({ BankAccountState copyWith({
BankAccountStatus? status, BankAccountStatus? status,
List<BankAccount>? accounts, List<StaffBankAccount>? accounts,
String? errorMessage, String? errorMessage,
bool? showForm, bool? showForm,
}) { }) {

View File

@@ -96,7 +96,7 @@ class BankAccountPage extends StatelessWidget {
style: UiTypography.headline4m.copyWith(color: UiColors.textPrimary), style: UiTypography.headline4m.copyWith(color: UiColors.textPrimary),
), ),
const SizedBox(height: UiConstants.space3), 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 // Add extra padding at bottom
const SizedBox(height: UiConstants.space20), 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; final bool isPrimary = account.isPrimary;
const Color primaryColor = UiColors.primary; const Color primaryColor = UiColors.primary;

View File

@@ -141,10 +141,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: characters name: characters
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.1" version: "1.4.0"
charcode: charcode:
dependency: transitive dependency: transitive
description: description:
@@ -741,6 +741,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.5" version: "1.0.5"
js:
dependency: transitive
description:
name: js
sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc"
url: "https://pub.dev"
source: hosted
version: "0.7.2"
json_annotation: json_annotation:
dependency: transitive dependency: transitive
description: description:
@@ -809,18 +817,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: matcher name: matcher
sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.12.18" version: "0.12.17"
material_color_utilities: material_color_utilities:
dependency: transitive dependency: transitive
description: description:
name: material_color_utilities name: material_color_utilities
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.13.0" version: "0.11.1"
melos: melos:
dependency: "direct dev" dependency: "direct dev"
description: description:
@@ -1318,26 +1326,26 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test name: test
sha256: "54c516bbb7cee2754d327ad4fca637f78abfc3cbcc5ace83b3eda117e42cd71a" sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.29.0" version: "1.26.3"
test_api: test_api:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.9" version: "0.7.7"
test_core: test_core:
dependency: transitive dependency: transitive
description: description:
name: test_core name: test_core
sha256: "394f07d21f0f2255ec9e3989f21e54d3c7dc0e6e9dbce160e5a9c1a6be0e2943" sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.6.15" version: "0.6.12"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:

View File

@@ -1,6 +1,6 @@
# --- Mobile App Development --- # --- 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 MOBILE_DIR := apps/mobile
@@ -19,6 +19,10 @@ mobile-info:
@echo "--> Fetching mobile command info..." @echo "--> Fetching mobile command info..."
@cd $(MOBILE_DIR) && melos run 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 --- # --- Hot Reload & Restart ---
mobile-hot-reload: mobile-hot-reload:
@echo "--> Triggering hot reload for running Flutter app..." @echo "--> Triggering hot reload for running Flutter app..."