diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/data/repositories/tax_forms_repository_impl.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/data/repositories/tax_forms_repository_impl.dart new file mode 100644 index 00000000..89eecc7a --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/data/repositories/tax_forms_repository_impl.dart @@ -0,0 +1,206 @@ +import 'dart:async'; + +import 'package:firebase_auth/firebase_auth.dart' hide User; +import 'package:firebase_data_connect/firebase_data_connect.dart'; +import 'package:krow_data_connect/krow_data_connect.dart' as dc; + +import '../../domain/entities/tax_form_entity.dart'; +import '../../domain/repositories/tax_forms_repository.dart'; + +class TaxFormsRepositoryImpl implements TaxFormsRepository { + TaxFormsRepositoryImpl({ + required this.firebaseAuth, + required this.dataConnect, + }); + + final FirebaseAuth firebaseAuth; + final dc.ExampleConnector dataConnect; + + String? _staffId; + + Future _getStaffId() async { + if (_staffId != null) return _staffId!; + + final user = firebaseAuth.currentUser; + if (user == null) { + throw Exception('User not logged in'); + } + + final result = + await dataConnect.getStaffByUserId(userId: user.uid).execute(); + final staffs = result.data.staffs; + if (staffs.isEmpty) { + // This might happen if the user is logged in but staff profile isn't created yet or not synced. + // For now, fail hard. Code can be improved to handle this case properly. + throw Exception('No staff profile found for user'); + } + + _staffId = staffs.first.id; + return _staffId!; + } + + @override + Future> getTaxForms() async { + final staffId = await _getStaffId(); + final result = + await dataConnect.getTaxFormsBystaffId(staffId: staffId).execute(); + + final forms = result.data.taxForms.map((e) => _mapToEntity(e)).toList(); + + // Check if required forms exist, create if not. + final typesPresent = forms.map((f) => f.type).toSet(); + bool createdNew = false; + + if (!typesPresent.contains(TaxFormType.i9)) { + await _createInitialForm(staffId, TaxFormType.i9); + createdNew = true; + } + if (!typesPresent.contains(TaxFormType.w4)) { + await _createInitialForm(staffId, TaxFormType.w4); + createdNew = true; + } + + if (createdNew) { + final result2 = + await dataConnect.getTaxFormsBystaffId(staffId: staffId).execute(); + return result2.data.taxForms.map((e) => _mapToEntity(e)).toList(); + } + + return forms; + } + + Future _createInitialForm(String staffId, TaxFormType type) async { + String title = ''; + String subtitle = ''; + String description = ''; + + if (type == TaxFormType.i9) { + title = 'Form I-9'; + subtitle = 'Employment Eligibility Verification'; + description = 'Required for all new hires to verify identity.'; + } else { + title = 'Form W-4'; + subtitle = 'Employee\'s Withholding Certificate'; + description = 'Determines federal income tax withholding.'; + } + + await dataConnect + .createTaxForm( + staffId: staffId, + formType: _mapTypeToGenerated(type), + title: title, + ) + .subtitle(subtitle) + .description(description) + .status(dc.TaxFormStatus.NOT_STARTED) + .execute(); + } + + @override + Future submitForm(TaxFormType type, Map data) async { + final staffId = await _getStaffId(); + final result = + await dataConnect.getTaxFormsBystaffId(staffId: staffId).execute(); + final targetTypeString = _mapTypeToGenerated(type).name; + + final form = result.data.taxForms.firstWhere( + (e) => e.formType.stringValue == targetTypeString, + orElse: () => throw Exception('Form not found for submission'), + ); + + // AnyValue expects a scalar, list, or map. + await dataConnect + .updateTaxForm( + id: form.id, + ) + .formData(AnyValue.fromJson(data)) + .status(dc.TaxFormStatus.SUBMITTED) + .execute(); + } + + @override + Future updateFormStatus(TaxFormType type, TaxFormStatus status) async { + final staffId = await _getStaffId(); + final result = + await dataConnect.getTaxFormsBystaffId(staffId: staffId).execute(); + final targetTypeString = _mapTypeToGenerated(type).name; + + final form = result.data.taxForms.firstWhere( + (e) => e.formType.stringValue == targetTypeString, + orElse: () => throw Exception('Form not found for update'), + ); + + await dataConnect + .updateTaxForm( + id: form.id, + ) + .status(_mapStatusToGenerated(status)) + .execute(); + } + + TaxFormEntity _mapToEntity(dc.GetTaxFormsBystaffIdTaxForms form) { + return TaxFormEntity( + type: _mapTypeFromString(form.formType.stringValue), + title: form.title, + subtitle: form.subtitle ?? '', + description: form.description ?? '', + status: _mapStatusFromString(form.status.stringValue), + lastUpdated: form.updatedAt?.toDateTime(), + ); + } + + TaxFormType _mapTypeFromString(String value) { + switch (value) { + case 'I9': + return TaxFormType.i9; + case 'W4': + return TaxFormType.w4; + default: + // Handle unexpected types gracefully if needed, or throw. + // Assuming database integrity for now. + if (value == 'I-9') return TaxFormType.i9; // Fallback just in case + throw Exception('Unknown TaxFormType string: $value'); + } + } + + TaxFormStatus _mapStatusFromString(String value) { + switch (value) { + case 'NOT_STARTED': + return TaxFormStatus.notStarted; + case 'DRAFT': + return TaxFormStatus.inProgress; + case 'SUBMITTED': + return TaxFormStatus.submitted; + case 'APPROVED': + return TaxFormStatus.approved; + case 'REJECTED': + return TaxFormStatus.rejected; + default: + return TaxFormStatus.notStarted; // Default fallback + } + } + + dc.TaxFormType _mapTypeToGenerated(TaxFormType type) { + switch (type) { + case TaxFormType.i9: + return dc.TaxFormType.I9; + case TaxFormType.w4: + return dc.TaxFormType.W4; + } + } + + dc.TaxFormStatus _mapStatusToGenerated(TaxFormStatus status) { + switch (status) { + case TaxFormStatus.notStarted: + return dc.TaxFormStatus.NOT_STARTED; + case TaxFormStatus.inProgress: + return dc.TaxFormStatus.DRAFT; + case TaxFormStatus.submitted: + return dc.TaxFormStatus.SUBMITTED; + case TaxFormStatus.approved: + return dc.TaxFormStatus.APPROVED; + case TaxFormStatus.rejected: + return dc.TaxFormStatus.REJECTED; + } + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/domain/usecases/get_tax_forms_usecase.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/domain/usecases/get_tax_forms_usecase.dart new file mode 100644 index 00000000..f8f95b15 --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/domain/usecases/get_tax_forms_usecase.dart @@ -0,0 +1,12 @@ +import '../entities/tax_form_entity.dart'; +import '../repositories/tax_forms_repository.dart'; + +class GetTaxFormsUseCase { + final TaxFormsRepository _repository; + + GetTaxFormsUseCase(this._repository); + + Future> call() async { + return _repository.getTaxForms(); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/domain/usecases/submit_tax_form_usecase.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/domain/usecases/submit_tax_form_usecase.dart new file mode 100644 index 00000000..431a99bc --- /dev/null +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/domain/usecases/submit_tax_form_usecase.dart @@ -0,0 +1,12 @@ +import '../entities/tax_form_entity.dart'; +import '../repositories/tax_forms_repository.dart'; + +class SubmitTaxFormUseCase { + final TaxFormsRepository _repository; + + SubmitTaxFormUseCase(this._repository); + + Future call(TaxFormType type, Map data) async { + return _repository.submitForm(type, data); + } +} diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/blocs/i9/form_i9_cubit.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/blocs/i9/form_i9_cubit.dart index f44bbe87..ac6f132e 100644 --- a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/blocs/i9/form_i9_cubit.dart +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/blocs/i9/form_i9_cubit.dart @@ -1,12 +1,12 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../domain/entities/tax_form_entity.dart'; -import '../../../domain/repositories/tax_forms_repository.dart'; +import '../../../domain/usecases/submit_tax_form_usecase.dart'; import 'form_i9_state.dart'; class FormI9Cubit extends Cubit { - final TaxFormsRepository _repository; + final SubmitTaxFormUseCase _submitTaxFormUseCase; - FormI9Cubit(this._repository) : super(const FormI9State()); + FormI9Cubit(this._submitTaxFormUseCase) : super(const FormI9State()); void nextStep(int totalSteps) { if (state.currentStep < totalSteps - 1) { @@ -20,45 +20,47 @@ class FormI9Cubit extends Cubit { } } - void updateCitizenship({required bool isUsCitizen}) { - emit(state.copyWith( - isUsCitizen: isUsCitizen, - isLawfulResident: false, - )); - } + // Personal Info + void firstNameChanged(String value) => emit(state.copyWith(firstName: value)); + void lastNameChanged(String value) => emit(state.copyWith(lastName: value)); + void middleInitialChanged(String value) => emit(state.copyWith(middleInitial: value)); + void otherLastNamesChanged(String value) => emit(state.copyWith(otherLastNames: value)); + void dobChanged(String value) => emit(state.copyWith(dob: value)); + void ssnChanged(String value) => emit(state.copyWith(ssn: value)); + void emailChanged(String value) => emit(state.copyWith(email: value)); + void phoneChanged(String value) => emit(state.copyWith(phone: value)); - void updateLawfulResident({required bool isLawfulResident}) { - emit(state.copyWith( - isLawfulResident: isLawfulResident, - isUsCitizen: false, - )); - } + // Address + void addressChanged(String value) => emit(state.copyWith(address: value)); + void aptNumberChanged(String value) => emit(state.copyWith(aptNumber: value)); + void cityChanged(String value) => emit(state.copyWith(city: value)); + void stateChanged(String value) => emit(state.copyWith(state: value)); + void zipCodeChanged(String value) => emit(state.copyWith(zipCode: value)); - void updateNonCitizen() { - emit(state.copyWith( - isUsCitizen: false, - isLawfulResident: false, - )); - } + // Citizenship + void citizenshipStatusChanged(String value) => emit(state.copyWith(citizenshipStatus: value)); + void uscisNumberChanged(String value) => emit(state.copyWith(uscisNumber: value)); + void admissionNumberChanged(String value) => emit(state.copyWith(admissionNumber: value)); + void passportNumberChanged(String value) => emit(state.copyWith(passportNumber: value)); + void countryIssuanceChanged(String value) => emit(state.copyWith(countryIssuance: value)); - void ssnChanged(String value) { - emit(state.copyWith(ssn: value)); - } - - void alienNumberChanged(String value) { - emit(state.copyWith(alienNumber: value)); - } + // Signature + void preparerUsedChanged(bool value) => emit(state.copyWith(preparerUsed: value)); + void signatureChanged(String value) => emit(state.copyWith(signature: value)); Future submit() async { emit(state.copyWith(status: FormI9Status.submitting)); try { - await _repository.submitForm( + await _submitTaxFormUseCase( TaxFormType.i9, - { - 'isUsCitizen': state.isUsCitizen, - 'isLawfulResident': state.isLawfulResident, + { + 'firstName': state.firstName, + 'lastName': state.lastName, + 'middleInitial': state.middleInitial, + 'citizenshipStatus': state.citizenshipStatus, 'ssn': state.ssn, - 'alienNumber': state.alienNumber, + 'signature': state.signature, + // ... add other fields as needed for backend }, ); emit(state.copyWith(status: FormI9Status.success)); diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/blocs/i9/form_i9_state.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/blocs/i9/form_i9_state.dart index f1d5b5ed..9fd739aa 100644 --- a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/blocs/i9/form_i9_state.dart +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/blocs/i9/form_i9_state.dart @@ -4,38 +4,110 @@ enum FormI9Status { initial, submitting, success, failure } class FormI9State extends Equatable { final int currentStep; - final bool isUsCitizen; - final bool isLawfulResident; + // Personal Info + final String firstName; + final String lastName; + final String middleInitial; + final String otherLastNames; + final String dob; final String ssn; - final String alienNumber; + final String email; + final String phone; + + // Address + final String address; + final String aptNumber; + final String city; + final String state; + final String zipCode; + + // Citizenship + final String citizenshipStatus; // citizen, noncitizen_national, permanent_resident, alien_authorized + final String uscisNumber; + final String admissionNumber; + final String passportNumber; + final String countryIssuance; + + // Signature + final bool preparerUsed; + final String signature; + final FormI9Status status; final String? errorMessage; const FormI9State({ this.currentStep = 0, - this.isUsCitizen = false, - this.isLawfulResident = false, + this.firstName = '', + this.lastName = '', + this.middleInitial = '', + this.otherLastNames = '', + this.dob = '', this.ssn = '', - this.alienNumber = '', + this.email = '', + this.phone = '', + this.address = '', + this.aptNumber = '', + this.city = '', + this.state = '', + this.zipCode = '', + this.citizenshipStatus = '', + this.uscisNumber = '', + this.admissionNumber = '', + this.passportNumber = '', + this.countryIssuance = '', + this.preparerUsed = false, + this.signature = '', this.status = FormI9Status.initial, this.errorMessage, }); FormI9State copyWith({ int? currentStep, - bool? isUsCitizen, - bool? isLawfulResident, + String? firstName, + String? lastName, + String? middleInitial, + String? otherLastNames, + String? dob, String? ssn, - String? alienNumber, + String? email, + String? phone, + String? address, + String? aptNumber, + String? city, + String? state, + String? zipCode, + String? citizenshipStatus, + String? uscisNumber, + String? admissionNumber, + String? passportNumber, + String? countryIssuance, + bool? preparerUsed, + String? signature, FormI9Status? status, String? errorMessage, }) { return FormI9State( currentStep: currentStep ?? this.currentStep, - isUsCitizen: isUsCitizen ?? this.isUsCitizen, - isLawfulResident: isLawfulResident ?? this.isLawfulResident, + firstName: firstName ?? this.firstName, + lastName: lastName ?? this.lastName, + middleInitial: middleInitial ?? this.middleInitial, + otherLastNames: otherLastNames ?? this.otherLastNames, + dob: dob ?? this.dob, ssn: ssn ?? this.ssn, - alienNumber: alienNumber ?? this.alienNumber, + email: email ?? this.email, + phone: phone ?? this.phone, + address: address ?? this.address, + aptNumber: aptNumber ?? this.aptNumber, + city: city ?? this.city, + state: state ?? this.state, + zipCode: zipCode ?? this.zipCode, + citizenshipStatus: citizenshipStatus ?? this.citizenshipStatus, + uscisNumber: uscisNumber ?? this.uscisNumber, + admissionNumber: admissionNumber ?? this.admissionNumber, + passportNumber: passportNumber ?? this.passportNumber, + countryIssuance: countryIssuance ?? this.countryIssuance, + preparerUsed: preparerUsed ?? this.preparerUsed, + signature: signature ?? this.signature, status: status ?? this.status, errorMessage: errorMessage ?? this.errorMessage, ); @@ -44,10 +116,26 @@ class FormI9State extends Equatable { @override List get props => [ currentStep, - isUsCitizen, - isLawfulResident, + firstName, + lastName, + middleInitial, + otherLastNames, + dob, ssn, - alienNumber, + email, + phone, + address, + aptNumber, + city, + state, + zipCode, + citizenshipStatus, + uscisNumber, + admissionNumber, + passportNumber, + countryIssuance, + preparerUsed, + signature, status, errorMessage, ]; diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/blocs/tax_forms/tax_forms_cubit.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/blocs/tax_forms/tax_forms_cubit.dart index aa7b5711..de06dd28 100644 --- a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/blocs/tax_forms/tax_forms_cubit.dart +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/blocs/tax_forms/tax_forms_cubit.dart @@ -1,17 +1,17 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../domain/entities/tax_form_entity.dart'; -import '../../../domain/repositories/tax_forms_repository.dart'; +import '../../../domain/usecases/get_tax_forms_usecase.dart'; import 'tax_forms_state.dart'; class TaxFormsCubit extends Cubit { - final TaxFormsRepository _repository; + final GetTaxFormsUseCase _getTaxFormsUseCase; - TaxFormsCubit(this._repository) : super(const TaxFormsState()); + TaxFormsCubit(this._getTaxFormsUseCase) : super(const TaxFormsState()); Future loadTaxForms() async { emit(state.copyWith(status: TaxFormsStatus.loading)); try { - final List forms = await _repository.getTaxForms(); + final List forms = await _getTaxFormsUseCase(); emit(state.copyWith( status: TaxFormsStatus.success, forms: forms, diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/blocs/w4/form_w4_cubit.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/blocs/w4/form_w4_cubit.dart index 68650ad7..47088736 100644 --- a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/blocs/w4/form_w4_cubit.dart +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/blocs/w4/form_w4_cubit.dart @@ -1,16 +1,18 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../domain/entities/tax_form_entity.dart'; -import '../../../domain/repositories/tax_forms_repository.dart'; +import '../../../domain/usecases/submit_tax_form_usecase.dart'; import 'form_w4_state.dart'; class FormW4Cubit extends Cubit { - final TaxFormsRepository _repository; + final SubmitTaxFormUseCase _submitTaxFormUseCase; - FormW4Cubit(this._repository) : super(const FormW4State()); + FormW4Cubit(this._submitTaxFormUseCase) : super(const FormW4State()); void nextStep(int totalSteps) { if (state.currentStep < totalSteps - 1) { emit(state.copyWith(currentStep: state.currentStep + 1)); + } else { + submit(); } } @@ -20,42 +22,38 @@ class FormW4Cubit extends Cubit { } } - void filingStatusChanged(String status) { - emit(state.copyWith(filingStatus: status)); - } + // Personal Info + void firstNameChanged(String value) => emit(state.copyWith(firstName: value)); + void lastNameChanged(String value) => emit(state.copyWith(lastName: value)); + void ssnChanged(String value) => emit(state.copyWith(ssn: value)); + void addressChanged(String value) => emit(state.copyWith(address: value)); + void cityStateZipChanged(String value) => emit(state.copyWith(cityStateZip: value)); - void multipleJobsChanged(bool value) { - emit(state.copyWith(multipleJobs: value)); - } - - void dependentsAmountChanged(String value) { - emit(state.copyWith(dependentsAmount: value)); - } - - void otherIncomeChanged(String value) { - emit(state.copyWith(otherIncome: value)); - } - - void deductionsChanged(String value) { - emit(state.copyWith(deductions: value)); - } - - void extraWithholdingChanged(String value) { - emit(state.copyWith(extraWithholding: value)); - } + // Form Data + void filingStatusChanged(String value) => emit(state.copyWith(filingStatus: value)); + void multipleJobsChanged(bool value) => emit(state.copyWith(multipleJobs: value)); + void qualifyingChildrenChanged(int value) => emit(state.copyWith(qualifyingChildren: value)); + void otherDependentsChanged(int value) => emit(state.copyWith(otherDependents: value)); + + // Adjustments + void otherIncomeChanged(String value) => emit(state.copyWith(otherIncome: value)); + void deductionsChanged(String value) => emit(state.copyWith(deductions: value)); + void extraWithholdingChanged(String value) => emit(state.copyWith(extraWithholding: value)); + void signatureChanged(String value) => emit(state.copyWith(signature: value)); Future submit() async { emit(state.copyWith(status: FormW4Status.submitting)); try { - await _repository.submitForm( + await _submitTaxFormUseCase( TaxFormType.w4, - { + { + 'firstName': state.firstName, + 'lastName': state.lastName, + 'ssn': state.ssn, 'filingStatus': state.filingStatus, 'multipleJobs': state.multipleJobs, - 'dependentsAmount': state.dependentsAmount, - 'otherIncome': state.otherIncome, - 'deductions': state.deductions, - 'extraWithholding': state.extraWithholding, + 'signature': state.signature, + // ... add other fields as needed }, ); emit(state.copyWith(status: FormW4Status.success)); diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/blocs/w4/form_w4_state.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/blocs/w4/form_w4_state.dart index 5d43b2f4..6c819d7d 100644 --- a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/blocs/w4/form_w4_state.dart +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/blocs/w4/form_w4_state.dart @@ -4,60 +4,106 @@ enum FormW4Status { initial, submitting, success, failure } class FormW4State extends Equatable { final int currentStep; + + // Personal Info + final String firstName; + final String lastName; + final String ssn; + final String address; + final String cityStateZip; + + // Form Data final String filingStatus; final bool multipleJobs; - final String dependentsAmount; + + // Dependents + final int qualifyingChildren; + final int otherDependents; + + // Adjustments final String otherIncome; final String deductions; final String extraWithholding; + + final String signature; final FormW4Status status; final String? errorMessage; const FormW4State({ this.currentStep = 0, - this.filingStatus = 'Single', + this.firstName = '', + this.lastName = '', + this.ssn = '', + this.address = '', + this.cityStateZip = '', + this.filingStatus = '', this.multipleJobs = false, - this.dependentsAmount = '', + this.qualifyingChildren = 0, + this.otherDependents = 0, this.otherIncome = '', this.deductions = '', this.extraWithholding = '', + this.signature = '', this.status = FormW4Status.initial, this.errorMessage, }); FormW4State copyWith({ int? currentStep, + String? firstName, + String? lastName, + String? ssn, + String? address, + String? cityStateZip, String? filingStatus, bool? multipleJobs, - String? dependentsAmount, + int? qualifyingChildren, + int? otherDependents, String? otherIncome, String? deductions, String? extraWithholding, + String? signature, FormW4Status? status, String? errorMessage, }) { return FormW4State( currentStep: currentStep ?? this.currentStep, + firstName: firstName ?? this.firstName, + lastName: lastName ?? this.lastName, + ssn: ssn ?? this.ssn, + address: address ?? this.address, + cityStateZip: cityStateZip ?? this.cityStateZip, filingStatus: filingStatus ?? this.filingStatus, multipleJobs: multipleJobs ?? this.multipleJobs, - dependentsAmount: dependentsAmount ?? this.dependentsAmount, + qualifyingChildren: qualifyingChildren ?? this.qualifyingChildren, + otherDependents: otherDependents ?? this.otherDependents, otherIncome: otherIncome ?? this.otherIncome, deductions: deductions ?? this.deductions, extraWithholding: extraWithholding ?? this.extraWithholding, + signature: signature ?? this.signature, status: status ?? this.status, errorMessage: errorMessage ?? this.errorMessage, ); } + int get totalCredits => (qualifyingChildren * 2000) + (otherDependents * 500); + @override List get props => [ currentStep, + firstName, + lastName, + ssn, + address, + cityStateZip, filingStatus, multipleJobs, - dependentsAmount, + qualifyingChildren, + otherDependents, otherIncome, deductions, extraWithholding, + signature, status, errorMessage, ]; diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/pages/form_i9_page.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/pages/form_i9_page.dart index 1439c73d..b62dd855 100644 --- a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/pages/form_i9_page.dart +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/pages/form_i9_page.dart @@ -1,6 +1,10 @@ import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_modular/flutter_modular.dart'; +import 'package:flutter_modular/flutter_modular.dart' hide ModularWatchExtension; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../blocs/i9/form_i9_cubit.dart'; +import '../blocs/i9/form_i9_state.dart'; class FormI9Page extends StatefulWidget { const FormI9Page({super.key}); @@ -10,34 +14,6 @@ class FormI9Page extends StatefulWidget { } class _FormI9PageState extends State { - int _currentStep = 0; - bool _isSubmitting = false; - bool _isSuccess = false; - - final Map _formData = { - 'firstName': '', - 'lastName': '', - 'middleInitial': '', - 'otherLastNames': '', - 'address': '', - 'aptNumber': '', - 'city': '', - 'state': '', - 'zipCode': '', - 'dob': '', - 'ssn': '', - 'email': '', - 'phone': '', - 'citizenshipStatus': '', - 'uscisNumber': '', - 'admissionNumber': '', - 'passportNumber': '', - 'countryIssuance': '', - }; - - String _signature = ''; - bool _preparerUsed = false; - final List _usStates = [ 'AL', 'AK', 'AZ', 'AR', 'CA', 'CO', 'CT', 'DE', 'FL', 'GA', 'HI', 'ID', 'IL', 'IN', 'IA', 'KS', 'KY', 'LA', 'ME', 'MD', @@ -53,76 +29,74 @@ class _FormI9PageState extends State { {'title': 'Review & Sign', 'subtitle': 'Confirm your information'}, ]; - void _updateField(String key, dynamic value) { - setState(() { - _formData[key] = value; - }); - } - - bool _canProceed() { - switch (_currentStep) { + bool _canProceed(FormI9State state) { + switch (state.currentStep) { case 0: - return (_formData['firstName'] as String).trim().isNotEmpty && - (_formData['lastName'] as String).trim().isNotEmpty && - (_formData['dob'] as String).isNotEmpty && - (_formData['ssn'] as String).replaceAll(RegExp(r'\D'), '').length >= 9; + return state.firstName.trim().isNotEmpty && + state.lastName.trim().isNotEmpty && + state.dob.isNotEmpty && + state.ssn.replaceAll(RegExp(r'\D'), '').length >= 9; case 1: - return (_formData['address'] as String).trim().isNotEmpty && - (_formData['city'] as String).trim().isNotEmpty && - (_formData['state'] as String).isNotEmpty && - (_formData['zipCode'] as String).isNotEmpty; + return state.address.trim().isNotEmpty && + state.city.trim().isNotEmpty && + state.state.isNotEmpty && + state.zipCode.isNotEmpty; case 2: - return (_formData['citizenshipStatus'] as String).isNotEmpty; + return state.citizenshipStatus.isNotEmpty; case 3: - return _signature.trim().isNotEmpty; + return state.signature.trim().isNotEmpty; default: return true; } } - void _handleNext() { - if (_currentStep < _steps.length - 1) { - setState(() => _currentStep++); + void _handleNext(BuildContext context, int currentStep) { + if (currentStep < _steps.length - 1) { + context.read().nextStep(_steps.length); } else { - _submitForm(); + context.read().submit(); } } - void _handleBack() { - if (_currentStep > 0) { - setState(() => _currentStep--); - } - } - - Future _submitForm() async { - setState(() => _isSubmitting = true); - // Mock API call - await Future.delayed(const Duration(seconds: 2)); - if (mounted) { - setState(() { - _isSubmitting = false; - _isSuccess = true; - }); - } + void _handleBack(BuildContext context) { + context.read().previousStep(); } @override Widget build(BuildContext context) { - if (_isSuccess) return _buildSuccessView(); + return BlocProvider.value( + value: Modular.get(), + child: BlocConsumer( + listener: (BuildContext context, FormI9State state) { + if (state.status == FormI9Status.success) { + // Success view is handled by state check in build or we can navigate + } else if (state.status == FormI9Status.failure) { + final ScaffoldMessengerState messenger = ScaffoldMessenger.of(context); + messenger.hideCurrentSnackBar(); + messenger.showSnackBar( + SnackBar(content: Text(state.errorMessage ?? 'An error occurred')), + ); + } + }, + builder: (BuildContext context, FormI9State state) { + if (state.status == FormI9Status.success) return _buildSuccessView(); - return Scaffold( - backgroundColor: UiColors.background, - body: Column( - children: [ - _buildHeader(), - Expanded( - child: SingleChildScrollView( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24), - child: _buildCurrentStep(), + return Scaffold( + backgroundColor: UiColors.background, + body: Column( + children: [ + _buildHeader(context, state), + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24), + child: _buildCurrentStep(context, state), + ), + ), + _buildFooter(context, state), + ], ), - ), - _buildFooter(), - ], + ); + }, ), ); } @@ -194,7 +168,7 @@ class _FormI9PageState extends State { ); } - Widget _buildHeader() { + Widget _buildHeader(BuildContext context, FormI9State state) { return Container( color: UiColors.primary, padding: const EdgeInsets.only(top: 60, bottom: 24, left: 20, right: 20), @@ -241,7 +215,7 @@ class _FormI9PageState extends State { child: Container( height: 4, decoration: BoxDecoration( - color: idx <= _currentStep + color: idx <= state.currentStep ? UiColors.bgPopup : UiColors.bgPopup.withOpacity(0.3), borderRadius: BorderRadius.circular(2), @@ -259,12 +233,12 @@ class _FormI9PageState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - 'Step ${_currentStep + 1} of ${_steps.length}', + 'Step ${state.currentStep + 1} of ${_steps.length}', style: UiTypography.body3r.copyWith(color: UiColors.bgPopup.withOpacity(0.7)), ), Expanded( child: Text( - _steps[_currentStep]['title']!, + _steps[state.currentStep]['title']!, textAlign: TextAlign.end, style: UiTypography.body3m.copyWith( color: UiColors.bgPopup, @@ -279,27 +253,27 @@ class _FormI9PageState extends State { ); } - Widget _buildCurrentStep() { - switch (_currentStep) { + Widget _buildCurrentStep(BuildContext context, FormI9State state) { + switch (state.currentStep) { case 0: - return _buildStep1(); + return _buildStep1(context, state); case 1: - return _buildStep2(); + return _buildStep2(context, state); case 2: - return _buildStep3(); + return _buildStep3(context, state); case 3: - return _buildStep4(); + return _buildStep4(context, state); default: return Container(); } } Widget _buildTextField( - String label, - String key, { + String label, { + required String value, + required ValueChanged onChanged, TextInputType? keyboardType, String? placeholder, - Function(String)? onChanged, }) { return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -313,11 +287,11 @@ class _FormI9PageState extends State { ), const SizedBox(height: 6), TextField( - controller: TextEditingController(text: _formData[key].toString()) + controller: TextEditingController(text: value) ..selection = TextSelection.fromPosition( - TextPosition(offset: (_formData[key].toString()).length), + TextPosition(offset: value.length), ), - onChanged: onChanged ?? (String val) => _updateField(key, val), + onChanged: onChanged, keyboardType: keyboardType, style: UiTypography.body2r.copyWith(color: UiColors.textPrimary), decoration: InputDecoration( @@ -347,7 +321,7 @@ class _FormI9PageState extends State { ); } - Widget _buildStep1() { + Widget _buildStep1(BuildContext context, FormI9State state) { return Column( children: [ Row( @@ -355,7 +329,8 @@ class _FormI9PageState extends State { Expanded( child: _buildTextField( 'First Name *', - 'firstName', + value: state.firstName, + onChanged: (String val) => context.read().firstNameChanged(val), placeholder: 'John', ), ), @@ -363,7 +338,8 @@ class _FormI9PageState extends State { Expanded( child: _buildTextField( 'Last Name *', - 'lastName', + value: state.lastName, + onChanged: (String val) => context.read().lastNameChanged(val), placeholder: 'Smith', ), ), @@ -375,7 +351,8 @@ class _FormI9PageState extends State { Expanded( child: _buildTextField( 'Middle Initial', - 'middleInitial', + value: state.middleInitial, + onChanged: (String val) => context.read().middleInitialChanged(val), placeholder: 'A', ), ), @@ -384,7 +361,8 @@ class _FormI9PageState extends State { flex: 2, child: _buildTextField( 'Other Last Names', - 'otherLastNames', + value: state.otherLastNames, + onChanged: (String val) => context.read().otherLastNamesChanged(val), placeholder: 'Maiden name (if any)', ), ), @@ -393,33 +371,36 @@ class _FormI9PageState extends State { const SizedBox(height: 16), _buildTextField( 'Date of Birth *', - 'dob', + value: state.dob, + onChanged: (String val) => context.read().dobChanged(val), placeholder: 'MM/DD/YYYY', keyboardType: TextInputType.datetime, ), const SizedBox(height: 16), _buildTextField( 'Social Security Number *', - 'ssn', + value: state.ssn, placeholder: 'XXX-XX-XXXX', keyboardType: TextInputType.number, onChanged: (String val) { String text = val.replaceAll(RegExp(r'\D'), ''); if (text.length > 9) text = text.substring(0, 9); - _updateField('ssn', text); + context.read().ssnChanged(text); }, ), const SizedBox(height: 16), _buildTextField( 'Email Address', - 'email', + value: state.email, + onChanged: (String val) => context.read().emailChanged(val), keyboardType: TextInputType.emailAddress, placeholder: 'john.smith@example.com', ), const SizedBox(height: 16), _buildTextField( 'Phone Number', - 'phone', + value: state.phone, + onChanged: (String val) => context.read().phoneChanged(val), keyboardType: TextInputType.phone, placeholder: '(555) 555-5555', ), @@ -427,18 +408,20 @@ class _FormI9PageState extends State { ); } - Widget _buildStep2() { + Widget _buildStep2(BuildContext context, FormI9State state) { return Column( children: [ _buildTextField( 'Address (Street Number and Name) *', - 'address', + value: state.address, + onChanged: (String val) => context.read().addressChanged(val), placeholder: '123 Main Street', ), const SizedBox(height: 16), _buildTextField( 'Apt. Number', - 'aptNumber', + value: state.aptNumber, + onChanged: (String val) => context.read().aptNumberChanged(val), placeholder: '4B', ), const SizedBox(height: 16), @@ -448,7 +431,8 @@ class _FormI9PageState extends State { flex: 2, child: _buildTextField( 'City or Town *', - 'city', + value: state.city, + onChanged: (String val) => context.read().cityChanged(val), placeholder: 'San Francisco', ), ), @@ -466,12 +450,12 @@ class _FormI9PageState extends State { ), const SizedBox(height: 6), DropdownButtonFormField( - value: _formData['state'].toString().isEmpty ? null : _formData['state'].toString(), - onChanged: (String? val) => _updateField('state', val), - items: _usStates.map((String state) { + value: state.state.isEmpty ? null : state.state, + onChanged: (String? val) => context.read().stateChanged(val ?? ''), + items: _usStates.map((String stateAbbr) { return DropdownMenuItem( - value: state, - child: Text(state), + value: stateAbbr, + child: Text(stateAbbr), ); }).toList(), decoration: InputDecoration( @@ -496,7 +480,8 @@ class _FormI9PageState extends State { const SizedBox(height: 16), _buildTextField( 'ZIP Code *', - 'zipCode', + value: state.zipCode, + onChanged: (String val) => context.read().zipCodeChanged(val), placeholder: '94103', keyboardType: TextInputType.number, ), @@ -504,7 +489,7 @@ class _FormI9PageState extends State { ); } - Widget _buildStep3() { + Widget _buildStep3(BuildContext context, FormI9State state) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -514,24 +499,31 @@ class _FormI9PageState extends State { ), const SizedBox(height: 24), _buildRadioOption( + context, + state, 'citizen', '1. A citizen of the United States', ), const SizedBox(height: 12), _buildRadioOption( + context, + state, 'noncitizen_national', '2. A noncitizen national of the United States', ), const SizedBox(height: 12), _buildRadioOption( + context, + state, 'permanent_resident', '3. A lawful permanent resident', - child: _formData['citizenshipStatus'] == 'permanent_resident' + child: state.citizenshipStatus == 'permanent_resident' ? Padding( padding: const EdgeInsets.only(top: 12), child: _buildTextField( 'USCIS Number', - 'uscisNumber', + value: state.uscisNumber, + onChanged: (String val) => context.read().uscisNumberChanged(val), placeholder: 'A-123456789', ), ) @@ -539,26 +531,31 @@ class _FormI9PageState extends State { ), const SizedBox(height: 12), _buildRadioOption( + context, + state, 'alien_authorized', '4. An alien authorized to work', - child: _formData['citizenshipStatus'] == 'alien_authorized' + child: state.citizenshipStatus == 'alien_authorized' ? Padding( padding: const EdgeInsets.only(top: 12), child: Column( children: [ _buildTextField( 'USCIS/Admission Number', - 'admissionNumber', + value: state.admissionNumber, + onChanged: (String val) => context.read().admissionNumberChanged(val), ), const SizedBox(height: 12), _buildTextField( 'Foreign Passport Number', - 'passportNumber', + value: state.passportNumber, + onChanged: (String val) => context.read().passportNumberChanged(val), ), const SizedBox(height: 12), _buildTextField( 'Country of Issuance', - 'countryIssuance', + value: state.countryIssuance, + onChanged: (String val) => context.read().countryIssuanceChanged(val), ), ], ), @@ -569,10 +566,10 @@ class _FormI9PageState extends State { ); } - Widget _buildRadioOption(String value, String label, {Widget? child}) { - final bool isSelected = _formData['citizenshipStatus'] == value; + Widget _buildRadioOption(BuildContext context, FormI9State state, String value, String label, {Widget? child}) { + final bool isSelected = state.citizenshipStatus == value; return GestureDetector( - onTap: () => _updateField('citizenshipStatus', value), + onTap: () => context.read().citizenshipStatusChanged(value), child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( @@ -616,7 +613,7 @@ class _FormI9PageState extends State { ); } - Widget _buildStep4() { + Widget _buildStep4(BuildContext context, FormI9State state) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -635,20 +632,18 @@ class _FormI9PageState extends State { style: UiTypography.headline4m.copyWith(fontSize: 14), ), const SizedBox(height: 12), - _buildSummaryRow('Name', '${_formData['firstName']} ${_formData['lastName']}'), - _buildSummaryRow('Address', '${_formData['address']}, ${_formData['city']}'), - _buildSummaryRow('SSN', '***-**-${(_formData['ssn'] as String).length >= 4 ? (_formData['ssn'] as String).substring((_formData['ssn'] as String).length - 4) : '****'}'), - _buildSummaryRow('Citizenship', _getReadableCitizenship(_formData['citizenshipStatus'] as String)), + _buildSummaryRow('Name', '${state.firstName} ${state.lastName}'), + _buildSummaryRow('Address', '${state.address}, ${state.city}'), + _buildSummaryRow('SSN', '***-**-${state.ssn.length >= 4 ? state.ssn.substring(state.ssn.length - 4) : '****'}'), + _buildSummaryRow('Citizenship', _getReadableCitizenship(state.citizenshipStatus)), ], ), ), const SizedBox(height: 24), CheckboxListTile( - value: _preparerUsed, + value: state.preparerUsed, onChanged: (bool? val) { - setState(() { - _preparerUsed = val ?? false; - }); + context.read().preparerUsedChanged(val ?? false); }, contentPadding: EdgeInsets.zero, title: Text( @@ -679,7 +674,11 @@ class _FormI9PageState extends State { ), const SizedBox(height: 6), TextField( - onChanged: (String val) => setState(() => _signature = val), + controller: TextEditingController(text: state.signature) + ..selection = TextSelection.fromPosition( + TextPosition(offset: state.signature.length), + ), + onChanged: (String val) => context.read().signatureChanged(val), decoration: InputDecoration( hintText: 'Type your full name', filled: true, @@ -767,7 +766,7 @@ class _FormI9PageState extends State { } } - Widget _buildFooter() { + Widget _buildFooter(BuildContext context, FormI9State state) { return Container( padding: const EdgeInsets.all(16), decoration: const BoxDecoration( @@ -777,12 +776,12 @@ class _FormI9PageState extends State { child: SafeArea( child: Row( children: [ - if (_currentStep > 0) + if (state.currentStep > 0) Expanded( child: Padding( padding: const EdgeInsets.only(right: 12), child: OutlinedButton( - onPressed: _handleBack, + onPressed: () => _handleBack(context), style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 16), side: const BorderSide(color: UiColors.border), @@ -807,8 +806,8 @@ class _FormI9PageState extends State { Expanded( flex: 2, child: ElevatedButton( - onPressed: (_canProceed() && !_isSubmitting) - ? _handleNext + onPressed: (_canProceed(state) && state.status != FormI9Status.submitting) + ? () => _handleNext(context, state.currentStep) : null, style: ElevatedButton.styleFrom( backgroundColor: UiColors.primary, @@ -820,7 +819,7 @@ class _FormI9PageState extends State { ), elevation: 0, ), - child: _isSubmitting + child: state.status == FormI9Status.submitting ? const SizedBox( width: 20, height: 20, @@ -833,11 +832,11 @@ class _FormI9PageState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ Text( - _currentStep == _steps.length - 1 + state.currentStep == _steps.length - 1 ? 'Sign & Submit' : 'Continue', ), - if (_currentStep < _steps.length - 1) ...[ + if (state.currentStep < _steps.length - 1) ...[ const SizedBox(width: 8), const Icon(UiIcons.arrowRight, size: 16, color: UiColors.bgPopup), ], diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/pages/form_w4_page.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/pages/form_w4_page.dart index d88f9285..d7eec588 100644 --- a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/pages/form_w4_page.dart +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/pages/form_w4_page.dart @@ -1,6 +1,10 @@ import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_modular/flutter_modular.dart'; +import 'package:flutter_modular/flutter_modular.dart' hide ModularWatchExtension; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../blocs/w4/form_w4_cubit.dart'; +import '../blocs/w4/form_w4_state.dart'; class FormW4Page extends StatefulWidget { const FormW4Page({super.key}); @@ -10,27 +14,6 @@ class FormW4Page extends StatefulWidget { } class _FormW4PageState extends State { - int _currentStep = 0; - bool _isSubmitting = false; - bool _isSuccess = false; - - final Map _formData = { - 'firstName': '', - 'lastName': '', - 'address': '', - 'cityStateZip': '', - 'ssn': '', - 'filingStatus': '', - 'multipleJobs': false, - 'qualifyingChildren': 0, - 'otherDependents': 0, - 'otherIncome': '', - 'deductions': '', - 'extraWithholding': '', - }; - - String _signature = ''; - final List> _steps = >[ {'title': 'Personal Information', 'subtitle': 'Step 1'}, {'title': 'Filing Status', 'subtitle': 'Step 1c'}, @@ -40,76 +23,75 @@ class _FormW4PageState extends State { {'title': 'Review & Sign', 'subtitle': 'Step 5'}, ]; - void _updateField(String key, dynamic value) { - setState(() { - _formData[key] = value; - }); - } - - bool _canProceed() { - switch (_currentStep) { + bool _canProceed(FormW4State state) { + switch (state.currentStep) { case 0: - return (_formData['firstName'] as String).trim().isNotEmpty && - (_formData['lastName'] as String).trim().isNotEmpty && - (_formData['ssn'] as String).replaceAll(RegExp(r'\D'), '').length >= 4 && - (_formData['address'] as String).trim().isNotEmpty; + return state.firstName.trim().isNotEmpty && + state.lastName.trim().isNotEmpty && + state.ssn.replaceAll(RegExp(r'\D'), '').length >= 4 && + state.address.trim().isNotEmpty; case 1: - return (_formData['filingStatus'] as String).isNotEmpty; + return state.filingStatus.isNotEmpty; case 5: - return _signature.trim().isNotEmpty; + return state.signature.trim().isNotEmpty; default: return true; } } - void _handleNext() { - if (_currentStep < _steps.length - 1) { - setState(() => _currentStep++); + void _handleNext(BuildContext context, int currentStep) { + if (currentStep < _steps.length - 1) { + context.read().nextStep(_steps.length); } else { - _submitForm(); + context.read().submit(); } } - void _handleBack() { - if (_currentStep > 0) { - setState(() => _currentStep--); - } + void _handleBack(BuildContext context) { + context.read().previousStep(); } - Future _submitForm() async { - setState(() => _isSubmitting = true); - // Mock API call - await Future.delayed(const Duration(seconds: 2)); - if (mounted) { - setState(() { - _isSubmitting = false; - _isSuccess = true; - }); - } - } - int get _totalCredits { - return ((_formData['qualifyingChildren'] as int) * 2000) + - ((_formData['otherDependents'] as int) * 500); + int _totalCredits(FormW4State state) { + return (state.qualifyingChildren * 2000) + + (state.otherDependents * 500); } @override Widget build(BuildContext context) { - if (_isSuccess) return _buildSuccessView(); + return BlocProvider.value( + value: Modular.get(), + child: BlocConsumer( + listener: (BuildContext context, FormW4State state) { + if (state.status == FormW4Status.success) { + // Handled in builder + } else if (state.status == FormW4Status.failure) { + final ScaffoldMessengerState messenger = ScaffoldMessenger.of(context); + messenger.hideCurrentSnackBar(); + messenger.showSnackBar( + SnackBar(content: Text(state.errorMessage ?? 'An error occurred')), + ); + } + }, + builder: (BuildContext context, FormW4State state) { + if (state.status == FormW4Status.success) return _buildSuccessView(); - return Scaffold( - backgroundColor: UiColors.background, - body: Column( - children: [ - _buildHeader(), - Expanded( - child: SingleChildScrollView( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24), - child: _buildCurrentStep(), + return Scaffold( + backgroundColor: UiColors.background, + body: Column( + children: [ + _buildHeader(context, state), + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24), + child: _buildCurrentStep(context, state), + ), + ), + _buildFooter(context, state), + ], ), - ), - _buildFooter(), - ], + ); + }, ), ); } @@ -181,7 +163,7 @@ class _FormW4PageState extends State { ); } - Widget _buildHeader() { + Widget _buildHeader(BuildContext context, FormW4State state) { return Container( color: UiColors.primary, padding: const EdgeInsets.only(top: 60, bottom: 24, left: 20, right: 20), @@ -205,7 +187,6 @@ class _FormW4PageState extends State { Text( 'Form W-4', style: UiTypography.headline4m.copyWith( - color: UiColors.bgPopup, ), ), @@ -229,7 +210,7 @@ class _FormW4PageState extends State { child: Container( height: 4, decoration: BoxDecoration( - color: idx <= _currentStep + color: idx <= state.currentStep ? UiColors.bgPopup : UiColors.bgPopup.withOpacity(0.3), borderRadius: BorderRadius.circular(2), @@ -247,11 +228,11 @@ class _FormW4PageState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - 'Step ${_currentStep + 1} of ${_steps.length}', + 'Step ${state.currentStep + 1} of ${_steps.length}', style: UiTypography.body3r.copyWith(color: UiColors.bgPopup.withOpacity(0.7)), ), Text( - _steps[_currentStep]['title']!, + _steps[state.currentStep]['title']!, style: UiTypography.body3m.copyWith( color: UiColors.bgPopup, fontWeight: FontWeight.w500, @@ -264,31 +245,31 @@ class _FormW4PageState extends State { ); } - Widget _buildCurrentStep() { - switch (_currentStep) { + Widget _buildCurrentStep(BuildContext context, FormW4State state) { + switch (state.currentStep) { case 0: - return _buildStep1(); + return _buildStep1(context, state); case 1: - return _buildStep2(); + return _buildStep2(context, state); case 2: - return _buildStep3(); + return _buildStep3(context, state); case 3: - return _buildStep4(); + return _buildStep4(context, state); case 4: - return _buildStep5(); + return _buildStep5(context, state); case 5: - return _buildStep6(); + return _buildStep6(context, state); default: return Container(); } } Widget _buildTextField( - String label, - String key, { + String label, { + required String value, + required ValueChanged onChanged, TextInputType? keyboardType, String? placeholder, - Function(String)? onChanged, }) { return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -302,11 +283,11 @@ class _FormW4PageState extends State { ), const SizedBox(height: 6), TextField( - controller: TextEditingController(text: _formData[key].toString()) + controller: TextEditingController(text: value) ..selection = TextSelection.fromPosition( - TextPosition(offset: (_formData[key].toString()).length), + TextPosition(offset: value.length), ), - onChanged: onChanged ?? (String val) => _updateField(key, val), + onChanged: onChanged, keyboardType: keyboardType, style: UiTypography.body2r.copyWith(color: UiColors.textPrimary), decoration: InputDecoration( @@ -336,7 +317,7 @@ class _FormW4PageState extends State { ); } - Widget _buildStep1() { + Widget _buildStep1(BuildContext context, FormW4State state) { return Column( children: [ Row( @@ -344,7 +325,8 @@ class _FormW4PageState extends State { Expanded( child: _buildTextField( 'First Name *', - 'firstName', + value: state.firstName, + onChanged: (String val) => context.read().firstNameChanged(val), placeholder: 'John', ), ), @@ -352,7 +334,8 @@ class _FormW4PageState extends State { Expanded( child: _buildTextField( 'Last Name *', - 'lastName', + value: state.lastName, + onChanged: (String val) => context.read().lastNameChanged(val), placeholder: 'Smith', ), ), @@ -361,28 +344,34 @@ class _FormW4PageState extends State { const SizedBox(height: 16), _buildTextField( 'Social Security Number *', - 'ssn', + value: state.ssn, placeholder: 'XXX-XX-XXXX', keyboardType: TextInputType.number, onChanged: (String val) { String text = val.replaceAll(RegExp(r'\D'), ''); if (text.length > 9) text = text.substring(0, 9); - _updateField('ssn', text); + context.read().ssnChanged(text); }, ), const SizedBox(height: 16), - _buildTextField('Address *', 'address', placeholder: '123 Main Street'), + _buildTextField( + 'Address *', + value: state.address, + onChanged: (String val) => context.read().addressChanged(val), + placeholder: '123 Main Street', + ), const SizedBox(height: 16), _buildTextField( 'City, State, ZIP', - 'cityStateZip', + value: state.cityStateZip, + onChanged: (String val) => context.read().cityStateZipChanged(val), placeholder: 'San Francisco, CA 94102', ), ], ); } - Widget _buildStep2() { + Widget _buildStep2(BuildContext context, FormW4State state) { return Column( children: [ Container( @@ -406,18 +395,24 @@ class _FormW4PageState extends State { ), const SizedBox(height: 24), _buildRadioOption( + context, + state, 'single', 'Single or Married filing separately', null, ), const SizedBox(height: 12), _buildRadioOption( + context, + state, 'married', 'Married filing jointly or Qualifying surviving spouse', null, ), const SizedBox(height: 12), _buildRadioOption( + context, + state, 'head_of_household', 'Head of household', 'Check only if you\'re unmarried and pay more than half the costs of keeping up a home', @@ -426,10 +421,10 @@ class _FormW4PageState extends State { ); } - Widget _buildRadioOption(String value, String label, String? subLabel) { - final bool isSelected = _formData['filingStatus'] == value; + Widget _buildRadioOption(BuildContext context, FormW4State state, String value, String label, String? subLabel) { + final bool isSelected = state.filingStatus == value; return GestureDetector( - onTap: () => _updateField('filingStatus', value), + onTap: () => context.read().filingStatusChanged(value), child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( @@ -484,7 +479,7 @@ class _FormW4PageState extends State { ); } - Widget _buildStep3() { + Widget _buildStep3(BuildContext context, FormW4State state) { return Column( children: [ Container( @@ -527,14 +522,14 @@ class _FormW4PageState extends State { ), const SizedBox(height: 24), GestureDetector( - onTap: () => _updateField('multipleJobs', !_formData['multipleJobs']), + onTap: () => context.read().multipleJobsChanged(!state.multipleJobs), child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: UiColors.bgPopup, borderRadius: BorderRadius.circular(12), border: Border.all( - color: (_formData['multipleJobs'] as bool) + color: state.multipleJobs ? UiColors.primary : UiColors.border, ), @@ -546,17 +541,17 @@ class _FormW4PageState extends State { width: 24, height: 24, decoration: BoxDecoration( - color: (_formData['multipleJobs'] as bool) + color: state.multipleJobs ? UiColors.primary : UiColors.bgPopup, borderRadius: BorderRadius.circular(6), border: Border.all( - color: (_formData['multipleJobs'] as bool) + color: state.multipleJobs ? UiColors.primary : Colors.grey, ), ), - child: (_formData['multipleJobs'] as bool) + child: state.multipleJobs ? const Icon( UiIcons.check, color: UiColors.bgPopup, @@ -599,7 +594,7 @@ class _FormW4PageState extends State { ); } - Widget _buildStep4() { + Widget _buildStep4(BuildContext context, FormW4State state) { return Column( children: [ Container( @@ -632,23 +627,29 @@ class _FormW4PageState extends State { child: Column( children: [ _buildCounter( + context, + state, 'Qualifying children under age 17', '\$2,000 each', - 'qualifyingChildren', + (FormW4State s) => s.qualifyingChildren, + (int val) => context.read().qualifyingChildrenChanged(val), ), const Padding( padding: EdgeInsets.symmetric(vertical: 16), child: Divider(height: 1, color: UiColors.border), ), _buildCounter( + context, + state, 'Other dependents', '\$500 each', - 'otherDependents', + (FormW4State s) => s.otherDependents, + (int val) => context.read().otherDependentsChanged(val), ), ], ), ), - if (_totalCredits > 0) ...[ + if (_totalCredits(state) > 0) ...[ const SizedBox(height: 16), Container( padding: const EdgeInsets.all(16), @@ -667,7 +668,7 @@ class _FormW4PageState extends State { ), ), Text( - '\$${_totalCredits}', + '\$${_totalCredits(state)}', style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 18, @@ -682,8 +683,15 @@ class _FormW4PageState extends State { ); } - Widget _buildCounter(String label, String badge, String key) { - final int value = _formData[key] as int; + Widget _buildCounter( + BuildContext context, + FormW4State state, + String label, + String badge, + int Function(FormW4State) getValue, + Function(int) onChanged, + ) { + final int value = getValue(state); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -718,7 +726,7 @@ class _FormW4PageState extends State { children: [ _buildCircleBtn( UiIcons.minus, - () => _updateField(key, value > 0 ? value - 1 : 0), + () => onChanged(value > 0 ? value - 1 : 0), ), SizedBox( width: 48, @@ -733,7 +741,7 @@ class _FormW4PageState extends State { ), _buildCircleBtn( UiIcons.add, - () => _updateField(key, value + 1), + () => onChanged(value + 1), ), ], ), @@ -757,7 +765,7 @@ class _FormW4PageState extends State { ); } - Widget _buildStep5() { + Widget _buildStep5(BuildContext context, FormW4State state) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -768,7 +776,8 @@ class _FormW4PageState extends State { const SizedBox(height: 24), _buildTextField( '4(a) Other income (not from jobs)', - 'otherIncome', + value: state.otherIncome, + onChanged: (String val) => context.read().otherIncomeChanged(val), placeholder: '\$0', keyboardType: TextInputType.number, ), @@ -782,7 +791,8 @@ class _FormW4PageState extends State { _buildTextField( '4(b) Deductions', - 'deductions', + value: state.deductions, + onChanged: (String val) => context.read().deductionsChanged(val), placeholder: '\$0', keyboardType: TextInputType.number, ), @@ -796,14 +806,15 @@ class _FormW4PageState extends State { _buildTextField( '4(c) Extra withholding', - 'extraWithholding', + value: state.extraWithholding, + onChanged: (String val) => context.read().extraWithholdingChanged(val), placeholder: '\$0', keyboardType: TextInputType.number, ), Padding( padding: const EdgeInsets.only(top: 4, bottom: 16), child: Text( - 'Additional tax to withhold each pay period', + 'Any additional tax you want withheld each pay period', style: UiTypography.body3r.copyWith(color: UiColors.textSecondary), ), ), @@ -811,7 +822,7 @@ class _FormW4PageState extends State { ); } - Widget _buildStep6() { + Widget _buildStep6(BuildContext context, FormW4State state) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -832,20 +843,20 @@ class _FormW4PageState extends State { const SizedBox(height: 12), _buildSummaryRow( 'Name', - '${_formData['firstName']} ${_formData['lastName']}', + '${state.firstName} ${state.lastName}', ), _buildSummaryRow( 'SSN', - '***-**-${(_formData['ssn'] as String).length >= 4 ? (_formData['ssn'] as String).substring((_formData['ssn'] as String).length - 4) : '****'}', + '***-**-${state.ssn.length >= 4 ? state.ssn.substring(state.ssn.length - 4) : '****'}', ), _buildSummaryRow( 'Filing Status', - _getFilingStatusLabel(_formData['filingStatus']), + _getFilingStatusLabel(state.filingStatus), ), - if (_totalCredits > 0) + if (_totalCredits(state) > 0) _buildSummaryRow( 'Credits', - '\$${_totalCredits}', + '\$${_totalCredits(state)}', valueColor: Colors.green[700], ), ], @@ -872,7 +883,11 @@ class _FormW4PageState extends State { ), const SizedBox(height: 6), TextField( - onChanged: (String val) => setState(() => _signature = val), + controller: TextEditingController(text: state.signature) + ..selection = TextSelection.fromPosition( + TextPosition(offset: state.signature.length), + ), + onChanged: (String val) => context.read().signatureChanged(val), decoration: InputDecoration( hintText: 'Type your full name', filled: true, @@ -955,7 +970,7 @@ class _FormW4PageState extends State { } } - Widget _buildFooter() { + Widget _buildFooter(BuildContext context, FormW4State state) { return Container( padding: const EdgeInsets.all(16), decoration: const BoxDecoration( @@ -965,12 +980,12 @@ class _FormW4PageState extends State { child: SafeArea( child: Row( children: [ - if (_currentStep > 0) + if (state.currentStep > 0) Expanded( child: Padding( padding: const EdgeInsets.only(right: 12), child: OutlinedButton( - onPressed: _handleBack, + onPressed: () => _handleBack(context), style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 16), side: const BorderSide(color: UiColors.border), @@ -995,8 +1010,8 @@ class _FormW4PageState extends State { Expanded( flex: 2, child: ElevatedButton( - onPressed: (_canProceed() && !_isSubmitting) - ? _handleNext + onPressed: (_canProceed(state) && state.status != FormW4Status.submitting) + ? () => _handleNext(context, state.currentStep) : null, style: ElevatedButton.styleFrom( backgroundColor: UiColors.primary, @@ -1008,7 +1023,7 @@ class _FormW4PageState extends State { ), elevation: 0, ), - child: _isSubmitting + child: state.status == FormW4Status.submitting ? const SizedBox( width: 20, height: 20, @@ -1021,11 +1036,11 @@ class _FormW4PageState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ Text( - _currentStep == _steps.length - 1 + state.currentStep == _steps.length - 1 ? 'Submit Form' : 'Continue', ), - if (_currentStep < _steps.length - 1) ...[ + if (state.currentStep < _steps.length - 1) ...[ const SizedBox(width: 8), const Icon(UiIcons.arrowRight, size: 16, color: UiColors.bgPopup), ], diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/pages/tax_forms_page.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/pages/tax_forms_page.dart index 6964d802..c4acec93 100644 --- a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/pages/tax_forms_page.dart +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/presentation/pages/tax_forms_page.dart @@ -11,7 +11,7 @@ class TaxFormsPage extends StatelessWidget { @override Widget build(BuildContext context) { - final cubit = Modular.get(); + final TaxFormsCubit cubit = Modular.get(); if (cubit.state.status == TaxFormsStatus.initial) { cubit.loadTaxForms(); diff --git a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/staff_tax_forms_module.dart b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/staff_tax_forms_module.dart index e4a91c2a..18f67e4b 100644 --- a/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/staff_tax_forms_module.dart +++ b/apps/mobile/packages/features/staff/profile_sections/compliance/tax_forms/lib/src/staff_tax_forms_module.dart @@ -1,6 +1,10 @@ +import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter_modular/flutter_modular.dart'; -import 'data/repositories/tax_forms_repository_mock.dart'; +import 'package:krow_data_connect/krow_data_connect.dart'; +import 'data/repositories/tax_forms_repository_impl.dart'; import 'domain/repositories/tax_forms_repository.dart'; +import 'domain/usecases/get_tax_forms_usecase.dart'; +import 'domain/usecases/submit_tax_form_usecase.dart'; import 'presentation/blocs/i9/form_i9_cubit.dart'; import 'presentation/blocs/tax_forms/tax_forms_cubit.dart'; import 'presentation/blocs/w4/form_w4_cubit.dart'; @@ -11,7 +15,18 @@ import 'presentation/pages/tax_forms_page.dart'; class StaffTaxFormsModule extends Module { @override void binds(Injector i) { - i.addLazySingleton(TaxFormsRepositoryMock.new); + i.addLazySingleton( + () => TaxFormsRepositoryImpl( + firebaseAuth: FirebaseAuth.instance, + dataConnect: ExampleConnector.instance, + ), + ); + + // Use Cases + i.addLazySingleton(GetTaxFormsUseCase.new); + i.addLazySingleton(SubmitTaxFormUseCase.new); + + // Blocs i.addLazySingleton(TaxFormsCubit.new); i.addLazySingleton(FormI9Cubit.new); i.addLazySingleton(FormW4Cubit.new);