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
|
||||
>(
|
||||
builder: (BuildContext context, core_localization.LocaleState state) {
|
||||
return MaterialApp.router(
|
||||
return core_localization.TranslationProvider(
|
||||
child: MaterialApp.router(
|
||||
title: "KROW Staff",
|
||||
theme: UiTheme.light,
|
||||
routerConfig: Modular.routerConfig,
|
||||
@@ -55,7 +56,7 @@ class AppWidget extends StatelessWidget {
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
);
|
||||
));
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@@ -504,6 +504,25 @@
|
||||
"privacy_security": "Privacy & Security",
|
||||
"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": {
|
||||
"button": "Sign Out"
|
||||
}
|
||||
|
||||
@@ -503,6 +503,25 @@
|
||||
"privacy_security": "Privacidad y Seguridad",
|
||||
"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": {
|
||||
"button": "Cerrar Sesión"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
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 {
|
||||
|
||||
@@ -10,6 +17,9 @@ class BankAccount extends Equatable {
|
||||
required this.accountNumber,
|
||||
required this.accountName,
|
||||
this.sortCode,
|
||||
this.type = BankAccountType.checking,
|
||||
this.isPrimary = false,
|
||||
this.last4,
|
||||
});
|
||||
/// Unique identifier.
|
||||
final String id;
|
||||
@@ -29,6 +39,15 @@ class BankAccount extends Equatable {
|
||||
/// 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];
|
||||
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.
|
||||
void pushPayments() {
|
||||
pushNamed('/payments');
|
||||
void navigateToPayments() {
|
||||
navigate('/worker-main/payments');
|
||||
}
|
||||
|
||||
/// Navigates to the shifts listing.
|
||||
|
||||
@@ -131,7 +131,7 @@ class WorkerHomePage extends StatelessWidget {
|
||||
child: QuickActionItem(
|
||||
icon: LucideIcons.dollarSign,
|
||||
label: quickI18n.earnings,
|
||||
onTap: () => Modular.to.pushPayments(),
|
||||
onTap: () => Modular.to.navigateToPayments(),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -16,7 +16,7 @@ class PendingPaymentCard extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final pendingI18n = t.staff.home.pending_payment;
|
||||
return GestureDetector(
|
||||
onTap: () => Modular.to.pushPayments(),
|
||||
onTap: () => Modular.to.navigateToPayments(),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
|
||||
@@ -58,7 +58,7 @@ extension ProfileNavigator on IModularNavigator {
|
||||
|
||||
/// Navigates to the bank account page.
|
||||
void pushBankAccount() {
|
||||
pushNamed('/bank-account');
|
||||
pushNamed('../bank-account');
|
||||
}
|
||||
|
||||
/// Navigates to the timecard page.
|
||||
|
||||
@@ -186,7 +186,7 @@ class StaffProfilePage extends StatelessWidget {
|
||||
ProfileMenuItem(
|
||||
icon: UiIcons.creditCard,
|
||||
label: i18n.menu_items.payments,
|
||||
onTap: () => Modular.to.navigate('/payments'),
|
||||
onTap: () => Modular.to.navigate('/worker-main/payments'),
|
||||
),
|
||||
ProfileMenuItem(
|
||||
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_emergency_contact/staff_emergency_contact.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/constants/staff_main_routes.dart';
|
||||
@@ -51,5 +52,6 @@ class StaffMainModule extends Module {
|
||||
r.module('/onboarding', module: StaffProfileInfoModule());
|
||||
r.module('/emergency-contact', module: StaffEmergencyContactModule());
|
||||
r.module('/experience', module: StaffProfileExperienceModule());
|
||||
r.module('/bank-account', module: StaffBankAccountModule());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,8 @@ dependencies:
|
||||
path: ../profile_sections/onboarding/emergency_contact
|
||||
staff_profile_experience:
|
||||
path: ../profile_sections/onboarding/experience
|
||||
staff_bank_account:
|
||||
path: ../profile_sections/finances/staff_bank_account
|
||||
# staff_shifts:
|
||||
# path: ../shifts
|
||||
# staff_payments:
|
||||
|
||||
@@ -1064,6 +1064,13 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
Reference in New Issue
Block a user