diff --git a/apps/mobile/apps/staff/lib/main.dart b/apps/mobile/apps/staff/lib/main.dart index ce6f8ff0..92770719 100644 --- a/apps/mobile/apps/staff/lib/main.dart +++ b/apps/mobile/apps/staff/lib/main.dart @@ -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, ], - ); + )); }, ), ); diff --git a/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json b/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json index f3390399..70da96c9 100644 --- a/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json +++ b/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json @@ -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" } diff --git a/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json b/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json index 4326451f..a39a1344 100644 --- a/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json +++ b/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json @@ -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" } diff --git a/apps/mobile/packages/domain/lib/src/entities/profile/bank_account.dart b/apps/mobile/packages/domain/lib/src/entities/profile/bank_account.dart index 91ff1f5b..deca9a28 100644 --- a/apps/mobile/packages/domain/lib/src/entities/profile/bank_account.dart +++ b/apps/mobile/packages/domain/lib/src/entities/profile/bank_account.dart @@ -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 get props => [id, userId, bankName, accountNumber, accountName, sortCode]; + List get props => [id, userId, bankName, accountNumber, accountName, sortCode, type, isPrimary, last4]; } \ No newline at end of file diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/navigation/home_navigator.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/navigation/home_navigator.dart index 9b569696..e3671168 100644 --- a/apps/mobile/packages/features/staff/home/lib/src/presentation/navigation/home_navigator.dart +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/navigation/home_navigator.dart @@ -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. diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/pages/worker_home_page.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/pages/worker_home_page.dart index cdc321f4..4b323dba 100644 --- a/apps/mobile/packages/features/staff/home/lib/src/presentation/pages/worker_home_page.dart +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/pages/worker_home_page.dart @@ -131,7 +131,7 @@ class WorkerHomePage extends StatelessWidget { child: QuickActionItem( icon: LucideIcons.dollarSign, label: quickI18n.earnings, - onTap: () => Modular.to.pushPayments(), + onTap: () => Modular.to.navigateToPayments(), ), ), ], diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/pending_payment_card.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/pending_payment_card.dart index 69c2e506..36271577 100644 --- a/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/pending_payment_card.dart +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/pending_payment_card.dart @@ -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( diff --git a/apps/mobile/packages/features/staff/profile/lib/src/presentation/navigation/profile_navigator.dart b/apps/mobile/packages/features/staff/profile/lib/src/presentation/navigation/profile_navigator.dart index 9f6012ca..fccae4c9 100644 --- a/apps/mobile/packages/features/staff/profile/lib/src/presentation/navigation/profile_navigator.dart +++ b/apps/mobile/packages/features/staff/profile/lib/src/presentation/navigation/profile_navigator.dart @@ -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. diff --git a/apps/mobile/packages/features/staff/profile/lib/src/presentation/pages/staff_profile_page.dart b/apps/mobile/packages/features/staff/profile/lib/src/presentation/pages/staff_profile_page.dart index e5e28b50..c81dc81a 100644 --- a/apps/mobile/packages/features/staff/profile/lib/src/presentation/pages/staff_profile_page.dart +++ b/apps/mobile/packages/features/staff/profile/lib/src/presentation/pages/staff_profile_page.dart @@ -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, diff --git a/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/data/repositories/bank_account_repository_impl.dart b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/data/repositories/bank_account_repository_impl.dart new file mode 100644 index 00000000..a939d6e6 --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/data/repositories/bank_account_repository_impl.dart @@ -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> getAccounts() async { + final auth.User? user = _auth.currentUser; + if (user == null) throw Exception('User not authenticated'); + + final QueryResult 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 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 type) { + if (type is Known) { + 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 + } + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/domain/arguments/add_bank_account_params.dart b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/domain/arguments/add_bank_account_params.dart new file mode 100644 index 00000000..ead4135d --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/domain/arguments/add_bank_account_params.dart @@ -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 get props => [account]; + + @override + bool? get stringify => true; +} diff --git a/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/domain/repositories/bank_account_repository.dart b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/domain/repositories/bank_account_repository.dart new file mode 100644 index 00000000..3e701aba --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/domain/repositories/bank_account_repository.dart @@ -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> getAccounts(); + + /// adds a new bank account. + Future addAccount(BankAccount account); +} diff --git a/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/domain/usecases/add_bank_account_usecase.dart b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/domain/usecases/add_bank_account_usecase.dart new file mode 100644 index 00000000..48d4a863 --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/domain/usecases/add_bank_account_usecase.dart @@ -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 { + final BankAccountRepository _repository; + + AddBankAccountUseCase(this._repository); + + @override + Future call(AddBankAccountParams params) { + return _repository.addAccount(params.account); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/domain/usecases/get_bank_accounts_usecase.dart b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/domain/usecases/get_bank_accounts_usecase.dart new file mode 100644 index 00000000..2ee64df3 --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/domain/usecases/get_bank_accounts_usecase.dart @@ -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> { + final BankAccountRepository _repository; + + GetBankAccountsUseCase(this._repository); + + @override + Future> call() { + return _repository.getAccounts(); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/blocs/bank_account_cubit.dart b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/blocs/bank_account_cubit.dart new file mode 100644 index 00000000..52e9a9b4 --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/blocs/bank_account_cubit.dart @@ -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 { + final GetBankAccountsUseCase _getBankAccountsUseCase; + final AddBankAccountUseCase _addBankAccountUseCase; + + BankAccountCubit({ + required GetBankAccountsUseCase getBankAccountsUseCase, + required AddBankAccountUseCase addBankAccountUseCase, + }) : _getBankAccountsUseCase = getBankAccountsUseCase, + _addBankAccountUseCase = addBankAccountUseCase, + super(const BankAccountState()); + + Future 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 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(), + )); + } + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/blocs/bank_account_state.dart b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/blocs/bank_account_state.dart new file mode 100644 index 00000000..30a5e8c0 --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/blocs/bank_account_state.dart @@ -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 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? accounts, + String? errorMessage, + bool? showForm, + }) { + return BankAccountState( + status: status ?? this.status, + accounts: accounts ?? this.accounts, + errorMessage: errorMessage, + showForm: showForm ?? this.showForm, + ); + } + + @override + List get props => [status, accounts, errorMessage, showForm]; +} diff --git a/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/navigation/staff_bank_account_navigator.dart b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/navigation/staff_bank_account_navigator.dart new file mode 100644 index 00000000..ef1b11fb --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/navigation/staff_bank_account_navigator.dart @@ -0,0 +1,7 @@ +import 'package:flutter_modular/flutter_modular.dart'; + +extension StaffBankAccountNavigator on IModularNavigator { + void popPage() { + pop(); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/pages/bank_account_page.dart b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/pages/bank_account_page.dart new file mode 100644 index 00000000..2951b35a --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/pages/bank_account_page.dart @@ -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(); + // 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( + 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: [ + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(UiConstants.space4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _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) ...[ + 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: [ + const Icon(UiIcons.shield, color: UiColors.primary, size: 20), + const SizedBox(width: UiConstants.space3), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + 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: [ + Row( + children: [ + 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: [ + 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: [ + 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, + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/widgets/add_account_form.dart b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/widgets/add_account_form.dart new file mode 100644 index 00000000..61dc8cff --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/presentation/widgets/add_account_form.dart @@ -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 createState() => _AddAccountFormState(); +} + +class _AddAccountFormState extends State { + 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: [ + 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: [ + 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: [ + Expanded( + child: UiButton.text( + text: widget.strings.cancel, + onPressed: () { + Modular.get().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, + ), + ), + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/staff_bank_account_module.dart b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/staff_bank_account_module.dart new file mode 100644 index 00000000..ba99cb14 --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/src/staff_bank_account_module.dart @@ -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(BankAccountRepositoryImpl.new); + + // Use Cases + i.add(GetBankAccountsUseCase.new); + i.add(AddBankAccountUseCase.new); + + // Blocs + i.addSingleton( + () => BankAccountCubit( + getBankAccountsUseCase: i.get(), + addBankAccountUseCase: i.get(), + ), + ); + } + + @override + void routes(RouteManager r) { + r.child('/', child: (_) => const BankAccountPage()); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/staff_bank_account.dart b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/staff_bank_account.dart new file mode 100644 index 00000000..226d9758 --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/lib/staff_bank_account.dart @@ -0,0 +1,3 @@ +library staff_bank_account; + +export 'src/staff_bank_account_module.dart'; diff --git a/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/pubspec.yaml b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/pubspec.yaml new file mode 100644 index 00000000..068cb813 --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/finances/staff_bank_account/pubspec.yaml @@ -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 diff --git a/apps/mobile/packages/features/staff/staff_main/lib/src/staff_main_module.dart b/apps/mobile/packages/features/staff/staff_main/lib/src/staff_main_module.dart index d00a7c52..98d776df 100644 --- a/apps/mobile/packages/features/staff/staff_main/lib/src/staff_main_module.dart +++ b/apps/mobile/packages/features/staff/staff_main/lib/src/staff_main_module.dart @@ -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()); } } diff --git a/apps/mobile/packages/features/staff/staff_main/pubspec.yaml b/apps/mobile/packages/features/staff/staff_main/pubspec.yaml index e513f6bd..706e3bbb 100644 --- a/apps/mobile/packages/features/staff/staff_main/pubspec.yaml +++ b/apps/mobile/packages/features/staff/staff_main/pubspec.yaml @@ -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: diff --git a/apps/mobile/pubspec.lock b/apps/mobile/pubspec.lock index 21ca16ea..7d289cde 100644 --- a/apps/mobile/pubspec.lock +++ b/apps/mobile/pubspec.lock @@ -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: