feat: Refactor W-4 form page to use BLoC pattern and implement form submission logic

- Updated FormW4Page to utilize FormW4Cubit for state management.
- Removed local state management and replaced with BLoC state.
- Implemented form validation logic based on BLoC state.
- Added methods for handling form field updates and navigation between steps.
- Integrated success and error handling for form submission.
- Refactored UI components to reflect changes in state management.

feat: Update TaxFormsPage to use TaxFormsCubit for loading tax forms

- Changed the way TaxFormsPage retrieves the TaxFormsCubit instance.
- Ensured that tax forms are loaded on initial build.

feat: Replace mock repository with implementation for tax forms

- Switched from TaxFormsRepositoryMock to TaxFormsRepositoryImpl.
- Added use cases for getting and submitting tax forms.
- Implemented the TaxFormsRepositoryImpl to handle data fetching and submission logic.

feat: Implement use cases for tax forms

- Created GetTaxFormsUseCase for retrieving tax forms.
- Created SubmitTaxFormUseCase for submitting tax form data.
- Ensured use cases interact with the repository for data operations.
This commit is contained in:
Achintha Isuru
2026-01-25 01:31:26 -05:00
parent 933e7a1543
commit dad706654b
12 changed files with 765 additions and 372 deletions

View File

@@ -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<String> _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<List<TaxFormEntity>> 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<void> _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<void> submitForm(TaxFormType type, Map<String, dynamic> 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<void> 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;
}
}
}

View File

@@ -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<List<TaxFormEntity>> call() async {
return _repository.getTaxForms();
}
}

View File

@@ -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<void> call(TaxFormType type, Map<String, dynamic> data) async {
return _repository.submitForm(type, data);
}
}

View File

