feat: Implement bank account management feature with UI, BLoC integration, and repository setup
This commit is contained in:
@@ -44,7 +44,8 @@ class AppWidget extends StatelessWidget {
|
|||||||
core_localization.LocaleState
|
core_localization.LocaleState
|
||||||
>(
|
>(
|
||||||
builder: (BuildContext context, core_localization.LocaleState state) {
|
builder: (BuildContext context, core_localization.LocaleState state) {
|
||||||
return MaterialApp.router(
|
return core_localization.TranslationProvider(
|
||||||
|
child: MaterialApp.router(
|
||||||
title: "KROW Staff",
|
title: "KROW Staff",
|
||||||
theme: UiTheme.light,
|
theme: UiTheme.light,
|
||||||
routerConfig: Modular.routerConfig,
|
routerConfig: Modular.routerConfig,
|
||||||
@@ -55,7 +56,7 @@ class AppWidget extends StatelessWidget {
|
|||||||
GlobalWidgetsLocalizations.delegate,
|
GlobalWidgetsLocalizations.delegate,
|
||||||
GlobalCupertinoLocalizations.delegate,
|
GlobalCupertinoLocalizations.delegate,
|
||||||
],
|
],
|
||||||
);
|
));
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -504,6 +504,25 @@
|
|||||||
"privacy_security": "Privacy & Security",
|
"privacy_security": "Privacy & Security",
|
||||||
"messages": "Messages"
|
"messages": "Messages"
|
||||||
},
|
},
|
||||||
|
"bank_account_page": {
|
||||||
|
"title": "Bank Account",
|
||||||
|
"linked_accounts": "LINKED ACCOUNTS",
|
||||||
|
"add_account": "Add New Account",
|
||||||
|
"secure_title": "100% Secured",
|
||||||
|
"secure_subtitle": "Your account details are encrypted and safe.",
|
||||||
|
"primary": "Primary",
|
||||||
|
"add_new_account": "Add New Account",
|
||||||
|
"routing_number": "Routing Number",
|
||||||
|
"routing_hint": "Enter routing number",
|
||||||
|
"account_number": "Account Number",
|
||||||
|
"account_hint": "Enter account number",
|
||||||
|
"account_type": "Account Type",
|
||||||
|
"checking": "Checking",
|
||||||
|
"savings": "Savings",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"save": "Save",
|
||||||
|
"account_ending": "Ending in $last4"
|
||||||
|
},
|
||||||
"logout": {
|
"logout": {
|
||||||
"button": "Sign Out"
|
"button": "Sign Out"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -503,6 +503,25 @@
|
|||||||
"privacy_security": "Privacidad y Seguridad",
|
"privacy_security": "Privacidad y Seguridad",
|
||||||
"messages": "Mensajes"
|
"messages": "Mensajes"
|
||||||
},
|
},
|
||||||
|
"bank_account_page": {
|
||||||
|
"title": "Cuenta Bancaria",
|
||||||
|
"linked_accounts": "Cuentas Vinculadas",
|
||||||
|
"add_account": "Agregar Cuenta Bancaria",
|
||||||
|
"secure_title": "Seguro y Cifrado",
|
||||||
|
"secure_subtitle": "Su información bancaria está cifrada y almacenada de forma segura. Nunca compartimos sus detalles.",
|
||||||
|
"add_new_account": "Agregar Nueva Cuenta",
|
||||||
|
"routing_number": "Número de Ruta",
|
||||||
|
"routing_hint": "9 dígitos",
|
||||||
|
"account_number": "Número de Cuenta",
|
||||||
|
"account_hint": "Ingrese número de cuenta",
|
||||||
|
"account_type": "Tipo de Cuenta",
|
||||||
|
"checking": "CORRIENTE",
|
||||||
|
"savings": "AHORROS",
|
||||||
|
"cancel": "Cancelar",
|
||||||
|
"save": "Guardar",
|
||||||
|
"primary": "Principal",
|
||||||
|
"account_ending": "Termina en $last4"
|
||||||
|
},
|
||||||
"logout": {
|
"logout": {
|
||||||
"button": "Cerrar Sesión"
|
"button": "Cerrar Sesión"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
/// Account type (Checking, Savings, etc).
|
||||||
|
enum BankAccountType {
|
||||||
|
checking,
|
||||||
|
savings,
|
||||||
|
other,
|
||||||
|
}
|
||||||
|
|
||||||
/// Represents bank account details for payroll.
|
/// Represents bank account details for payroll.
|
||||||
class BankAccount extends Equatable {
|
class BankAccount extends Equatable {
|
||||||
|
|
||||||
@@ -10,6 +17,9 @@ class BankAccount extends Equatable {
|
|||||||
required this.accountNumber,
|
required this.accountNumber,
|
||||||
required this.accountName,
|
required this.accountName,
|
||||||
this.sortCode,
|
this.sortCode,
|
||||||
|
this.type = BankAccountType.checking,
|
||||||
|
this.isPrimary = false,
|
||||||
|
this.last4,
|
||||||
});
|
});
|
||||||
/// Unique identifier.
|
/// Unique identifier.
|
||||||
final String id;
|
final String id;
|
||||||
@@ -29,6 +39,15 @@ class BankAccount extends Equatable {
|
|||||||
/// Sort code (if applicable).
|
/// Sort code (if applicable).
|
||||||
final String? sortCode;
|
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
|
@override
|
||||||
List<Object?> get props => <Object?>[id, userId, bankName, accountNumber, accountName, sortCode];
|
List<Object?> get props => <Object?>[id, userId, bankName, accountNumber, accountName, sortCode, type, isPrimary, last4];
|
||||||
}
|
}
|
||||||
@@ -22,8 +22,8 @@ extension HomeNavigator on IModularNavigator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Navigates to the payments page.
|
/// Navigates to the payments page.
|
||||||
void pushPayments() {
|
void navigateToPayments() {
|
||||||
pushNamed('/payments');
|
navigate('/worker-main/payments');
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Navigates to the shifts listing.
|
/// Navigates to the shifts listing.
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ class WorkerHomePage extends StatelessWidget {
|
|||||||
child: QuickActionItem(
|
child: QuickActionItem(
|
||||||
icon: LucideIcons.dollarSign,
|
icon: LucideIcons.dollarSign,
|
||||||
label: quickI18n.earnings,
|
label: quickI18n.earnings,
|
||||||
onTap: () => Modular.to.pushPayments(),
|
onTap: () => Modular.to.navigateToPayments(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class PendingPaymentCard extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final pendingI18n = t.staff.home.pending_payment;
|
final pendingI18n = t.staff.home.pending_payment;
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () => Modular.to.pushPayments(),
|
onTap: () => Modular.to.navigateToPayments(),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(UiConstants.space4),
|
padding: const EdgeInsets.all(UiConstants.space4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ extension ProfileNavigator on IModularNavigator {
|
|||||||
|
|
||||||
/// Navigates to the bank account page.
|
/// Navigates to the bank account page.
|
||||||
void pushBankAccount() {
|
void pushBankAccount() {
|
||||||
pushNamed('/bank-account');
|
pushNamed('../bank-account');
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Navigates to the timecard page.
|
/// Navigates to the timecard page.
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ class StaffProfilePage extends StatelessWidget {
|
|||||||
ProfileMenuItem(
|
ProfileMenuItem(
|
||||||
icon: UiIcons.creditCard,
|
icon: UiIcons.creditCard,
|
||||||
label: i18n.menu_items.payments,
|
label: i18n.menu_items.payments,
|
||||||
onTap: () => Modular.to.navigate('/payments'),
|
onTap: () => Modular.to.navigate('/worker-main/payments'),
|
||||||
),
|
),
|
||||||
ProfileMenuItem(
|
ProfileMenuItem(
|
||||||
icon: UiIcons.clock,
|
icon: UiIcons.clock,
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
// ignore_for_file: implementation_imports
|
||||||
|
import 'package:firebase_auth/firebase_auth.dart' as auth;
|
||||||
|
import 'package:firebase_data_connect/firebase_data_connect.dart';
|
||||||
|
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
import 'package:krow_data_connect/src/dataconnect_generated/generated.dart';
|
||||||
|
|
||||||
|
import '../../domain/repositories/bank_account_repository.dart';
|
||||||
|
|
||||||
|
/// Implementation of [BankAccountRepository].
|
||||||
|
class BankAccountRepositoryImpl implements BankAccountRepository {
|
||||||
|
final ExampleConnector _connector;
|
||||||
|
final auth.FirebaseAuth _auth;
|
||||||
|
|
||||||
|
BankAccountRepositoryImpl({
|
||||||
|
ExampleConnector? connector,
|
||||||
|
auth.FirebaseAuth? firebaseAuth,
|
||||||
|
}) : _connector = connector ?? ExampleConnector.instance,
|
||||||
|
_auth = firebaseAuth ?? auth.FirebaseAuth.instance;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<BankAccount>> getAccounts() async {
|
||||||
|
final auth.User? user = _auth.currentUser;
|
||||||
|
if (user == null) throw Exception('User not authenticated');
|
||||||
|
|
||||||
|
final QueryResult<GetAccountsByOwnerIdData, GetAccountsByOwnerIdVariables> result = await _connector.getAccountsByOwnerId(ownerId: user.uid).execute();
|
||||||
|
|
||||||
|
return result.data.accounts.map((GetAccountsByOwnerIdAccounts account) {
|
||||||
|
return BankAccount(
|
||||||
|
id: account.id,
|
||||||
|
userId: account.ownerId,
|
||||||
|
bankName: account.bank,
|
||||||
|
accountNumber: account.last4, // Using last4 as account number representation for now
|
||||||
|
last4: account.last4,
|
||||||
|
accountName: '', // Not returned by API
|
||||||
|
type: _mapAccountType(account.type),
|
||||||
|
isPrimary: account.isPrimary ?? false,
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> addAccount(BankAccount account) async {
|
||||||
|
final auth.User? user = _auth.currentUser;
|
||||||
|
if (user == null) throw Exception('User not authenticated');
|
||||||
|
|
||||||
|
await _connector.createAccount(
|
||||||
|
bank: account.bankName,
|
||||||
|
type: _mapDomainType(account.type),
|
||||||
|
last4: account.last4 ?? account.accountNumber.substring(account.accountNumber.length - 4),
|
||||||
|
ownerId: user.uid,
|
||||||
|
).isPrimary(account.isPrimary).execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
BankAccountType _mapAccountType(EnumValue<AccountType> type) {
|
||||||
|
if (type is Known<AccountType>) {
|
||||||
|
switch (type.value) {
|
||||||
|
case AccountType.CHECKING:
|
||||||
|
return BankAccountType.checking;
|
||||||
|
case AccountType.SAVINGS:
|
||||||
|
return BankAccountType.savings;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return BankAccountType.other;
|
||||||
|
}
|
||||||
|
|
||||||
|
AccountType _mapDomainType(BankAccountType type) {
|
||||||
|
switch (type) {
|
||||||
|
case BankAccountType.checking:
|
||||||
|
return AccountType.CHECKING;
|
||||||
|
case BankAccountType.savings:
|
||||||
|
return AccountType.SAVINGS;
|
||||||
|
default:
|
||||||
|
return AccountType.CHECKING; // Default fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:krow_core/core.dart';
|
||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
|
/// Arguments for adding a bank account.
|
||||||
|
class AddBankAccountParams extends UseCaseArgument with EquatableMixin {
|
||||||
|
final BankAccount account;
|
||||||
|
|
||||||
|
const AddBankAccountParams({required this.account});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [account];
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool? get stringify => true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
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();
|
||||||
|
|
||||||
|
/// adds a new bank account.
|
||||||
|
Future<void> addAccount(BankAccount account);
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import 'package:krow_core/core.dart';
|
||||||
|
import '../repositories/bank_account_repository.dart';
|
||||||
|
import '../arguments/add_bank_account_params.dart';
|
||||||
|
|
||||||
|
/// Use case to add a bank account.
|
||||||
|
class AddBankAccountUseCase implements UseCase<AddBankAccountParams, void> {
|
||||||
|
final BankAccountRepository _repository;
|
||||||
|
|
||||||
|
AddBankAccountUseCase(this._repository);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> call(AddBankAccountParams params) {
|
||||||
|
return _repository.addAccount(params.account);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import 'package:krow_core/core.dart'; // For UseCase
|
||||||
|
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>> {
|
||||||
|
final BankAccountRepository _repository;
|
||||||
|
|
||||||
|
GetBankAccountsUseCase(this._repository);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<BankAccount>> call() {
|
||||||
|
return _repository.getAccounts();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:krow_core/core.dart';
|
||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
import '../../domain/arguments/add_bank_account_params.dart';
|
||||||
|
import '../../domain/usecases/add_bank_account_usecase.dart';
|
||||||
|
import '../../domain/usecases/get_bank_accounts_usecase.dart';
|
||||||
|
import 'bank_account_state.dart';
|
||||||
|
|
||||||
|
class BankAccountCubit extends Cubit<BankAccountState> {
|
||||||
|
final GetBankAccountsUseCase _getBankAccountsUseCase;
|
||||||
|
final AddBankAccountUseCase _addBankAccountUseCase;
|
||||||
|
|
||||||
|
BankAccountCubit({
|
||||||
|
required GetBankAccountsUseCase getBankAccountsUseCase,
|
||||||
|
required AddBankAccountUseCase addBankAccountUseCase,
|
||||||
|
}) : _getBankAccountsUseCase = getBankAccountsUseCase,
|
||||||
|
_addBankAccountUseCase = addBankAccountUseCase,
|
||||||
|
super(const BankAccountState());
|
||||||
|
|
||||||
|
Future<void> loadAccounts() async {
|
||||||
|
emit(state.copyWith(status: BankAccountStatus.loading));
|
||||||
|
try {
|
||||||
|
final accounts = await _getBankAccountsUseCase();
|
||||||
|
emit(state.copyWith(
|
||||||
|
status: BankAccountStatus.loaded,
|
||||||
|
accounts: accounts,
|
||||||
|
));
|
||||||
|
} catch (e) {
|
||||||
|
emit(state.copyWith(
|
||||||
|
status: BankAccountStatus.error,
|
||||||
|
errorMessage: e.toString(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void toggleForm(bool show) {
|
||||||
|
emit(state.copyWith(showForm: show));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> addAccount({
|
||||||
|
required String routingNumber,
|
||||||
|
required String accountNumber,
|
||||||
|
required String type,
|
||||||
|
}) async {
|
||||||
|
emit(state.copyWith(status: BankAccountStatus.loading));
|
||||||
|
|
||||||
|
// Create domain entity
|
||||||
|
final newAccount = BankAccount(
|
||||||
|
id: '', // Generated by server usually
|
||||||
|
userId: '', // Handled by Repo/Auth
|
||||||
|
bankName: 'New Bank', // Mock
|
||||||
|
accountNumber: accountNumber,
|
||||||
|
accountName: '',
|
||||||
|
type: type == 'CHECKING' ? BankAccountType.checking : BankAccountType.savings,
|
||||||
|
last4: accountNumber.length > 4 ? accountNumber.substring(accountNumber.length - 4) : accountNumber,
|
||||||
|
isPrimary: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await _addBankAccountUseCase(AddBankAccountParams(account: newAccount));
|
||||||
|
|
||||||
|
// Re-fetch to get latest state including server-generated IDs
|
||||||
|
await loadAccounts();
|
||||||
|
|
||||||
|
emit(state.copyWith(
|
||||||
|
showForm: false, // Close form on success
|
||||||
|
));
|
||||||
|
} catch (e) {
|
||||||
|
emit(state.copyWith(
|
||||||
|
status: BankAccountStatus.error,
|
||||||
|
errorMessage: e.toString(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
|
enum BankAccountStatus { initial, loading, loaded, error }
|
||||||
|
|
||||||
|
class BankAccountState extends Equatable {
|
||||||
|
final BankAccountStatus status;
|
||||||
|
final List<BankAccount> accounts;
|
||||||
|
final String? errorMessage;
|
||||||
|
final bool showForm;
|
||||||
|
|
||||||
|
const BankAccountState({
|
||||||
|
this.status = BankAccountStatus.initial,
|
||||||
|
this.accounts = const [],
|
||||||
|
this.errorMessage,
|
||||||
|
this.showForm = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
BankAccountState copyWith({
|
||||||
|
BankAccountStatus? status,
|
||||||
|
List<BankAccount>? accounts,
|
||||||
|
String? errorMessage,
|
||||||
|
bool? showForm,
|
||||||
|
}) {
|
||||||
|
return BankAccountState(
|
||||||
|
status: status ?? this.status,
|
||||||
|
accounts: accounts ?? this.accounts,
|
||||||
|
errorMessage: errorMessage,
|
||||||
|
showForm: showForm ?? this.showForm,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [status, accounts, errorMessage, showForm];
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
|
||||||
|
extension StaffBankAccountNavigator on IModularNavigator {
|
||||||
|
void popPage() {
|
||||||
|
pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:core_localization/core_localization.dart';
|
||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
// ignore: depend_on_referenced_packages
|
||||||
|
|
||||||
|
import '../blocs/bank_account_cubit.dart';
|
||||||
|
import '../blocs/bank_account_state.dart';
|
||||||
|
import '../navigation/staff_bank_account_navigator.dart';
|
||||||
|
import '../widgets/add_account_form.dart';
|
||||||
|
|
||||||
|
class BankAccountPage extends StatelessWidget {
|
||||||
|
const BankAccountPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final BankAccountCubit cubit = Modular.get<BankAccountCubit>();
|
||||||
|
// Load accounts initially
|
||||||
|
if (cubit.state.status == BankAccountStatus.initial) {
|
||||||
|
cubit.loadAccounts();
|
||||||
|
}
|
||||||
|
|
||||||
|
// final t = AppTranslation.current; // Replaced
|
||||||
|
final Translations t = Translations.of(context);
|
||||||
|
final dynamic strings = t.staff.profile.bank_account_page;
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: UiColors.background,
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: UiColors.background, // Was surface
|
||||||
|
elevation: 0,
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(UiIcons.arrowLeft, color: UiColors.textSecondary),
|
||||||
|
onPressed: () => Modular.to.popPage(),
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
strings.title,
|
||||||
|
style: UiTypography.headline3m.copyWith(color: UiColors.textPrimary),
|
||||||
|
),
|
||||||
|
bottom: PreferredSize(
|
||||||
|
preferredSize: const Size.fromHeight(1.0),
|
||||||
|
child: Container(color: UiColors.border, height: 1.0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: BlocBuilder<BankAccountCubit, BankAccountState>(
|
||||||
|
bloc: cubit,
|
||||||
|
builder: (BuildContext context, BankAccountState state) {
|
||||||
|
if (state.status == BankAccountStatus.loading && state.accounts.isEmpty) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.status == BankAccountStatus.error) {
|
||||||
|
return Center(child: Text(state.errorMessage ?? 'Error'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: <Widget>[
|
||||||
|
Expanded(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(UiConstants.space4),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
_buildSecurityNotice(strings),
|
||||||
|
const SizedBox(height: UiConstants.space6),
|
||||||
|
Text(
|
||||||
|
strings.linked_accounts,
|
||||||
|
style: UiTypography.headline4m.copyWith(color: UiColors.textPrimary),
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space3),
|
||||||
|
...state.accounts.map((BankAccount a) => _buildAccountCard(a, strings)), // Added type
|
||||||
|
|
||||||
|
if (state.showForm) ...<Widget>[
|
||||||
|
const SizedBox(height: UiConstants.space6),
|
||||||
|
AddAccountForm(
|
||||||
|
strings: strings,
|
||||||
|
onSubmit: (String routing, String account, String type) { // Added types
|
||||||
|
cubit.addAccount(
|
||||||
|
routingNumber: routing,
|
||||||
|
accountNumber: account,
|
||||||
|
type: type);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
// Add extra padding at bottom
|
||||||
|
const SizedBox(height: 80),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (!state.showForm)
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(UiConstants.space5),
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: UiColors.background, // Was surface
|
||||||
|
border: Border(top: BorderSide(color: UiColors.border)),
|
||||||
|
),
|
||||||
|
child: SafeArea(
|
||||||
|
child: UiButton.primary(
|
||||||
|
text: strings.add_account,
|
||||||
|
leadingIcon: UiIcons.add,
|
||||||
|
onPressed: () => cubit.toggleForm(true),
|
||||||
|
fullWidth: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSecurityNotice(dynamic strings) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(UiConstants.space4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.primary.withOpacity(0.08),
|
||||||
|
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
const Icon(UiIcons.shield, color: UiColors.primary, size: 20),
|
||||||
|
const SizedBox(width: UiConstants.space3),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
strings.secure_title,
|
||||||
|
style: UiTypography.body2r.copyWith( // Was body2
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: UiColors.textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(
|
||||||
|
strings.secure_subtitle,
|
||||||
|
style: UiTypography.body3r.copyWith(color: UiColors.textSecondary), // Was bodySmall
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildAccountCard(BankAccount account, dynamic strings) {
|
||||||
|
final bool isPrimary = account.isPrimary;
|
||||||
|
const Color primaryColor = UiColors.primary;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: UiConstants.space3),
|
||||||
|
padding: const EdgeInsets.all(UiConstants.space4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.bgPopup, // Was surface, using bgPopup (white) for card
|
||||||
|
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||||
|
border: Border.all(
|
||||||
|
color: isPrimary ? primaryColor : UiColors.border,
|
||||||
|
width: isPrimary ? 2 : 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: <Widget>[
|
||||||
|
Row(
|
||||||
|
children: <Widget>[
|
||||||
|
Container(
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: primaryColor.withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||||
|
),
|
||||||
|
child: const Center(
|
||||||
|
child: Icon(
|
||||||
|
UiIcons.building,
|
||||||
|
color: primaryColor,
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: UiConstants.space3),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
account.bankName,
|
||||||
|
style: UiTypography.body2r.copyWith( // Was body2
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: UiColors.textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
strings.account_ending(last4: account.last4),
|
||||||
|
style: UiTypography.body2r.copyWith( // Was body2
|
||||||
|
color: UiColors.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (isPrimary)
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: primaryColor.withOpacity(0.15),
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: <Widget>[
|
||||||
|
const Icon(UiIcons.check, size: 12, color: primaryColor),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
strings.primary,
|
||||||
|
style: UiTypography.body3r.copyWith( // Was bodySmall
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: primaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import '../blocs/bank_account_cubit.dart';
|
||||||
|
|
||||||
|
class AddAccountForm extends StatefulWidget {
|
||||||
|
final dynamic strings;
|
||||||
|
final Function(String routing, String account, String type) onSubmit;
|
||||||
|
|
||||||
|
const AddAccountForm({super.key, required this.strings, required this.onSubmit});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AddAccountForm> createState() => _AddAccountFormState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AddAccountFormState extends State<AddAccountForm> {
|
||||||
|
final TextEditingController _routingController = TextEditingController();
|
||||||
|
final TextEditingController _accountController = TextEditingController();
|
||||||
|
String _selectedType = 'CHECKING';
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_routingController.dispose();
|
||||||
|
_accountController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(UiConstants.space4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.bgPopup, // Was surface
|
||||||
|
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||||
|
border: Border.all(color: UiColors.border),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
widget.strings.add_new_account,
|
||||||
|
style: UiTypography.headline4m.copyWith(color: UiColors.textPrimary), // Was header4
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space4),
|
||||||
|
UiTextField(
|
||||||
|
label: widget.strings.routing_number,
|
||||||
|
hintText: widget.strings.routing_hint,
|
||||||
|
controller: _routingController,
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space4),
|
||||||
|
UiTextField(
|
||||||
|
label: widget.strings.account_number,
|
||||||
|
hintText: widget.strings.account_hint,
|
||||||
|
controller: _accountController,
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space4),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: UiConstants.space2),
|
||||||
|
child: Text(
|
||||||
|
widget.strings.account_type,
|
||||||
|
style: UiTypography.body2r.copyWith( // Was body2
|
||||||
|
color: UiColors.textSecondary, fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: <Widget>[
|
||||||
|
Expanded(
|
||||||
|
child: _buildTypeButton('CHECKING', widget.strings.checking)),
|
||||||
|
const SizedBox(width: UiConstants.space2),
|
||||||
|
Expanded(
|
||||||
|
child: _buildTypeButton('SAVINGS', widget.strings.savings)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space6),
|
||||||
|
Row(
|
||||||
|
children: <Widget>[
|
||||||
|
Expanded(
|
||||||
|
child: UiButton.text(
|
||||||
|
text: widget.strings.cancel,
|
||||||
|
onPressed: () {
|
||||||
|
Modular.get<BankAccountCubit>().toggleForm(false);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: UiConstants.space2),
|
||||||
|
Expanded(
|
||||||
|
child: UiButton.primary(
|
||||||
|
text: widget.strings.save,
|
||||||
|
onPressed: () {
|
||||||
|
widget.onSubmit(
|
||||||
|
_routingController.text,
|
||||||
|
_accountController.text,
|
||||||
|
_selectedType,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTypeButton(String type, String label) {
|
||||||
|
final bool isSelected = _selectedType == type;
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => setState(() => _selectedType = type),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isSelected
|
||||||
|
? UiColors.primary.withOpacity(0.05)
|
||||||
|
: UiColors.bgPopup, // Was surface
|
||||||
|
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||||
|
border: Border.all(
|
||||||
|
color: isSelected ? UiColors.primary : UiColors.border,
|
||||||
|
width: isSelected ? 2 : 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
style: UiTypography.body2r.copyWith( // Was body2
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: isSelected ? UiColors.primary : UiColors.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
import 'package:staff_bank_account/src/data/repositories/bank_account_repository_impl.dart';
|
||||||
|
import 'presentation/blocs/bank_account_cubit.dart';
|
||||||
|
import 'presentation/pages/bank_account_page.dart';
|
||||||
|
import 'domain/repositories/bank_account_repository.dart';
|
||||||
|
import 'domain/usecases/get_bank_accounts_usecase.dart';
|
||||||
|
import 'domain/usecases/add_bank_account_usecase.dart';
|
||||||
|
|
||||||
|
class StaffBankAccountModule extends Module {
|
||||||
|
@override
|
||||||
|
void binds(Injector i) {
|
||||||
|
// Repositories
|
||||||
|
i.add<BankAccountRepository>(BankAccountRepositoryImpl.new);
|
||||||
|
|
||||||
|
// Use Cases
|
||||||
|
i.add(GetBankAccountsUseCase.new);
|
||||||
|
i.add(AddBankAccountUseCase.new);
|
||||||
|
|
||||||
|
// Blocs
|
||||||
|
i.addSingleton<BankAccountCubit>(
|
||||||
|
() => BankAccountCubit(
|
||||||
|
getBankAccountsUseCase: i.get<GetBankAccountsUseCase>(),
|
||||||
|
addBankAccountUseCase: i.get<AddBankAccountUseCase>(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void routes(RouteManager r) {
|
||||||
|
r.child('/', child: (_) => const BankAccountPage());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
library staff_bank_account;
|
||||||
|
|
||||||
|
export 'src/staff_bank_account_module.dart';
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
name: staff_bank_account
|
||||||
|
description: Staff Bank Account feature.
|
||||||
|
version: 0.0.1
|
||||||
|
publish_to: none
|
||||||
|
resolution: workspace
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: '>=3.10.0 <4.0.0'
|
||||||
|
flutter: ">=3.0.0"
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
flutter:
|
||||||
|
sdk: flutter
|
||||||
|
flutter_bloc: ^8.1.0
|
||||||
|
bloc: ^8.1.0
|
||||||
|
flutter_modular: ^6.3.0
|
||||||
|
equatable: ^2.0.5
|
||||||
|
lucide_icons: ^0.257.0
|
||||||
|
|
||||||
|
# Architecture Packages
|
||||||
|
design_system:
|
||||||
|
path: ../../../../../design_system
|
||||||
|
core_localization:
|
||||||
|
path: ../../../../../core_localization
|
||||||
|
krow_core:
|
||||||
|
path: ../../../../../core
|
||||||
|
krow_domain:
|
||||||
|
path: ../../../../../domain
|
||||||
|
krow_data_connect:
|
||||||
|
path: ../../../../../data_connect
|
||||||
|
|
||||||
|
dev_dependencies:
|
||||||
|
flutter_test:
|
||||||
|
sdk: flutter
|
||||||
|
flutter_lints: ^2.0.0
|
||||||
@@ -5,6 +5,7 @@ import 'package:staff_profile/staff_profile.dart';
|
|||||||
import 'package:staff_profile_info/staff_profile_info.dart';
|
import 'package:staff_profile_info/staff_profile_info.dart';
|
||||||
import 'package:staff_emergency_contact/staff_emergency_contact.dart';
|
import 'package:staff_emergency_contact/staff_emergency_contact.dart';
|
||||||
import 'package:staff_profile_experience/staff_profile_experience.dart';
|
import 'package:staff_profile_experience/staff_profile_experience.dart';
|
||||||
|
import 'package:staff_bank_account/staff_bank_account.dart';
|
||||||
|
|
||||||
import 'package:staff_main/src/presentation/blocs/staff_main_cubit.dart';
|
import 'package:staff_main/src/presentation/blocs/staff_main_cubit.dart';
|
||||||
import 'package:staff_main/src/presentation/constants/staff_main_routes.dart';
|
import 'package:staff_main/src/presentation/constants/staff_main_routes.dart';
|
||||||
@@ -51,5 +52,6 @@ class StaffMainModule extends Module {
|
|||||||
r.module('/onboarding', module: StaffProfileInfoModule());
|
r.module('/onboarding', module: StaffProfileInfoModule());
|
||||||
r.module('/emergency-contact', module: StaffEmergencyContactModule());
|
r.module('/emergency-contact', module: StaffEmergencyContactModule());
|
||||||
r.module('/experience', module: StaffProfileExperienceModule());
|
r.module('/experience', module: StaffProfileExperienceModule());
|
||||||
|
r.module('/bank-account', module: StaffBankAccountModule());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ dependencies:
|
|||||||
path: ../profile_sections/onboarding/emergency_contact
|
path: ../profile_sections/onboarding/emergency_contact
|
||||||
staff_profile_experience:
|
staff_profile_experience:
|
||||||
path: ../profile_sections/onboarding/experience
|
path: ../profile_sections/onboarding/experience
|
||||||
|
staff_bank_account:
|
||||||
|
path: ../profile_sections/finances/staff_bank_account
|
||||||
# staff_shifts:
|
# staff_shifts:
|
||||||
# path: ../shifts
|
# path: ../shifts
|
||||||
# staff_payments:
|
# staff_payments:
|
||||||
|
|||||||
@@ -1064,6 +1064,13 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.12.1"
|
version: "1.12.1"
|
||||||
|
staff_bank_account:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
path: "packages/features/staff/profile_sections/finances/staff_bank_account"
|
||||||
|
relative: true
|
||||||
|
source: path
|
||||||
|
version: "0.0.1"
|
||||||
stream_channel:
|
stream_channel:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
Reference in New Issue
Block a user