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/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';

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 {
/// 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';

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 '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<BillingRepository>(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>(
() => BillingBloc(
getBankAccounts: i.get<GetBankAccountsUseCase>(),
getCurrentBillAmount: i.get<GetCurrentBillAmountUseCase>(),
getSavingsAmount: i.get<GetSavingsAmountUseCase>(),
getPendingInvoices: i.get<GetPendingInvoicesUseCase>(),

View File

@@ -16,6 +16,23 @@ class BillingRepositoryImpl implements BillingRepository {
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.
@override
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(
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.
/// It allows the Domain layer to remain independent of specific data sources.
abstract class BillingRepository {
/// Fetches bank accounts associated with the business.
Future<List<BusinessBankAccount>> getBankAccounts();
/// Fetches invoices that are pending approval or payment.
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: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<BillingEvent, BillingState>
with BlocErrorHandler<BillingState> {
/// 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<BillingEvent, BillingState>
on<BillingPeriodChanged>(_onPeriodChanged);
}
final GetBankAccountsUseCase _getBankAccounts;
final GetCurrentBillAmountUseCase _getCurrentBillAmount;
final GetSavingsAmountUseCase _getSavingsAmount;
final GetPendingInvoicesUseCase _getPendingInvoices;
@@ -52,12 +56,15 @@ class BillingBloc extends Bloc<BillingEvent, BillingState>
_getPendingInvoices.call(),
_getInvoiceHistory.call(),
_getSpendingBreakdown.call(state.period),
_getBankAccounts.call(),
]);
final double savings = results[1] as double;
final List<Invoice> pendingInvoices = results[2] as List<Invoice>;
final List<Invoice> invoiceHistory = results[3] as List<Invoice>;
final List<InvoiceItem> spendingItems = results[4] as List<InvoiceItem>;
final List<BusinessBankAccount> bankAccounts =
results[5] as List<BusinessBankAccount>;
// Map Domain Entities to Presentation Models
final List<BillingInvoice> uiPendingInvoices =
@@ -79,6 +86,7 @@ class BillingBloc extends Bloc<BillingEvent, BillingState>
pendingInvoices: uiPendingInvoices,
invoiceHistory: uiInvoiceHistory,
spendingBreakdown: uiSpendingBreakdown,
bankAccounts: bankAccounts,
),
);
},

View File

@@ -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 <BillingInvoice>[],
this.invoiceHistory = const <BillingInvoice>[],
this.spendingBreakdown = const <SpendingBreakdownItem>[],
this.bankAccounts = const <BusinessBankAccount>[],
this.period = BillingPeriod.week,
this.errorMessage,
});
@@ -50,6 +52,9 @@ class BillingState extends Equatable {
/// Breakdown of spending by category.
final List<SpendingBreakdownItem> spendingBreakdown;
/// Bank accounts associated with the business.
final List<BusinessBankAccount> bankAccounts;
/// Selected period for the breakdown.
final BillingPeriod period;
@@ -64,6 +69,7 @@ class BillingState extends Equatable {
List<BillingInvoice>? pendingInvoices,
List<BillingInvoice>? invoiceHistory,
List<SpendingBreakdownItem>? spendingBreakdown,
List<BusinessBankAccount>? 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,
];

View File

@@ -71,19 +71,20 @@ class _BillingViewState extends State<BillingView> {
@override
Widget build(BuildContext context) {
return BlocConsumer<BillingBloc, BillingState>(
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<BillingBloc, BillingState>(
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: <Widget>[
SliverAppBar(
@@ -97,7 +98,7 @@ class _BillingViewState extends State<BillingView> {
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<BillingView> {
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<BillingView> {
),
),
SliverList(
delegate: SliverChildListDelegate(
<Widget>[
_buildContent(context, state),
],
),
delegate: SliverChildListDelegate(<Widget>[
_buildContent(context, state),
]),
),
],
),
);
},
);
},
),
);
}
@@ -211,7 +211,9 @@ class _BillingViewState extends State<BillingView> {
const SizedBox(height: UiConstants.space4),
UiButton.secondary(
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 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),
],

View File

@@ -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<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
Widget build(BuildContext context) {
return FutureBuilder<dc.GetAccountsByOwnerIdData?>(
future: _accountsFuture,
builder:
(
BuildContext context,
AsyncSnapshot<dc.GetAccountsByOwnerIdData?> snapshot,
) {
final List<dc.GetAccountsByOwnerIdAccounts> accounts =
snapshot.data?.accounts ?? <dc.GetAccountsByOwnerIdAccounts>[];
final dc.GetAccountsByOwnerIdAccounts? account = accounts.isNotEmpty
? accounts.first
: null;
return BlocBuilder<BillingBloc, BillingState>(
builder: (BuildContext context, BillingState state) {
final List<BusinessBankAccount> 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>[
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>[
BoxShadow(
color: UiColors.black.withValues(alpha: 0.04),
blurRadius: 8,
offset: const Offset(0, 2),
),
child: Column(
],
),
child: Column(
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
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,
),
),
],
),
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,
),
),
],
),
),
],
),
);
},
);
}
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';
}
}

View File

@@ -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: <Widget>[
Row(
children: <Widget>[
/// 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,

View File

@@ -14,13 +14,10 @@ class BankAccountRepositoryImpl implements BankAccountRepository {
final DataConnectService _service;
@override
Future<List<BankAccount>> getAccounts() async {
Future<List<StaffBankAccount>> getAccounts() async {
return _service.run(() async {
final String staffId = await _service.getStaffId();
var x = staffId;
print(x);
final QueryResult<GetAccountsByOwnerIdData, GetAccountsByOwnerIdVariables>
result = await _service.connector
.getAccountsByOwnerId(ownerId: staffId)
@@ -44,7 +41,7 @@ class BankAccountRepositoryImpl implements BankAccountRepository {
}
@override
Future<void> addAccount(BankAccount account) async {
Future<void> addAccount(StaffBankAccount account) async {
return _service.run(() async {
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.
class AddBankAccountParams extends UseCaseArgument with EquatableMixin {
final BankAccount account;
final StaffBankAccount 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.
abstract class BankAccountRepository {
/// Fetches the list of bank accounts for the current user.
Future<List<BankAccount>> getAccounts();
Future<List<StaffBankAccount>> getAccounts();
/// 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';
/// Use case to fetch bank accounts.
class GetBankAccountsUseCase implements NoInputUseCase<List<BankAccount>> {
class GetBankAccountsUseCase implements NoInputUseCase<List<StaffBankAccount>> {
final BankAccountRepository _repository;
GetBankAccountsUseCase(this._repository);
@override
Future<List<BankAccount>> call() {
Future<List<StaffBankAccount>> call() {
return _repository.getAccounts();
}
}

View File

@@ -23,19 +23,15 @@ class BankAccountCubit extends Cubit<BankAccountState>
await handleError(
emit: emit,
action: () async {
final List<BankAccount> accounts = await _getBankAccountsUseCase();
final List<StaffBankAccount> 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<BankAccountState>
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<BankAccountState>
),
);
},
onError:
(String errorKey) => state.copyWith(
status: BankAccountStatus.error,
errorMessage: errorKey,
),
onError: (String errorKey) => state.copyWith(
status: BankAccountStatus.error,
errorMessage: errorKey,
),
);
}
}

View File

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

View File

@@ -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;

View File

@@ -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:

View File

@@ -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..."