@@ -1,12 +1,12 @@
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../domain/entities/tax_form_entity.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'; import 'form_i9_state.dart';
class FormI9Cubit extends Cubit<FormI9State> { class FormI9Cubit extends Cubit<FormI9State> {
final TaxFormsRepository _repository; final SubmitTaxFormUseCase _submitTaxFormUseCase;
FormI9Cubit(this._repository) : super(const FormI9State()); FormI9Cubit(this._submitTaxFormUseCase) : super(const FormI9State());
void nextStep(int totalSteps) { void nextStep(int totalSteps) {
if (state.currentStep < totalSteps - 1) { if (state.currentStep < totalSteps - 1) {
@@ -20,45 +20,47 @@ class FormI9Cubit extends Cubit<FormI9State> {
} }
} }
void updateCitizenship({required bool isUsCitizen}) { // Personal Info
emit(state.copyWith( void firstNameChanged(String value) => emit(state.copyWith(firstName: value));
isUsCitizen: isUsCitizen, void lastNameChanged(String value) => emit(state.copyWith(lastName: value));
isLawfulResident: false, 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}) { // Address
emit(state.copyWith( void addressChanged(String value) => emit(state.copyWith(address: value));
isLawfulResident: isLawfulResident, void aptNumberChanged(String value) => emit(state.copyWith(aptNumber: value));
isUsCitizen: false, 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() { // Citizenship
emit(state.copyWith( void citizenshipStatusChanged(String value) => emit(state.copyWith(citizenshipStatus: value));
isUsCitizen: false, void uscisNumberChanged(String value) => emit(state.copyWith(uscisNumber: value));
isLawfulResident: false, 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) { // Signature
emit(state.copyWith(ssn: value)); void preparerUsedChanged(bool value) => emit(state.copyWith(preparerUsed: value));
} void signatureChanged(String value) => emit(state.copyWith(signature: value));
void alienNumberChanged(String value) {
emit(state.copyWith(alienNumber: value));
}
Future<void> submit() async { Future<void> submit() async {
emit(state.copyWith(status: FormI9Status.submitting)); emit(state.copyWith(status: FormI9Status.submitting));
try { try {
await _repository.submitForm( await _submitTaxFormUseCase(
TaxFormType.i9, TaxFormType.i9,
{ <String, dynamic>{
'isUsCitizen': state.isUsCitizen, 'firstName': state.firstName,
'isLawfulResident': state.isLawfulResident, 'lastName': state.lastName,
'middleInitial': state.middleInitial,
'citizenshipStatus': state.citizenshipStatus,
'ssn': state.ssn, 'ssn': state.ssn,
'alienNumber': state.alienNumber, 'signature': state.signature,
// ... add other fields as needed for backend
}, },
); );
emit(state.copyWith(status: FormI9Status.success)); emit(state.copyWith(status: FormI9Status.success));

View File

@@ -4,38 +4,110 @@ enum FormI9Status { initial, submitting, success, failure }
class FormI9State extends Equatable { class FormI9State extends Equatable {
final int currentStep; final int currentStep;
final bool isUsCitizen; // Personal Info
final bool isLawfulResident; final String firstName;
final String lastName;
final String middleInitial;
final String otherLastNames;
final String dob;
final String ssn; 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 FormI9Status status;
final String? errorMessage; final String? errorMessage;
const FormI9State({ const FormI9State({
this.currentStep = 0, this.currentStep = 0,
this.isUsCitizen = false, this.firstName = '',
this.isLawfulResident = false, this.lastName = '',
this.middleInitial = '',
this.otherLastNames = '',
this.dob = '',
this.ssn = '', 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.status = FormI9Status.initial,
this.errorMessage, this.errorMessage,
}); });
FormI9State copyWith({ FormI9State copyWith({
int? currentStep, int? currentStep,
bool? isUsCitizen, String? firstName,
bool? isLawfulResident, String? lastName,
String? middleInitial,
String? otherLastNames,
String? dob,
String? ssn, 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, FormI9Status? status,
String? errorMessage, String? errorMessage,
}) { }) {
return FormI9State( return FormI9State(
currentStep: currentStep ?? this.currentStep, currentStep: currentStep ?? this.currentStep,
isUsCitizen: isUsCitizen ?? this.isUsCitizen, firstName: firstName ?? this.firstName,
isLawfulResident: isLawfulResident ?? this.isLawfulResident, lastName: lastName ?? this.lastName,
middleInitial: middleInitial ?? this.middleInitial,
otherLastNames: otherLastNames ?? this.otherLastNames,
dob: dob ?? this.dob,
ssn: ssn ?? this.ssn, 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, status: status ?? this.status,
errorMessage: errorMessage ?? this.errorMessage, errorMessage: errorMessage ?? this.errorMessage,
); );
@@ -44,10 +116,26 @@ class FormI9State extends Equatable {
@override @override
List<Object?> get props => [ List<Object?> get props => [
currentStep, currentStep,
isUsCitizen, firstName,
isLawfulResident, lastName,
middleInitial,
otherLastNames,
dob,
ssn, ssn,
alienNumber, email,
phone,
address,
aptNumber,
city,
state,
zipCode,
citizenshipStatus,
uscisNumber,
admissionNumber,
passportNumber,
countryIssuance,
preparerUsed,
signature,
status, status,
errorMessage, errorMessage,
]; ];

View File

@@ -1,17 +1,17 @@
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../domain/entities/tax_form_entity.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'; import 'tax_forms_state.dart';
class TaxFormsCubit extends Cubit<TaxFormsState> { class TaxFormsCubit extends Cubit<TaxFormsState> {
final TaxFormsRepository _repository; final GetTaxFormsUseCase _getTaxFormsUseCase;
TaxFormsCubit(this._repository) : super(const TaxFormsState()); TaxFormsCubit(this._getTaxFormsUseCase) : super(const TaxFormsState());
Future<void> loadTaxForms() async { Future<void> loadTaxForms() async {
emit(state.copyWith(status: TaxFormsStatus.loading)); emit(state.copyWith(status: TaxFormsStatus.loading));
try { try {
final List<TaxFormEntity> forms = await _repository.getTaxForms(); final List<TaxFormEntity> forms = await _getTaxFormsUseCase();
emit(state.copyWith( emit(state.copyWith(
status: TaxFormsStatus.success, status: TaxFormsStatus.success,
forms: forms, forms: forms,

View File

@@ -1,16 +1,18 @@
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../domain/entities/tax_form_entity.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'; import 'form_w4_state.dart';
class FormW4Cubit extends Cubit<FormW4State> { class FormW4Cubit extends Cubit<FormW4State> {
final TaxFormsRepository _repository; final SubmitTaxFormUseCase _submitTaxFormUseCase;
FormW4Cubit(this._repository) : super(const FormW4State()); FormW4Cubit(this._submitTaxFormUseCase) : super(const FormW4State());
void nextStep(int totalSteps) { void nextStep(int totalSteps) {
if (state.currentStep < totalSteps - 1) { if (state.currentStep < totalSteps - 1) {
emit(state.copyWith(currentStep: state.currentStep + 1)); emit(state.copyWith(currentStep: state.currentStep + 1));
} else {
submit();
} }
} }
@@ -20,42 +22,38 @@ class FormW4Cubit extends Cubit<FormW4State> {
} }
} }
void filingStatusChanged(String status) { // Personal Info
emit(state.copyWith(filingStatus: status)); 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) { // Form Data
emit(state.copyWith(multipleJobs: value)); 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 dependentsAmountChanged(String value) { void otherDependentsChanged(int value) => emit(state.copyWith(otherDependents: value));
emit(state.copyWith(dependentsAmount: value));
} // Adjustments
void otherIncomeChanged(String value) => emit(state.copyWith(otherIncome: value));
void otherIncomeChanged(String value) { void deductionsChanged(String value) => emit(state.copyWith(deductions: value));
emit(state.copyWith(otherIncome: value)); void extraWithholdingChanged(String value) => emit(state.copyWith(extraWithholding: value));
} void signatureChanged(String value) => emit(state.copyWith(signature: value));
void deductionsChanged(String value) {
emit(state.copyWith(deductions: value));
}
void extraWithholdingChanged(String value) {
emit(state.copyWith(extraWithholding: value));
}
Future<void> submit() async { Future<void> submit() async {
emit(state.copyWith(status: FormW4Status.submitting)); emit(state.copyWith(status: FormW4Status.submitting));
try { try {
await _repository.submitForm( await _submitTaxFormUseCase(
TaxFormType.w4, TaxFormType.w4,
{ <String, dynamic>{
'firstName': state.firstName,
'lastName': state.lastName,
'ssn': state.ssn,
'filingStatus': state.filingStatus, 'filingStatus': state.filingStatus,
'multipleJobs': state.multipleJobs, 'multipleJobs': state.multipleJobs,
'dependentsAmount': state.dependentsAmount, 'signature': state.signature,
'otherIncome': state.otherIncome, // ... add other fields as needed
'deductions': state.deductions,
'extraWithholding': state.extraWithholding,
}, },
); );
emit(state.copyWith(status: FormW4Status.success)); emit(state.copyWith(status: FormW4Status.success));

View File

@@ -4,60 +4,106 @@ enum FormW4Status { initial, submitting, success, failure }
class FormW4State extends Equatable { class FormW4State extends Equatable {
final int currentStep; 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 String filingStatus;
final bool multipleJobs; final bool multipleJobs;
final String dependentsAmount;
// Dependents
final int qualifyingChildren;
final int otherDependents;
// Adjustments
final String otherIncome; final String otherIncome;
final String deductions; final String deductions;
final String extraWithholding; final String extraWithholding;
final String signature;
final FormW4Status status; final FormW4Status status;
final String? errorMessage; final String? errorMessage;
const FormW4State({ const FormW4State({
this.currentStep = 0, this.currentStep = 0,
this.filingStatus = 'Single', this.firstName = '',
this.lastName = '',
this.ssn = '',
this.address = '',
this.cityStateZip = '',
this.filingStatus = '',
this.multipleJobs = false, this.multipleJobs = false,
this.dependentsAmount = '', this.qualifyingChildren = 0,
this.otherDependents = 0,
this.otherIncome = '', this.otherIncome = '',
this.deductions = '', this.deductions = '',
this.extraWithholding = '', this.extraWithholding = '',
this.signature = '',
this.status = FormW4Status.initial, this.status = FormW4Status.initial,
this.errorMessage, this.errorMessage,
}); });
FormW4State copyWith({ FormW4State copyWith({
int? currentStep, int? currentStep,
String? firstName,
String? lastName,
String? ssn,
String? address,
String? cityStateZip,
String? filingStatus, String? filingStatus,
bool? multipleJobs, bool? multipleJobs,
String? dependentsAmount, int? qualifyingChildren,
int? otherDependents,
String? otherIncome, String? otherIncome,
String? deductions, String? deductions,
String? extraWithholding, String? extraWithholding,
String? signature,
FormW4Status? status, FormW4Status? status,
String? errorMessage, String? errorMessage,
}) { }) {
return FormW4State( return FormW4State(
currentStep: currentStep ?? this.currentStep, 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, filingStatus: filingStatus ?? this.filingStatus,
multipleJobs: multipleJobs ?? this.multipleJobs, multipleJobs: multipleJobs ?? this.multipleJobs,
dependentsAmount: dependentsAmount ?? this.dependentsAmount, qualifyingChildren: qualifyingChildren ?? this.qualifyingChildren,
otherDependents: otherDependents ?? this.otherDependents,
otherIncome: otherIncome ?? this.otherIncome, otherIncome: otherIncome ?? this.otherIncome,
deductions: deductions ?? this.deductions, deductions: deductions ?? this.deductions,
extraWithholding: extraWithholding ?? this.extraWithholding, extraWithholding: extraWithholding ?? this.extraWithholding,
signature: signature ?? this.signature,
status: status ?? this.status, status: status ?? this.status,
errorMessage: errorMessage ?? this.errorMessage, errorMessage: errorMessage ?? this.errorMessage,
); );
} }
int get totalCredits => (qualifyingChildren * 2000) + (otherDependents * 500);
@override @override
List<Object?> get props => [ List<Object?> get props => [
currentStep, currentStep,
firstName,
lastName,
ssn,
address,
cityStateZip,
filingStatus, filingStatus,
multipleJobs, multipleJobs,
dependentsAmount, qualifyingChildren,
otherDependents,
otherIncome, otherIncome,
deductions, deductions,
extraWithholding, extraWithholding,
signature,
status, status,
errorMessage, errorMessage,
]; ];

View File

@@ -1,6 +1,10 @@
import 'package:design_system/design_system.dart'; import 'package:design_system/design_system.dart';
import 'package:flutter/material.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 { class FormI9Page extends StatefulWidget {
const FormI9Page({super.key}); const FormI9Page({super.key});
@@ -10,34 +14,6 @@ class FormI9Page extends StatefulWidget {
} }
class _FormI9PageState extends State<FormI9Page> { class _FormI9PageState extends State<FormI9Page> {
int _currentStep = 0;
bool _isSubmitting = false;
bool _isSuccess = false;
final Map<String, dynamic> _formData = <String, dynamic>{
'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<String> _usStates = <String>[ final List<String> _usStates = <String>[
'AL', 'AK', 'AZ', 'AR', 'CA', 'CO', 'CT', 'DE', 'FL', 'GA', 'AL', 'AK', 'AZ', 'AR', 'CA', 'CO', 'CT', 'DE', 'FL', 'GA',
'HI', 'ID', 'IL', 'IN', 'IA', 'KS', 'KY', 'LA', 'ME', 'MD', 'HI', 'ID', 'IL', 'IN', 'IA', 'KS', 'KY', 'LA', 'ME', 'MD',
@@ -53,76 +29,74 @@ class _FormI9PageState extends State<FormI9Page> {
<String, String>{'title': 'Review & Sign', 'subtitle': 'Confirm your information'}, <String, String>{'title': 'Review & Sign', 'subtitle': 'Confirm your information'},
]; ];
void _updateField(String key, dynamic value) { bool _canProceed(FormI9State state) {
setState(() { switch (state.currentStep) {
_formData[key] = value;
});
}
bool _canProceed() {
switch (_currentStep) {
case 0: case 0:
return (_formData['firstName'] as String).trim().isNotEmpty && return state.firstName.trim().isNotEmpty &&
(_formData['lastName'] as String).trim().isNotEmpty && state.lastName.trim().isNotEmpty &&
(_formData['dob'] as String).isNotEmpty && state.dob.isNotEmpty &&
(_formData['ssn'] as String).replaceAll(RegExp(r'\D'), '').length >= 9; state.ssn.replaceAll(RegExp(r'\D'), '').length >= 9;
case 1: case 1:
return (_formData['address'] as String).trim().isNotEmpty && return state.address.trim().isNotEmpty &&
(_formData['city'] as String).trim().isNotEmpty && state.city.trim().isNotEmpty &&
(_formData['state'] as String).isNotEmpty && state.state.isNotEmpty &&
(_formData['zipCode'] as String).isNotEmpty; state.zipCode.isNotEmpty;
case 2: case 2:
return (_formData['citizenshipStatus'] as String).isNotEmpty; return state.citizenshipStatus.isNotEmpty;
case 3: case 3:
return _signature.trim().isNotEmpty; return state.signature.trim().isNotEmpty;
default: default:
return true; return true;
} }
} }
void _handleNext() { void _handleNext(BuildContext context, int currentStep) {
if (_currentStep < _steps.length - 1) { if (currentStep < _steps.length - 1) {
setState(() => _currentStep++); context.read<FormI9Cubit>().nextStep(_steps.length);
} else { } else {
_submitForm(); context.read<FormI9Cubit>().submit();
} }
} }
void _handleBack() { void _handleBack(BuildContext context) {
if (_currentStep > 0) { context.read<FormI9Cubit>().previousStep();
setState(() => _currentStep--);
}
}
Future<void> _submitForm() async {
setState(() => _isSubmitting = true);
// Mock API call
await Future<void>.delayed(const Duration(seconds: 2));
if (mounted) {
setState(() {
_isSubmitting = false;
_isSuccess = true;
});
}
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_isSuccess) return _buildSuccessView(); return BlocProvider<FormI9Cubit>.value(
value: Modular.get<FormI9Cubit>(),
child: BlocConsumer<FormI9Cubit, FormI9State>(
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( return Scaffold(
backgroundColor: UiColors.background, backgroundColor: UiColors.background,
body: Column( body: Column(
children: <Widget>[ children: <Widget>[
_buildHeader(), _buildHeader(context, state),
Expanded( Expanded(
child: SingleChildScrollView( child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24), padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24),
child: _buildCurrentStep(), child: _buildCurrentStep(context, state),
),
),
_buildFooter(context, state),
],
), ),
), );
_buildFooter(), },
],
), ),
); );
} }
@@ -194,7 +168,7 @@ class _FormI9PageState extends State<FormI9Page> {
); );
} }
Widget _buildHeader() { Widget _buildHeader(BuildContext context, FormI9State state) {
return Container( return Container(
color: UiColors.primary, color: UiColors.primary,
padding: const EdgeInsets.only(top: 60, bottom: 24, left: 20, right: 20), padding: const EdgeInsets.only(top: 60, bottom: 24, left: 20, right: 20),
@@ -241,7 +215,7 @@ class _FormI9PageState extends State<FormI9Page> {
child: Container( child: Container(
height: 4, height: 4,
decoration: BoxDecoration( decoration: BoxDecoration(
color: idx <= _currentStep color: idx <= state.currentStep
? UiColors.bgPopup ? UiColors.bgPopup
: UiColors.bgPopup.withOpacity(0.3), : UiColors.bgPopup.withOpacity(0.3),
borderRadius: BorderRadius.circular(2), borderRadius: BorderRadius.circular(2),
@@ -259,12 +233,12 @@ class _FormI9PageState extends State<FormI9Page> {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[ children: <Widget>[
Text( Text(
'Step ${_currentStep + 1} of ${_steps.length}', 'Step ${state.currentStep + 1} of ${_steps.length}',
style: UiTypography.body3r.copyWith(color: UiColors.bgPopup.withOpacity(0.7)), style: UiTypography.body3r.copyWith(color: UiColors.bgPopup.withOpacity(0.7)),
), ),
Expanded( Expanded(
child: Text( child: Text(
_steps[_currentStep]['title']!, _steps[state.currentStep]['title']!,
textAlign: TextAlign.end, textAlign: TextAlign.end,
style: UiTypography.body3m.copyWith( style: UiTypography.body3m.copyWith(
color: UiColors.bgPopup, color: UiColors.bgPopup,
@@ -279,27 +253,27 @@ class _FormI9PageState extends State<FormI9Page> {
); );
} }
Widget _buildCurrentStep() { Widget _buildCurrentStep(BuildContext context, FormI9State state) {
switch (_currentStep) { switch (state.currentStep) {
case 0: case 0:
return _buildStep1(); return _buildStep1(context, state);
case 1: case 1:
return _buildStep2(); return _buildStep2(context, state);
case 2: case 2:
return _buildStep3(); return _buildStep3(context, state);
case 3: case 3:
return _buildStep4(); return _buildStep4(context, state);
default: default:
return Container(); return Container();
} }
} }
Widget _buildTextField( Widget _buildTextField(
String label, String label, {
String key, { required String value,
required ValueChanged<String> onChanged,
TextInputType? keyboardType, TextInputType? keyboardType,
String? placeholder, String? placeholder,
Function(String)? onChanged,
}) { }) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -313,11 +287,11 @@ class _FormI9PageState extends State<FormI9Page> {
), ),
const SizedBox(height: 6), const SizedBox(height: 6),
TextField( TextField(
controller: TextEditingController(text: _formData[key].toString()) controller: TextEditingController(text: value)
..selection = TextSelection.fromPosition( ..selection = TextSelection.fromPosition(
TextPosition(offset: (_formData[key].toString()).length), TextPosition(offset: value.length),
), ),
onChanged: onChanged ?? (String val) => _updateField(key, val), onChanged: onChanged,
keyboardType: keyboardType, keyboardType: keyboardType,
style: UiTypography.body2r.copyWith(color: UiColors.textPrimary), style: UiTypography.body2r.copyWith(color: UiColors.textPrimary),
decoration: InputDecoration( decoration: InputDecoration(
@@ -347,7 +321,7 @@ class _FormI9PageState extends State<FormI9Page> {
); );
} }
Widget _buildStep1() { Widget _buildStep1(BuildContext context, FormI9State state) {
return Column( return Column(
children: <Widget>[ children: <Widget>[
Row( Row(
@@ -355,7 +329,8 @@ class _FormI9PageState extends State<FormI9Page> {
Expanded( Expanded(
child: _buildTextField( child: _buildTextField(
'First Name *', 'First Name *',
'firstName', value: state.firstName,
onChanged: (String val) => context.read<FormI9Cubit>().firstNameChanged(val),
placeholder: 'John', placeholder: 'John',
), ),
), ),
@@ -363,7 +338,8 @@ class _FormI9PageState extends State<FormI9Page> {
Expanded( Expanded(
child: _buildTextField( child: _buildTextField(
'Last Name *', 'Last Name *',
'lastName', value: state.lastName,
onChanged: (String val) => context.read<FormI9Cubit>().lastNameChanged(val),
placeholder: 'Smith', placeholder: 'Smith',
), ),
), ),
@@ -375,7 +351,8 @@ class _FormI9PageState extends State<FormI9Page> {
Expanded( Expanded(
child: _buildTextField( child: _buildTextField(
'Middle Initial', 'Middle Initial',
'middleInitial', value: state.middleInitial,
onChanged: (String val) => context.read<FormI9Cubit>().middleInitialChanged(val),
placeholder: 'A', placeholder: 'A',
), ),
), ),
@@ -384,7 +361,8 @@ class _FormI9PageState extends State<FormI9Page> {
flex: 2, flex: 2,
child: _buildTextField( child: _buildTextField(
'Other Last Names', 'Other Last Names',
'otherLastNames', value: state.otherLastNames,
onChanged: (String val) => context.read<FormI9Cubit>().otherLastNamesChanged(val),
placeholder: 'Maiden name (if any)', placeholder: 'Maiden name (if any)',
), ),
), ),
@@ -393,33 +371,36 @@ class _FormI9PageState extends State<FormI9Page> {
const SizedBox(height: 16), const SizedBox(height: 16),
_buildTextField( _buildTextField(
'Date of Birth *', 'Date of Birth *',
'dob', value: state.dob,
onChanged: (String val) => context.read<FormI9Cubit>().dobChanged(val),
placeholder: 'MM/DD/YYYY', placeholder: 'MM/DD/YYYY',
keyboardType: TextInputType.datetime, keyboardType: TextInputType.datetime,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
_buildTextField( _buildTextField(
'Social Security Number *', 'Social Security Number *',
'ssn', value: state.ssn,
placeholder: 'XXX-XX-XXXX', placeholder: 'XXX-XX-XXXX',
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
onChanged: (String val) { onChanged: (String val) {
String text = val.replaceAll(RegExp(r'\D'), ''); String text = val.replaceAll(RegExp(r'\D'), '');
if (text.length > 9) text = text.substring(0, 9); if (text.length > 9) text = text.substring(0, 9);
_updateField('ssn', text); context.read<FormI9Cubit>().ssnChanged(text);
}, },
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
_buildTextField( _buildTextField(
'Email Address', 'Email Address',
'email', value: state.email,
onChanged: (String val) => context.read<FormI9Cubit>().emailChanged(val),
keyboardType: TextInputType.emailAddress, keyboardType: TextInputType.emailAddress,
placeholder: 'john.smith@example.com', placeholder: 'john.smith@example.com',
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
_buildTextField( _buildTextField(
'Phone Number', 'Phone Number',
'phone', value: state.phone,
onChanged: (String val) => context.read<FormI9Cubit>().phoneChanged(val),
keyboardType: TextInputType.phone, keyboardType: TextInputType.phone,
placeholder: '(555) 555-5555', placeholder: '(555) 555-5555',
), ),
@@ -427,18 +408,20 @@ class _FormI9PageState extends State<FormI9Page> {
); );
} }
Widget _buildStep2() { Widget _buildStep2(BuildContext context, FormI9State state) {
return Column( return Column(
children: <Widget>[ children: <Widget>[
_buildTextField( _buildTextField(
'Address (Street Number and Name) *', 'Address (Street Number and Name) *',
'address', value: state.address,
onChanged: (String val) => context.read<FormI9Cubit>().addressChanged(val),
placeholder: '123 Main Street', placeholder: '123 Main Street',
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
_buildTextField( _buildTextField(
'Apt. Number', 'Apt. Number',
'aptNumber', value: state.aptNumber,
onChanged: (String val) => context.read<FormI9Cubit>().aptNumberChanged(val),
placeholder: '4B', placeholder: '4B',
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
@@ -448,7 +431,8 @@ class _FormI9PageState extends State<FormI9Page> {
flex: 2, flex: 2,
child: _buildTextField( child: _buildTextField(
'City or Town *', 'City or Town *',
'city', value: state.city,
onChanged: (String val) => context.read<FormI9Cubit>().cityChanged(val),
placeholder: 'San Francisco', placeholder: 'San Francisco',
), ),
), ),
@@ -466,12 +450,12 @@ class _FormI9PageState extends State<FormI9Page> {
), ),
const SizedBox(height: 6), const SizedBox(height: 6),
DropdownButtonFormField<String>( DropdownButtonFormField<String>(
value: _formData['state'].toString().isEmpty ? null : _formData['state'].toString(), value: state.state.isEmpty ? null : state.state,
onChanged: (String? val) => _updateField('state', val), onChanged: (String? val) => context.read<FormI9Cubit>().stateChanged(val ?? ''),
items: _usStates.map((String state) { items: _usStates.map((String stateAbbr) {
return DropdownMenuItem<String>( return DropdownMenuItem<String>(
value: state, value: stateAbbr,
child: Text(state), child: Text(stateAbbr),
); );
}).toList(), }).toList(),
decoration: InputDecoration( decoration: InputDecoration(
@@ -496,7 +480,8 @@ class _FormI9PageState extends State<FormI9Page> {
const SizedBox(height: 16), const SizedBox(height: 16),
_buildTextField( _buildTextField(
'ZIP Code *', 'ZIP Code *',
'zipCode', value: state.zipCode,
onChanged: (String val) => context.read<FormI9Cubit>().zipCodeChanged(val),
placeholder: '94103', placeholder: '94103',
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
), ),
@@ -504,7 +489,7 @@ class _FormI9PageState extends State<FormI9Page> {
); );
} }
Widget _buildStep3() { Widget _buildStep3(BuildContext context, FormI9State state) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
@@ -514,24 +499,31 @@ class _FormI9PageState extends State<FormI9Page> {
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
_buildRadioOption( _buildRadioOption(
context,
state,
'citizen', 'citizen',
'1. A citizen of the United States', '1. A citizen of the United States',
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
_buildRadioOption( _buildRadioOption(
context,
state,
'noncitizen_national', 'noncitizen_national',
'2. A noncitizen national of the United States', '2. A noncitizen national of the United States',
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
_buildRadioOption( _buildRadioOption(
context,
state,
'permanent_resident', 'permanent_resident',
'3. A lawful permanent resident', '3. A lawful permanent resident',
child: _formData['citizenshipStatus'] == 'permanent_resident' child: state.citizenshipStatus == 'permanent_resident'
? Padding( ? Padding(
padding: const EdgeInsets.only(top: 12), padding: const EdgeInsets.only(top: 12),
child: _buildTextField( child: _buildTextField(
'USCIS Number', 'USCIS Number',
'uscisNumber', value: state.uscisNumber,
onChanged: (String val) => context.read<FormI9Cubit>().uscisNumberChanged(val),
placeholder: 'A-123456789', placeholder: 'A-123456789',
), ),
) )
@@ -539,26 +531,31 @@ class _FormI9PageState extends State<FormI9Page> {
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
_buildRadioOption( _buildRadioOption(
context,
state,
'alien_authorized', 'alien_authorized',
'4. An alien authorized to work', '4. An alien authorized to work',
child: _formData['citizenshipStatus'] == 'alien_authorized' child: state.citizenshipStatus == 'alien_authorized'
? Padding( ? Padding(
padding: const EdgeInsets.only(top: 12), padding: const EdgeInsets.only(top: 12),
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
_buildTextField( _buildTextField(
'USCIS/Admission Number', 'USCIS/Admission Number',
'admissionNumber', value: state.admissionNumber,
onChanged: (String val) => context.read<FormI9Cubit>().admissionNumberChanged(val),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
_buildTextField( _buildTextField(
'Foreign Passport Number', 'Foreign Passport Number',
'passportNumber', value: state.passportNumber,
onChanged: (String val) => context.read<FormI9Cubit>().passportNumberChanged(val),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
_buildTextField( _buildTextField(
'Country of Issuance', 'Country of Issuance',
'countryIssuance', value: state.countryIssuance,
onChanged: (String val) => context.read<FormI9Cubit>().countryIssuanceChanged(val),
), ),
], ],
), ),
@@ -569,10 +566,10 @@ class _FormI9PageState extends State<FormI9Page> {
); );
} }
Widget _buildRadioOption(String value, String label, {Widget? child}) { Widget _buildRadioOption(BuildContext context, FormI9State state, String value, String label, {Widget? child}) {
final bool isSelected = _formData['citizenshipStatus'] == value; final bool isSelected = state.citizenshipStatus == value;
return GestureDetector( return GestureDetector(
onTap: () => _updateField('citizenshipStatus', value), onTap: () => context.read<FormI9Cubit>().citizenshipStatusChanged(value),
child: Container( child: Container(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -616,7 +613,7 @@ class _FormI9PageState extends State<FormI9Page> {
); );
} }
Widget _buildStep4() { Widget _buildStep4(BuildContext context, FormI9State state) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
@@ -635,20 +632,18 @@ class _FormI9PageState extends State<FormI9Page> {
style: UiTypography.headline4m.copyWith(fontSize: 14), style: UiTypography.headline4m.copyWith(fontSize: 14),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
_buildSummaryRow('Name', '${_formData['firstName']} ${_formData['lastName']}'), _buildSummaryRow('Name', '${state.firstName} ${state.lastName}'),
_buildSummaryRow('Address', '${_formData['address']}, ${_formData['city']}'), _buildSummaryRow('Address', '${state.address}, ${state.city}'),
_buildSummaryRow('SSN', '***-**-${(_formData['ssn'] as String).length >= 4 ? (_formData['ssn'] as String).substring((_formData['ssn'] as String).length - 4) : '****'}'), _buildSummaryRow('SSN', '***-**-${state.ssn.length >= 4 ? state.ssn.substring(state.ssn.length - 4) : '****'}'),
_buildSummaryRow('Citizenship', _getReadableCitizenship(_formData['citizenshipStatus'] as String)), _buildSummaryRow('Citizenship', _getReadableCitizenship(state.citizenshipStatus)),
], ],
), ),
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
CheckboxListTile( CheckboxListTile(
value: _preparerUsed, value: state.preparerUsed,
onChanged: (bool? val) { onChanged: (bool? val) {
setState(() { context.read<FormI9Cubit>().preparerUsedChanged(val ?? false);
_preparerUsed = val ?? false;
});
}, },
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
title: Text( title: Text(
@@ -679,7 +674,11 @@ class _FormI9PageState extends State<FormI9Page> {
), ),
const SizedBox(height: 6), const SizedBox(height: 6),
TextField( 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<FormI9Cubit>().signatureChanged(val),
decoration: InputDecoration( decoration: InputDecoration(
hintText: 'Type your full name', hintText: 'Type your full name',
filled: true, filled: true,
@@ -767,7 +766,7 @@ class _FormI9PageState extends State<FormI9Page> {
} }
} }
Widget _buildFooter() { Widget _buildFooter(BuildContext context, FormI9State state) {
return Container( return Container(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
decoration: const BoxDecoration( decoration: const BoxDecoration(
@@ -777,12 +776,12 @@ class _FormI9PageState extends State<FormI9Page> {
child: SafeArea( child: SafeArea(
child: Row( child: Row(
children: <Widget>[ children: <Widget>[
if (_currentStep > 0) if (state.currentStep > 0)
Expanded( Expanded(
child: Padding( child: Padding(
padding: const EdgeInsets.only(right: 12), padding: const EdgeInsets.only(right: 12),
child: OutlinedButton( child: OutlinedButton(
onPressed: _handleBack, onPressed: () => _handleBack(context),
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16), padding: const EdgeInsets.symmetric(vertical: 16),
side: const BorderSide(color: UiColors.border), side: const BorderSide(color: UiColors.border),
@@ -807,8 +806,8 @@ class _FormI9PageState extends State<FormI9Page> {
Expanded( Expanded(
flex: 2, flex: 2,
child: ElevatedButton( child: ElevatedButton(
onPressed: (_canProceed() && !_isSubmitting) onPressed: (_canProceed(state) && state.status != FormI9Status.submitting)
? _handleNext ? () => _handleNext(context, state.currentStep)
: null, : null,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: UiColors.primary, backgroundColor: UiColors.primary,
@@ -820,7 +819,7 @@ class _FormI9PageState extends State<FormI9Page> {
), ),
elevation: 0, elevation: 0,
), ),
child: _isSubmitting child: state.status == FormI9Status.submitting
? const SizedBox( ? const SizedBox(
width: 20, width: 20,
height: 20, height: 20,
@@ -833,11 +832,11 @@ class _FormI9PageState extends State<FormI9Page> {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[ children: <Widget>[
Text( Text(
_currentStep == _steps.length - 1 state.currentStep == _steps.length - 1
? 'Sign & Submit' ? 'Sign & Submit'
: 'Continue', : 'Continue',
), ),
if (_currentStep < _steps.length - 1) ...<Widget>[ if (state.currentStep < _steps.length - 1) ...<Widget>[
const SizedBox(width: 8), const SizedBox(width: 8),
const Icon(UiIcons.arrowRight, size: 16, color: UiColors.bgPopup), const Icon(UiIcons.arrowRight, size: 16, color: UiColors.bgPopup),
], ],

View File

@@ -1,6 +1,10 @@
import 'package:design_system/design_system.dart'; import 'package:design_system/design_system.dart';
import 'package:flutter/material.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 { class FormW4Page extends StatefulWidget {
const FormW4Page({super.key}); const FormW4Page({super.key});
@@ -10,27 +14,6 @@ class FormW4Page extends StatefulWidget {
} }
class _FormW4PageState extends State<FormW4Page> { class _FormW4PageState extends State<FormW4Page> {
int _currentStep = 0;
bool _isSubmitting = false;
bool _isSuccess = false;
final Map<String, dynamic> _formData = <String, dynamic>{
'firstName': '',
'lastName': '',
'address': '',
'cityStateZip': '',
'ssn': '',
'filingStatus': '',
'multipleJobs': false,
'qualifyingChildren': 0,
'otherDependents': 0,
'otherIncome': '',
'deductions': '',
'extraWithholding': '',
};
String _signature = '';
final List<Map<String, String>> _steps = <Map<String, String>>[ final List<Map<String, String>> _steps = <Map<String, String>>[
<String, String>{'title': 'Personal Information', 'subtitle': 'Step 1'}, <String, String>{'title': 'Personal Information', 'subtitle': 'Step 1'},
<String, String>{'title': 'Filing Status', 'subtitle': 'Step 1c'}, <String, String>{'title': 'Filing Status', 'subtitle': 'Step 1c'},
@@ -40,76 +23,75 @@ class _FormW4PageState extends State<FormW4Page> {
<String, String>{'title': 'Review & Sign', 'subtitle': 'Step 5'}, <String, String>{'title': 'Review & Sign', 'subtitle': 'Step 5'},
]; ];
void _updateField(String key, dynamic value) { bool _canProceed(FormW4State state) {
setState(() { switch (state.currentStep) {
_formData[key] = value;
});
}
bool _canProceed() {
switch (_currentStep) {
case 0: case 0:
return (_formData['firstName'] as String).trim().isNotEmpty && return state.firstName.trim().isNotEmpty &&
(_formData['lastName'] as String).trim().isNotEmpty && state.lastName.trim().isNotEmpty &&
(_formData['ssn'] as String).replaceAll(RegExp(r'\D'), '').length >= 4 && state.ssn.replaceAll(RegExp(r'\D'), '').length >= 4 &&
(_formData['address'] as String).trim().isNotEmpty; state.address.trim().isNotEmpty;
case 1: case 1:
return (_formData['filingStatus'] as String).isNotEmpty; return state.filingStatus.isNotEmpty;
case 5: case 5:
return _signature.trim().isNotEmpty; return state.signature.trim().isNotEmpty;
default: default:
return true; return true;
} }
} }
void _handleNext() { void _handleNext(BuildContext context, int currentStep) {
if (_currentStep < _steps.length - 1) { if (currentStep < _steps.length - 1) {
setState(() => _currentStep++); context.read<FormW4Cubit>().nextStep(_steps.length);
} else { } else {
_submitForm(); context.read<FormW4Cubit>().submit();
} }
} }
void _handleBack() { void _handleBack(BuildContext context) {
if (_currentStep > 0) { context.read<FormW4Cubit>().previousStep();
setState(() => _currentStep--);
}
} }
Future<void> _submitForm() async {
setState(() => _isSubmitting = true);
// Mock API call
await Future<void>.delayed(const Duration(seconds: 2));
if (mounted) {
setState(() {
_isSubmitting = false;
_isSuccess = true;
});
}
}
int get _totalCredits { int _totalCredits(FormW4State state) {
return ((_formData['qualifyingChildren'] as int) * 2000) + return (state.qualifyingChildren * 2000) +
((_formData['otherDependents'] as int) * 500); (state.otherDependents * 500);
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_isSuccess) return _buildSuccessView(); return BlocProvider<FormW4Cubit>.value(
value: Modular.get<FormW4Cubit>(),
child: BlocConsumer<FormW4Cubit, FormW4State>(
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( return Scaffold(
backgroundColor: UiColors.background, backgroundColor: UiColors.background,
body: Column( body: Column(
children: <Widget>[ children: <Widget>[
_buildHeader(), _buildHeader(context, state),
Expanded( Expanded(
child: SingleChildScrollView( child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24), padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24),
child: _buildCurrentStep(), child: _buildCurrentStep(context, state),
),
),
_buildFooter(context, state),
],
), ),
), );
_buildFooter(), },
],
), ),
); );
} }
@@ -181,7 +163,7 @@ class _FormW4PageState extends State<FormW4Page> {
); );
} }
Widget _buildHeader() { Widget _buildHeader(BuildContext context, FormW4State state) {
return Container( return Container(
color: UiColors.primary, color: UiColors.primary,
padding: const EdgeInsets.only(top: 60, bottom: 24, left: 20, right: 20), padding: const EdgeInsets.only(top: 60, bottom: 24, left: 20, right: 20),
@@ -205,7 +187,6 @@ class _FormW4PageState extends State<FormW4Page> {
Text( Text(
'Form W-4', 'Form W-4',
style: UiTypography.headline4m.copyWith( style: UiTypography.headline4m.copyWith(
color: UiColors.bgPopup, color: UiColors.bgPopup,
), ),
), ),
@@ -229,7 +210,7 @@ class _FormW4PageState extends State<FormW4Page> {
child: Container( child: Container(
height: 4, height: 4,
decoration: BoxDecoration( decoration: BoxDecoration(
color: idx <= _currentStep color: idx <= state.currentStep
? UiColors.bgPopup ? UiColors.bgPopup
: UiColors.bgPopup.withOpacity(0.3), : UiColors.bgPopup.withOpacity(0.3),
borderRadius: BorderRadius.circular(2), borderRadius: BorderRadius.circular(2),
@@ -247,11 +228,11 @@ class _FormW4PageState extends State<FormW4Page> {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[ children: <Widget>[
Text( Text(
'Step ${_currentStep + 1} of ${_steps.length}', 'Step ${state.currentStep + 1} of ${_steps.length}',
style: UiTypography.body3r.copyWith(color: UiColors.bgPopup.withOpacity(0.7)), style: UiTypography.body3r.copyWith(color: UiColors.bgPopup.withOpacity(0.7)),
), ),
Text( Text(
_steps[_currentStep]['title']!, _steps[state.currentStep]['title']!,
style: UiTypography.body3m.copyWith( style: UiTypography.body3m.copyWith(
color: UiColors.bgPopup, color: UiColors.bgPopup,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
@@ -264,31 +245,31 @@ class _FormW4PageState extends State<FormW4Page> {
); );
} }
Widget _buildCurrentStep() { Widget _buildCurrentStep(BuildContext context, FormW4State state) {
switch (_currentStep) { switch (state.currentStep) {
case 0: case 0:
return _buildStep1(); return _buildStep1(context, state);
case 1: case 1:
return _buildStep2(); return _buildStep2(context, state);
case 2: case 2:
return _buildStep3(); return _buildStep3(context, state);
case 3: case 3:
return _buildStep4(); return _buildStep4(context, state);
case 4: case 4:
return _buildStep5(); return _buildStep5(context, state);
case 5: case 5:
return _buildStep6(); return _buildStep6(context, state);
default: default:
return Container(); return Container();
} }
} }
Widget _buildTextField( Widget _buildTextField(
String label, String label, {
String key, { required String value,
required ValueChanged<String> onChanged,
TextInputType? keyboardType, TextInputType? keyboardType,
String? placeholder, String? placeholder,
Function(String)? onChanged,
}) { }) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -302,11 +283,11 @@ class _FormW4PageState extends State<FormW4Page> {
), ),
const SizedBox(height: 6), const SizedBox(height: 6),
TextField( TextField(
controller: TextEditingController(text: _formData[key].toString()) controller: TextEditingController(text: value)
..selection = TextSelection.fromPosition( ..selection = TextSelection.fromPosition(
TextPosition(offset: (_formData[key].toString()).length), TextPosition(offset: value.length),
), ),
onChanged: onChanged ?? (String val) => _updateField(key, val), onChanged: onChanged,
keyboardType: keyboardType, keyboardType: keyboardType,
style: UiTypography.body2r.copyWith(color: UiColors.textPrimary), style: UiTypography.body2r.copyWith(color: UiColors.textPrimary),
decoration: InputDecoration( decoration: InputDecoration(
@@ -336,7 +317,7 @@ class _FormW4PageState extends State<FormW4Page> {
); );
} }
Widget _buildStep1() { Widget _buildStep1(BuildContext context, FormW4State state) {
return Column( return Column(
children: <Widget>[ children: <Widget>[
Row( Row(
@@ -344,7 +325,8 @@ class _FormW4PageState extends State<FormW4Page> {
Expanded( Expanded(
child: _buildTextField( child: _buildTextField(
'First Name *', 'First Name *',
'firstName', value: state.firstName,
onChanged: (String val) => context.read<FormW4Cubit>().firstNameChanged(val),
placeholder: 'John', placeholder: 'John',
), ),
), ),
@@ -352,7 +334,8 @@ class _FormW4PageState extends State<FormW4Page> {
Expanded( Expanded(
child: _buildTextField( child: _buildTextField(
'Last Name *', 'Last Name *',
'lastName', value: state.lastName,
onChanged: (String val) => context.read<FormW4Cubit>().lastNameChanged(val),
placeholder: 'Smith', placeholder: 'Smith',
), ),
), ),
@@ -361,28 +344,34 @@ class _FormW4PageState extends State<FormW4Page> {
const SizedBox(height: 16), const SizedBox(height: 16),
_buildTextField( _buildTextField(
'Social Security Number *', 'Social Security Number *',
'ssn', value: state.ssn,
placeholder: 'XXX-XX-XXXX', placeholder: 'XXX-XX-XXXX',
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
onChanged: (String val) { onChanged: (String val) {
String text = val.replaceAll(RegExp(r'\D'), ''); String text = val.replaceAll(RegExp(r'\D'), '');
if (text.length > 9) text = text.substring(0, 9); if (text.length > 9) text = text.substring(0, 9);
_updateField('ssn', text); context.read<FormW4Cubit>().ssnChanged(text);
}, },
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
_buildTextField('Address *', 'address', placeholder: '123 Main Street'), _buildTextField(
'Address *',
value: state.address,
onChanged: (String val) => context.read<FormW4Cubit>().addressChanged(val),
placeholder: '123 Main Street',
),
const SizedBox(height: 16), const SizedBox(height: 16),
_buildTextField( _buildTextField(
'City, State, ZIP', 'City, State, ZIP',
'cityStateZip', value: state.cityStateZip,
onChanged: (String val) => context.read<FormW4Cubit>().cityStateZipChanged(val),
placeholder: 'San Francisco, CA 94102', placeholder: 'San Francisco, CA 94102',
), ),
], ],
); );
} }
Widget _buildStep2() { Widget _buildStep2(BuildContext context, FormW4State state) {
return Column( return Column(
children: <Widget>[ children: <Widget>[
Container( Container(
@@ -406,18 +395,24 @@ class _FormW4PageState extends State<FormW4Page> {
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
_buildRadioOption( _buildRadioOption(
context,
state,
'single', 'single',
'Single or Married filing separately', 'Single or Married filing separately',
null, null,
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
_buildRadioOption( _buildRadioOption(
context,
state,
'married', 'married',
'Married filing jointly or Qualifying surviving spouse', 'Married filing jointly or Qualifying surviving spouse',
null, null,
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
_buildRadioOption( _buildRadioOption(
context,
state,
'head_of_household', 'head_of_household',
'Head of household', 'Head of household',
'Check only if you\'re unmarried and pay more than half the costs of keeping up a home', '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<FormW4Page> {
); );
} }
Widget _buildRadioOption(String value, String label, String? subLabel) { Widget _buildRadioOption(BuildContext context, FormW4State state, String value, String label, String? subLabel) {
final bool isSelected = _formData['filingStatus'] == value; final bool isSelected = state.filingStatus == value;
return GestureDetector( return GestureDetector(
onTap: () => _updateField('filingStatus', value), onTap: () => context.read<FormW4Cubit>().filingStatusChanged(value),
child: Container( child: Container(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -484,7 +479,7 @@ class _FormW4PageState extends State<FormW4Page> {
); );
} }
Widget _buildStep3() { Widget _buildStep3(BuildContext context, FormW4State state) {
return Column( return Column(
children: <Widget>[ children: <Widget>[
Container( Container(
@@ -527,14 +522,14 @@ class _FormW4PageState extends State<FormW4Page> {
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
GestureDetector( GestureDetector(
onTap: () => _updateField('multipleJobs', !_formData['multipleJobs']), onTap: () => context.read<FormW4Cubit>().multipleJobsChanged(!state.multipleJobs),
child: Container( child: Container(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: UiColors.bgPopup, color: UiColors.bgPopup,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
border: Border.all( border: Border.all(
color: (_formData['multipleJobs'] as bool) color: state.multipleJobs
? UiColors.primary ? UiColors.primary
: UiColors.border, : UiColors.border,
), ),
@@ -546,17 +541,17 @@ class _FormW4PageState extends State<FormW4Page> {
width: 24, width: 24,
height: 24, height: 24,
decoration: BoxDecoration( decoration: BoxDecoration(
color: (_formData['multipleJobs'] as bool) color: state.multipleJobs
? UiColors.primary ? UiColors.primary
: UiColors.bgPopup, : UiColors.bgPopup,
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(6),
border: Border.all( border: Border.all(
color: (_formData['multipleJobs'] as bool) color: state.multipleJobs
? UiColors.primary ? UiColors.primary
: Colors.grey, : Colors.grey,
), ),
), ),
child: (_formData['multipleJobs'] as bool) child: state.multipleJobs
? const Icon( ? const Icon(
UiIcons.check, UiIcons.check,
color: UiColors.bgPopup, color: UiColors.bgPopup,
@@ -599,7 +594,7 @@ class _FormW4PageState extends State<FormW4Page> {
); );
} }
Widget _buildStep4() { Widget _buildStep4(BuildContext context, FormW4State state) {
return Column( return Column(
children: <Widget>[ children: <Widget>[
Container( Container(
@@ -632,23 +627,29 @@ class _FormW4PageState extends State<FormW4Page> {
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
_buildCounter( _buildCounter(
context,
state,
'Qualifying children under age 17', 'Qualifying children under age 17',
'\$2,000 each', '\$2,000 each',
'qualifyingChildren', (FormW4State s) => s.qualifyingChildren,
(int val) => context.read<FormW4Cubit>().qualifyingChildrenChanged(val),
), ),
const Padding( const Padding(
padding: EdgeInsets.symmetric(vertical: 16), padding: EdgeInsets.symmetric(vertical: 16),
child: Divider(height: 1, color: UiColors.border), child: Divider(height: 1, color: UiColors.border),
), ),
_buildCounter( _buildCounter(
context,
state,
'Other dependents', 'Other dependents',
'\$500 each', '\$500 each',
'otherDependents', (FormW4State s) => s.otherDependents,
(int val) => context.read<FormW4Cubit>().otherDependentsChanged(val),
), ),
], ],
), ),
), ),
if (_totalCredits > 0) ...<Widget>[ if (_totalCredits(state) > 0) ...<Widget>[
const SizedBox(height: 16), const SizedBox(height: 16),
Container( Container(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
@@ -667,7 +668,7 @@ class _FormW4PageState extends State<FormW4Page> {
), ),
), ),
Text( Text(
'\$${_totalCredits}', '\$${_totalCredits(state)}',
style: const TextStyle( style: const TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 18, fontSize: 18,
@@ -682,8 +683,15 @@ class _FormW4PageState extends State<FormW4Page> {
); );
} }
Widget _buildCounter(String label, String badge, String key) { Widget _buildCounter(
final int value = _formData[key] as int; BuildContext context,
FormW4State state,
String label,
String badge,
int Function(FormW4State) getValue,
Function(int) onChanged,
) {
final int value = getValue(state);
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
@@ -718,7 +726,7 @@ class _FormW4PageState extends State<FormW4Page> {
children: <Widget>[ children: <Widget>[
_buildCircleBtn( _buildCircleBtn(
UiIcons.minus, UiIcons.minus,
() => _updateField(key, value > 0 ? value - 1 : 0), () => onChanged(value > 0 ? value - 1 : 0),
), ),
SizedBox( SizedBox(
width: 48, width: 48,
@@ -733,7 +741,7 @@ class _FormW4PageState extends State<FormW4Page> {
), ),
_buildCircleBtn( _buildCircleBtn(
UiIcons.add, UiIcons.add,
() => _updateField(key, value + 1), () => onChanged(value + 1),
), ),
], ],
), ),
@@ -757,7 +765,7 @@ class _FormW4PageState extends State<FormW4Page> {
); );
} }
Widget _buildStep5() { Widget _buildStep5(BuildContext context, FormW4State state) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
@@ -768,7 +776,8 @@ class _FormW4PageState extends State<FormW4Page> {
const SizedBox(height: 24), const SizedBox(height: 24),
_buildTextField( _buildTextField(
'4(a) Other income (not from jobs)', '4(a) Other income (not from jobs)',
'otherIncome', value: state.otherIncome,
onChanged: (String val) => context.read<FormW4Cubit>().otherIncomeChanged(val),
placeholder: '\$0', placeholder: '\$0',
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
), ),
@@ -782,7 +791,8 @@ class _FormW4PageState extends State<FormW4Page> {
_buildTextField( _buildTextField(
'4(b) Deductions', '4(b) Deductions',
'deductions', value: state.deductions,
onChanged: (String val) => context.read<FormW4Cubit>().deductionsChanged(val),
placeholder: '\$0', placeholder: '\$0',
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
), ),
@@ -796,14 +806,15 @@ class _FormW4PageState extends State<FormW4Page> {
_buildTextField( _buildTextField(
'4(c) Extra withholding', '4(c) Extra withholding',
'extraWithholding', value: state.extraWithholding,
onChanged: (String val) => context.read<FormW4Cubit>().extraWithholdingChanged(val),
placeholder: '\$0', placeholder: '\$0',
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
), ),
Padding( Padding(
padding: const EdgeInsets.only(top: 4, bottom: 16), padding: const EdgeInsets.only(top: 4, bottom: 16),
child: Text( 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), style: UiTypography.body3r.copyWith(color: UiColors.textSecondary),
), ),
), ),
@@ -811,7 +822,7 @@ class _FormW4PageState extends State<FormW4Page> {
); );
} }
Widget _buildStep6() { Widget _buildStep6(BuildContext context, FormW4State state) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
@@ -832,20 +843,20 @@ class _FormW4PageState extends State<FormW4Page> {
const SizedBox(height: 12), const SizedBox(height: 12),
_buildSummaryRow( _buildSummaryRow(
'Name', 'Name',
'${_formData['firstName']} ${_formData['lastName']}', '${state.firstName} ${state.lastName}',
), ),
_buildSummaryRow( _buildSummaryRow(
'SSN', '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( _buildSummaryRow(
'Filing Status', 'Filing Status',
_getFilingStatusLabel(_formData['filingStatus']), _getFilingStatusLabel(state.filingStatus),
), ),
if (_totalCredits > 0) if (_totalCredits(state) > 0)
_buildSummaryRow( _buildSummaryRow(
'Credits', 'Credits',
'\$${_totalCredits}', '\$${_totalCredits(state)}',
valueColor: Colors.green[700], valueColor: Colors.green[700],
), ),
], ],
@@ -872,7 +883,11 @@ class _FormW4PageState extends State<FormW4Page> {
), ),
const SizedBox(height: 6), const SizedBox(height: 6),
TextField( 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<FormW4Cubit>().signatureChanged(val),
decoration: InputDecoration( decoration: InputDecoration(
hintText: 'Type your full name', hintText: 'Type your full name',
filled: true, filled: true,
@@ -955,7 +970,7 @@ class _FormW4PageState extends State<FormW4Page> {
} }
} }
Widget _buildFooter() { Widget _buildFooter(BuildContext context, FormW4State state) {
return Container( return Container(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
decoration: const BoxDecoration( decoration: const BoxDecoration(
@@ -965,12 +980,12 @@ class _FormW4PageState extends State<FormW4Page> {
child: SafeArea( child: SafeArea(
child: Row( child: Row(
children: <Widget>[ children: <Widget>[
if (_currentStep > 0) if (state.currentStep > 0)
Expanded( Expanded(
child: Padding( child: Padding(
padding: const EdgeInsets.only(right: 12), padding: const EdgeInsets.only(right: 12),
child: OutlinedButton( child: OutlinedButton(
onPressed: _handleBack, onPressed: () => _handleBack(context),
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16), padding: const EdgeInsets.symmetric(vertical: 16),
side: const BorderSide(color: UiColors.border), side: const BorderSide(color: UiColors.border),
@@ -995,8 +1010,8 @@ class _FormW4PageState extends State<FormW4Page> {
Expanded( Expanded(
flex: 2, flex: 2,
child: ElevatedButton( child: ElevatedButton(
onPressed: (_canProceed() && !_isSubmitting) onPressed: (_canProceed(state) && state.status != FormW4Status.submitting)
? _handleNext ? () => _handleNext(context, state.currentStep)
: null, : null,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: UiColors.primary, backgroundColor: UiColors.primary,
@@ -1008,7 +1023,7 @@ class _FormW4PageState extends State<FormW4Page> {
), ),
elevation: 0, elevation: 0,
), ),
child: _isSubmitting child: state.status == FormW4Status.submitting
? const SizedBox( ? const SizedBox(
width: 20, width: 20,
height: 20, height: 20,
@@ -1021,11 +1036,11 @@ class _FormW4PageState extends State<FormW4Page> {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[ children: <Widget>[
Text( Text(
_currentStep == _steps.length - 1 state.currentStep == _steps.length - 1
? 'Submit Form' ? 'Submit Form'
: 'Continue', : 'Continue',
), ),
if (_currentStep < _steps.length - 1) ...<Widget>[ if (state.currentStep < _steps.length - 1) ...<Widget>[
const SizedBox(width: 8), const SizedBox(width: 8),
const Icon(UiIcons.arrowRight, size: 16, color: UiColors.bgPopup), const Icon(UiIcons.arrowRight, size: 16, color: UiColors.bgPopup),
], ],

View File

@@ -11,7 +11,7 @@ class TaxFormsPage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final cubit = Modular.get<TaxFormsCubit>(); final TaxFormsCubit cubit = Modular.get<TaxFormsCubit>();
if (cubit.state.status == TaxFormsStatus.initial) { if (cubit.state.status == TaxFormsStatus.initial) {
cubit.loadTaxForms(); cubit.loadTaxForms();

View File

@@ -1,6 +1,10 @@
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_modular/flutter_modular.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/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/i9/form_i9_cubit.dart';
import 'presentation/blocs/tax_forms/tax_forms_cubit.dart'; import 'presentation/blocs/tax_forms/tax_forms_cubit.dart';
import 'presentation/blocs/w4/form_w4_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 { class StaffTaxFormsModule extends Module {
@override @override
void binds(Injector i) { void binds(Injector i) {
i.addLazySingleton<TaxFormsRepository>(TaxFormsRepositoryMock.new); i.addLazySingleton<TaxFormsRepository>(
() => TaxFormsRepositoryImpl(
firebaseAuth: FirebaseAuth.instance,
dataConnect: ExampleConnector.instance,
),
);
// Use Cases
i.addLazySingleton(GetTaxFormsUseCase.new);
i.addLazySingleton(SubmitTaxFormUseCase.new);
// Blocs
i.addLazySingleton(TaxFormsCubit.new); i.addLazySingleton(TaxFormsCubit.new);
i.addLazySingleton(FormI9Cubit.new); i.addLazySingleton(FormI9Cubit.new);
i.addLazySingleton(FormW4Cubit.new); i.addLazySingleton(FormW4Cubit.new);