feat: Centralized Error Handling & Crash Fixes
This commit is contained in:
@@ -74,11 +74,10 @@ class CertificatesPage extends StatelessWidget {
|
||||
onEditExpiry: () => _showEditExpiryDialog(context, doc),
|
||||
onRemove: () => _showRemoveConfirmation(context, doc),
|
||||
onView: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(t.staff_certificates.card.opened_snackbar),
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: t.staff_certificates.card.opened_snackbar,
|
||||
type: UiSnackbarType.success,
|
||||
);
|
||||
},
|
||||
)),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart' hide ModularWatchExtension;
|
||||
@@ -76,6 +77,15 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final i18n = Translations.of(context).staff_compliance.tax_forms.i9;
|
||||
|
||||
final List<Map<String, String>> steps = <Map<String, String>>[
|
||||
<String, String>{'title': i18n.steps.personal, 'subtitle': i18n.steps.personal_sub},
|
||||
<String, String>{'title': i18n.steps.address, 'subtitle': i18n.steps.address_sub},
|
||||
<String, String>{'title': i18n.steps.citizenship, 'subtitle': i18n.steps.citizenship_sub},
|
||||
<String, String>{'title': i18n.steps.review, 'subtitle': i18n.steps.review_sub},
|
||||
];
|
||||
|
||||
return BlocProvider<FormI9Cubit>.value(
|
||||
value: Modular.get<FormI9Cubit>(),
|
||||
child: BlocConsumer<FormI9Cubit, FormI9State>(
|
||||
@@ -83,34 +93,32 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
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'),
|
||||
),
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: translateErrorKey(state.errorMessage ?? 'An error occurred'),
|
||||
type: UiSnackbarType.error,
|
||||
margin: const EdgeInsets.only(left: 16, right: 16, bottom: 100),
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (BuildContext context, FormI9State state) {
|
||||
if (state.status == FormI9Status.success) return _buildSuccessView();
|
||||
if (state.status == FormI9Status.success) return _buildSuccessView(i18n);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: UiColors.background,
|
||||
body: Column(
|
||||
children: <Widget>[
|
||||
_buildHeader(context, state),
|
||||
_buildHeader(context, state, steps, i18n),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space5,
|
||||
vertical: UiConstants.space6,
|
||||
),
|
||||
child: _buildCurrentStep(context, state),
|
||||
child: _buildCurrentStep(context, state, i18n),
|
||||
),
|
||||
),
|
||||
_buildFooter(context, state),
|
||||
_buildFooter(context, state, steps),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -119,7 +127,7 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSuccessView() {
|
||||
Widget _buildSuccessView(TranslationsStaffComplianceTaxFormsI9En i18n) {
|
||||
return Scaffold(
|
||||
backgroundColor: UiColors.background,
|
||||
body: Center(
|
||||
@@ -150,12 +158,12 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
Text(
|
||||
'Form I-9 Submitted!',
|
||||
i18n.submitted_title,
|
||||
style: UiTypography.headline4m.textPrimary,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
Text(
|
||||
'Your employment eligibility verification has been submitted.',
|
||||
i18n.submitted_desc,
|
||||
textAlign: TextAlign.center,
|
||||
style: UiTypography.body2r.textSecondary,
|
||||
),
|
||||
@@ -175,7 +183,7 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
),
|
||||
elevation: 0,
|
||||
),
|
||||
child: const Text('Back to Documents'),
|
||||
child: Text(Translations.of(context).staff_compliance.tax_forms.w4.back_to_docs),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -186,7 +194,12 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader(BuildContext context, FormI9State state) {
|
||||
Widget _buildHeader(
|
||||
BuildContext context,
|
||||
FormI9State state,
|
||||
List<Map<String, String>> steps,
|
||||
TranslationsStaffComplianceTaxFormsI9En i18n,
|
||||
) {
|
||||
return Container(
|
||||
color: UiColors.primary,
|
||||
padding: const EdgeInsets.only(
|
||||
@@ -213,11 +226,11 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'Form I-9',
|
||||
i18n.title,
|
||||
style: UiTypography.headline4m.white,
|
||||
),
|
||||
Text(
|
||||
'Employment Eligibility Verification',
|
||||
i18n.subtitle,
|
||||
style: UiTypography.body3r.copyWith(
|
||||
color: UiColors.white.withValues(alpha: 0.7),
|
||||
),
|
||||
@@ -228,12 +241,12 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
Row(
|
||||
children: _steps
|
||||
children: steps
|
||||
.asMap()
|
||||
.entries
|
||||
.map((MapEntry<int, Map<String, String>> entry) {
|
||||
final int idx = entry.key;
|
||||
final bool isLast = idx == _steps.length - 1;
|
||||
final bool isLast = idx == steps.length - 1;
|
||||
return Expanded(
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
@@ -259,14 +272,17 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'Step ${state.currentStep + 1} of ${_steps.length}',
|
||||
i18n.step_label(
|
||||
current: (state.currentStep + 1).toString(),
|
||||
total: steps.length.toString(),
|
||||
),
|
||||
style: UiTypography.body3r.copyWith(
|
||||
color: UiColors.white.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_steps[state.currentStep]['title']!,
|
||||
steps[state.currentStep]['title']!,
|
||||
textAlign: TextAlign.end,
|
||||
style: UiTypography.body3m.white.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
@@ -280,16 +296,20 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCurrentStep(BuildContext context, FormI9State state) {
|
||||
Widget _buildCurrentStep(
|
||||
BuildContext context,
|
||||
FormI9State state,
|
||||
TranslationsStaffComplianceTaxFormsI9En i18n,
|
||||
) {
|
||||
switch (state.currentStep) {
|
||||
case 0:
|
||||
return _buildStep1(context, state);
|
||||
return _buildStep1(context, state, i18n);
|
||||
case 1:
|
||||
return _buildStep2(context, state);
|
||||
return _buildStep2(context, state, i18n);
|
||||
case 2:
|
||||
return _buildStep3(context, state);
|
||||
return _buildStep3(context, state, i18n);
|
||||
case 3:
|
||||
return _buildStep4(context, state);
|
||||
return _buildStep4(context, state, i18n);
|
||||
default:
|
||||
return Container();
|
||||
}
|
||||
@@ -347,26 +367,30 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStep1(BuildContext context, FormI9State state) {
|
||||
Widget _buildStep1(
|
||||
BuildContext context,
|
||||
FormI9State state,
|
||||
TranslationsStaffComplianceTaxFormsI9En i18n,
|
||||
) {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: _buildTextField(
|
||||
'First Name *',
|
||||
i18n.fields.first_name,
|
||||
value: state.firstName,
|
||||
onChanged: (String val) => context.read<FormI9Cubit>().firstNameChanged(val),
|
||||
placeholder: 'John',
|
||||
placeholder: i18n.fields.hints.first_name,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildTextField(
|
||||
'Last Name *',
|
||||
i18n.fields.last_name,
|
||||
value: state.lastName,
|
||||
onChanged: (String val) => context.read<FormI9Cubit>().lastNameChanged(val),
|
||||
placeholder: 'Smith',
|
||||
placeholder: i18n.fields.hints.last_name,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -376,37 +400,37 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: _buildTextField(
|
||||
'Middle Initial',
|
||||
i18n.fields.middle_initial,
|
||||
value: state.middleInitial,
|
||||
onChanged: (String val) => context.read<FormI9Cubit>().middleInitialChanged(val),
|
||||
placeholder: 'A',
|
||||
placeholder: i18n.fields.hints.middle_initial,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: _buildTextField(
|
||||
'Other Last Names',
|
||||
i18n.fields.other_last_names,
|
||||
value: state.otherLastNames,
|
||||
onChanged: (String val) => context.read<FormI9Cubit>().otherLastNamesChanged(val),
|
||||
placeholder: 'Maiden name (if any)',
|
||||
placeholder: i18n.fields.maiden_name,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildTextField(
|
||||
'Date of Birth *',
|
||||
i18n.fields.dob,
|
||||
value: state.dob,
|
||||
onChanged: (String val) => context.read<FormI9Cubit>().dobChanged(val),
|
||||
placeholder: 'MM/DD/YYYY',
|
||||
placeholder: i18n.fields.hints.dob,
|
||||
keyboardType: TextInputType.datetime,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildTextField(
|
||||
'Social Security Number *',
|
||||
i18n.fields.ssn,
|
||||
value: state.ssn,
|
||||
placeholder: 'XXX-XX-XXXX',
|
||||
placeholder: i18n.fields.hints.ssn,
|
||||
keyboardType: TextInputType.number,
|
||||
onChanged: (String val) {
|
||||
String text = val.replaceAll(RegExp(r'\D'), '');
|
||||
@@ -416,39 +440,43 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildTextField(
|
||||
'Email Address',
|
||||
i18n.fields.email,
|
||||
value: state.email,
|
||||
onChanged: (String val) => context.read<FormI9Cubit>().emailChanged(val),
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
placeholder: 'john.smith@example.com',
|
||||
placeholder: i18n.fields.hints.email,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildTextField(
|
||||
'Phone Number',
|
||||
i18n.fields.phone,
|
||||
value: state.phone,
|
||||
onChanged: (String val) => context.read<FormI9Cubit>().phoneChanged(val),
|
||||
keyboardType: TextInputType.phone,
|
||||
placeholder: '(555) 555-5555',
|
||||
placeholder: i18n.fields.hints.phone,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStep2(BuildContext context, FormI9State state) {
|
||||
Widget _buildStep2(
|
||||
BuildContext context,
|
||||
FormI9State state,
|
||||
TranslationsStaffComplianceTaxFormsI9En i18n,
|
||||
) {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
_buildTextField(
|
||||
'Address (Street Number and Name) *',
|
||||
i18n.fields.address_long,
|
||||
value: state.address,
|
||||
onChanged: (String val) => context.read<FormI9Cubit>().addressChanged(val),
|
||||
placeholder: '123 Main Street',
|
||||
placeholder: i18n.fields.hints.address,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildTextField(
|
||||
'Apt. Number',
|
||||
i18n.fields.apt,
|
||||
value: state.aptNumber,
|
||||
onChanged: (String val) => context.read<FormI9Cubit>().aptNumberChanged(val),
|
||||
placeholder: '4B',
|
||||
placeholder: i18n.fields.hints.apt,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
@@ -456,10 +484,10 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: _buildTextField(
|
||||
'City or Town *',
|
||||
i18n.fields.city,
|
||||
value: state.city,
|
||||
onChanged: (String val) => context.read<FormI9Cubit>().cityChanged(val),
|
||||
placeholder: 'San Francisco',
|
||||
placeholder: i18n.fields.hints.city,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
@@ -468,7 +496,7 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'State *',
|
||||
i18n.fields.state,
|
||||
style: UiTypography.body3m.textSecondary.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
@@ -507,22 +535,26 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildTextField(
|
||||
'ZIP Code *',
|
||||
i18n.fields.zip,
|
||||
value: state.zipCode,
|
||||
onChanged: (String val) => context.read<FormI9Cubit>().zipCodeChanged(val),
|
||||
placeholder: '94103',
|
||||
placeholder: i18n.fields.hints.zip,
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStep3(BuildContext context, FormI9State state) {
|
||||
Widget _buildStep3(
|
||||
BuildContext context,
|
||||
FormI9State state,
|
||||
TranslationsStaffComplianceTaxFormsI9En i18n,
|
||||
) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'I attest, under penalty of perjury, that I am (check one of the following boxes):',
|
||||
i18n.fields.attestation,
|
||||
style: UiTypography.body2m.textPrimary,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
@@ -530,29 +562,29 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
context,
|
||||
state,
|
||||
'CITIZEN',
|
||||
'1. A citizen of the United States',
|
||||
i18n.fields.citizen,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildRadioOption(
|
||||
context,
|
||||
state,
|
||||
'NONCITIZEN',
|
||||
'2. A noncitizen national of the United States',
|
||||
i18n.fields.noncitizen,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildRadioOption(
|
||||
context,
|
||||
state,
|
||||
'PERMANENT_RESIDENT',
|
||||
'3. A lawful permanent resident',
|
||||
i18n.fields.permanent_resident,
|
||||
child: state.citizenshipStatus == 'PERMANENT_RESIDENT'
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(top: 12),
|
||||
child: _buildTextField(
|
||||
'USCIS Number',
|
||||
i18n.fields.uscis_number_label,
|
||||
value: state.uscisNumber,
|
||||
onChanged: (String val) => context.read<FormI9Cubit>().uscisNumberChanged(val),
|
||||
placeholder: 'A-123456789',
|
||||
placeholder: i18n.fields.hints.uscis,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
@@ -562,26 +594,26 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
context,
|
||||
state,
|
||||
'ALIEN',
|
||||
'4. An alien authorized to work',
|
||||
i18n.fields.alien,
|
||||
child: state.citizenshipStatus == 'ALIEN'
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(top: 12),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
_buildTextField(
|
||||
'USCIS/Admission Number',
|
||||
i18n.fields.admission_number,
|
||||
value: state.admissionNumber,
|
||||
onChanged: (String val) => context.read<FormI9Cubit>().admissionNumberChanged(val),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildTextField(
|
||||
'Foreign Passport Number',
|
||||
i18n.fields.passport,
|
||||
value: state.passportNumber,
|
||||
onChanged: (String val) => context.read<FormI9Cubit>().passportNumberChanged(val),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildTextField(
|
||||
'Country of Issuance',
|
||||
i18n.fields.country,
|
||||
value: state.countryIssuance,
|
||||
onChanged: (String val) => context.read<FormI9Cubit>().countryIssuanceChanged(val),
|
||||
),
|
||||
@@ -645,7 +677,11 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStep4(BuildContext context, FormI9State state) {
|
||||
Widget _buildStep4(
|
||||
BuildContext context,
|
||||
FormI9State state,
|
||||
TranslationsStaffComplianceTaxFormsI9En i18n,
|
||||
) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
@@ -660,18 +696,18 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'Summary',
|
||||
i18n.fields.summary_title,
|
||||
style: UiTypography.headline4m.copyWith(fontSize: 14),
|
||||
),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
_buildSummaryRow('Name', '${state.firstName} ${state.lastName}'),
|
||||
_buildSummaryRow('Address', '${state.address}, ${state.city}'),
|
||||
_buildSummaryRow(i18n.fields.summary_name, '${state.firstName} ${state.lastName}'),
|
||||
_buildSummaryRow(i18n.fields.summary_address, '${state.address}, ${state.city}'),
|
||||
_buildSummaryRow(
|
||||
'SSN',
|
||||
i18n.fields.summary_ssn,
|
||||
'***-**-${state.ssn.length >= 4 ? state.ssn.substring(state.ssn.length - 4) : '****'}',
|
||||
),
|
||||
_buildSummaryRow(
|
||||
'Citizenship',
|
||||
i18n.fields.summary_citizenship,
|
||||
_getReadableCitizenship(state.citizenshipStatus),
|
||||
),
|
||||
],
|
||||
@@ -685,7 +721,7 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
},
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Text(
|
||||
'I used a preparer or translator',
|
||||
i18n.fields.preparer,
|
||||
style: UiTypography.body2r.textPrimary,
|
||||
),
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
@@ -699,13 +735,13 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
child: Text(
|
||||
'I am aware that federal law provides for imprisonment and/or fines for false statements or use of false documents in connection with the completion of this form.',
|
||||
i18n.fields.warning,
|
||||
style: UiTypography.body3r.textWarning.copyWith(fontSize: 12),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
Text(
|
||||
'Signature (type your full name) *',
|
||||
i18n.fields.signature_label,
|
||||
style: UiTypography.body3m.textSecondary,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
@@ -717,7 +753,7 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
onChanged: (String val) =>
|
||||
context.read<FormI9Cubit>().signatureChanged(val),
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Type your full name',
|
||||
hintText: i18n.fields.signature_hint,
|
||||
filled: true,
|
||||
fillColor: UiColors.bgPopup,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
@@ -741,7 +777,7 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
Text(
|
||||
'Date',
|
||||
i18n.fields.date_label,
|
||||
style: UiTypography.body3m.textSecondary,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
@@ -788,21 +824,28 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
}
|
||||
|
||||
String _getReadableCitizenship(String status) {
|
||||
final i18n = Translations.of(context).staff_compliance.tax_forms.i9.fields;
|
||||
switch (status) {
|
||||
case 'CITIZEN':
|
||||
return 'US Citizen';
|
||||
return i18n.status_us_citizen;
|
||||
case 'NONCITIZEN':
|
||||
return 'Noncitizen National';
|
||||
return i18n.status_noncitizen;
|
||||
case 'PERMANENT_RESIDENT':
|
||||
return 'Permanent Resident';
|
||||
return i18n.status_permanent_resident;
|
||||
case 'ALIEN':
|
||||
return 'Alien Authorized to Work';
|
||||
return i18n.status_alien;
|
||||
default:
|
||||
return 'Unknown';
|
||||
return i18n.status_unknown;
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildFooter(BuildContext context, FormI9State state) {
|
||||
Widget _buildFooter(
|
||||
BuildContext context,
|
||||
FormI9State state,
|
||||
List<Map<String, String>> steps,
|
||||
) {
|
||||
final i18n = Translations.of(context).staff_compliance.tax_forms.i9;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: const BoxDecoration(
|
||||
@@ -837,7 +880,7 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Back',
|
||||
i18n.back,
|
||||
style: UiTypography.body2r.textPrimary,
|
||||
),
|
||||
],
|
||||
@@ -878,11 +921,11 @@ class _FormI9PageState extends State<FormI9Page> {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
state.currentStep == _steps.length - 1
|
||||
? 'Sign & Submit'
|
||||
: 'Continue',
|
||||
state.currentStep == steps.length - 1
|
||||
? i18n.submit
|
||||
: i18n.kContinue,
|
||||
),
|
||||
if (state.currentStep < _steps.length - 1) ...<Widget>[
|
||||
if (state.currentStep < steps.length - 1) ...<Widget>[
|
||||
const SizedBox(width: 8),
|
||||
const Icon(UiIcons.arrowRight, size: 16, color: UiColors.white),
|
||||
],
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart' hide ModularWatchExtension;
|
||||
@@ -122,6 +123,17 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final i18n = Translations.of(context).staff_compliance.tax_forms.w4;
|
||||
|
||||
final List<Map<String, String>> steps = <Map<String, String>>[
|
||||
<String, String>{'title': i18n.steps.personal, 'subtitle': i18n.step_label(current: '1', total: '5')},
|
||||
<String, String>{'title': i18n.steps.filing, 'subtitle': i18n.step_label(current: '1c', total: '5')},
|
||||
<String, String>{'title': i18n.steps.multiple_jobs, 'subtitle': i18n.step_label(current: '2', total: '5')},
|
||||
<String, String>{'title': i18n.steps.dependents, 'subtitle': i18n.step_label(current: '3', total: '5')},
|
||||
<String, String>{'title': i18n.steps.adjustments, 'subtitle': i18n.step_label(current: '4', total: '5')},
|
||||
<String, String>{'title': i18n.steps.review, 'subtitle': i18n.step_label(current: '5', total: '5')},
|
||||
];
|
||||
|
||||
return BlocProvider<FormW4Cubit>.value(
|
||||
value: Modular.get<FormW4Cubit>(),
|
||||
child: BlocConsumer<FormW4Cubit, FormW4State>(
|
||||
@@ -129,31 +141,32 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
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')),
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: translateErrorKey(state.errorMessage ?? 'An error occurred'),
|
||||
type: UiSnackbarType.error,
|
||||
margin: const EdgeInsets.only(left: 16, right: 16, bottom: 100),
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (BuildContext context, FormW4State state) {
|
||||
if (state.status == FormW4Status.success) return _buildSuccessView();
|
||||
if (state.status == FormW4Status.success) return _buildSuccessView(i18n);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: UiColors.background,
|
||||
body: Column(
|
||||
children: <Widget>[
|
||||
_buildHeader(context, state),
|
||||
_buildHeader(context, state, steps, i18n),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space5,
|
||||
vertical: UiConstants.space6,
|
||||
),
|
||||
child: _buildCurrentStep(context, state),
|
||||
child: _buildCurrentStep(context, state, i18n),
|
||||
),
|
||||
),
|
||||
_buildFooter(context, state),
|
||||
_buildFooter(context, state, steps),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -162,7 +175,7 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSuccessView() {
|
||||
Widget _buildSuccessView(TranslationsStaffComplianceTaxFormsW4En i18n) {
|
||||
return Scaffold(
|
||||
backgroundColor: UiColors.background,
|
||||
body: Center(
|
||||
@@ -193,12 +206,12 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
Text(
|
||||
'Form W-4 Submitted!',
|
||||
i18n.submitted_title,
|
||||
style: UiTypography.headline4m.textPrimary,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
Text(
|
||||
'Your withholding certificate has been submitted to your employer.',
|
||||
i18n.submitted_desc,
|
||||
textAlign: TextAlign.center,
|
||||
style: UiTypography.body2r.textSecondary,
|
||||
),
|
||||
@@ -218,7 +231,7 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
),
|
||||
elevation: 0,
|
||||
),
|
||||
child: const Text('Back to Documents'),
|
||||
child: Text(i18n.back_to_docs),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -229,7 +242,12 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader(BuildContext context, FormW4State state) {
|
||||
Widget _buildHeader(
|
||||
BuildContext context,
|
||||
FormW4State state,
|
||||
List<Map<String, String>> steps,
|
||||
TranslationsStaffComplianceTaxFormsW4En i18n,
|
||||
) {
|
||||
return Container(
|
||||
color: UiColors.primary,
|
||||
padding: const EdgeInsets.only(
|
||||
@@ -256,11 +274,11 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'Form W-4',
|
||||
i18n.title,
|
||||
style: UiTypography.headline4m.white,
|
||||
),
|
||||
Text(
|
||||
'Employee\'s Withholding Certificate',
|
||||
i18n.subtitle,
|
||||
style: UiTypography.body3r.copyWith(
|
||||
color: UiColors.white.withValues(alpha: 0.7),
|
||||
),
|
||||
@@ -271,12 +289,12 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
Row(
|
||||
children: _steps
|
||||
children: steps
|
||||
.asMap()
|
||||
.entries
|
||||
.map((MapEntry<int, Map<String, String>> entry) {
|
||||
final int idx = entry.key;
|
||||
final bool isLast = idx == _steps.length - 1;
|
||||
final bool isLast = idx == steps.length - 1;
|
||||
return Expanded(
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
@@ -302,13 +320,16 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'Step ${state.currentStep + 1} of ${_steps.length}',
|
||||
i18n.step_label(
|
||||
current: (state.currentStep + 1).toString(),
|
||||
total: steps.length.toString(),
|
||||
),
|
||||
style: UiTypography.body3r.copyWith(
|
||||
color: UiColors.white.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_steps[state.currentStep]['title']!,
|
||||
steps[state.currentStep]['title']!,
|
||||
style: UiTypography.body3m.white.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
@@ -320,20 +341,24 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCurrentStep(BuildContext context, FormW4State state) {
|
||||
Widget _buildCurrentStep(
|
||||
BuildContext context,
|
||||
FormW4State state,
|
||||
TranslationsStaffComplianceTaxFormsW4En i18n,
|
||||
) {
|
||||
switch (state.currentStep) {
|
||||
case 0:
|
||||
return _buildStep1(context, state);
|
||||
return _buildStep1(context, state, i18n);
|
||||
case 1:
|
||||
return _buildStep2(context, state);
|
||||
return _buildStep2(context, state, i18n);
|
||||
case 2:
|
||||
return _buildStep3(context, state);
|
||||
return _buildStep3(context, state, i18n);
|
||||
case 3:
|
||||
return _buildStep4(context, state);
|
||||
return _buildStep4(context, state, i18n);
|
||||
case 4:
|
||||
return _buildStep5(context, state);
|
||||
return _buildStep5(context, state, i18n);
|
||||
case 5:
|
||||
return _buildStep6(context, state);
|
||||
return _buildStep6(context, state, i18n);
|
||||
default:
|
||||
return Container();
|
||||
}
|
||||
@@ -391,35 +416,39 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStep1(BuildContext context, FormW4State state) {
|
||||
Widget _buildStep1(
|
||||
BuildContext context,
|
||||
FormW4State state,
|
||||
TranslationsStaffComplianceTaxFormsW4En i18n,
|
||||
) {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: _buildTextField(
|
||||
'First Name *',
|
||||
i18n.fields.first_name,
|
||||
value: state.firstName,
|
||||
onChanged: (String val) => context.read<FormW4Cubit>().firstNameChanged(val),
|
||||
placeholder: 'John',
|
||||
placeholder: i18n.fields.placeholder_john,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildTextField(
|
||||
'Last Name *',
|
||||
i18n.fields.last_name,
|
||||
value: state.lastName,
|
||||
onChanged: (String val) => context.read<FormW4Cubit>().lastNameChanged(val),
|
||||
placeholder: 'Smith',
|
||||
placeholder: i18n.fields.placeholder_smith,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildTextField(
|
||||
'Social Security Number *',
|
||||
i18n.fields.ssn,
|
||||
value: state.ssn,
|
||||
placeholder: 'XXX-XX-XXXX',
|
||||
placeholder: i18n.fields.placeholder_ssn,
|
||||
keyboardType: TextInputType.number,
|
||||
onChanged: (String val) {
|
||||
String text = val.replaceAll(RegExp(r'\D'), '');
|
||||
@@ -429,23 +458,27 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildTextField(
|
||||
'Address *',
|
||||
i18n.fields.address,
|
||||
value: state.address,
|
||||
onChanged: (String val) => context.read<FormW4Cubit>().addressChanged(val),
|
||||
placeholder: '123 Main Street',
|
||||
placeholder: i18n.fields.placeholder_address,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildTextField(
|
||||
'City, State, ZIP',
|
||||
i18n.fields.city_state_zip,
|
||||
value: state.cityStateZip,
|
||||
onChanged: (String val) => context.read<FormW4Cubit>().cityStateZipChanged(val),
|
||||
placeholder: 'San Francisco, CA 94102',
|
||||
placeholder: i18n.fields.placeholder_csz,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStep2(BuildContext context, FormW4State state) {
|
||||
Widget _buildStep2(
|
||||
BuildContext context,
|
||||
FormW4State state,
|
||||
TranslationsStaffComplianceTaxFormsW4En i18n,
|
||||
) {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
@@ -460,7 +493,7 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Your filing status determines your standard deduction and tax rates.',
|
||||
i18n.fields.filing_info,
|
||||
style: UiTypography.body2r.textPrimary,
|
||||
),
|
||||
),
|
||||
@@ -472,7 +505,7 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
context,
|
||||
state,
|
||||
'SINGLE',
|
||||
'Single or Married filing separately',
|
||||
i18n.fields.single,
|
||||
null,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
@@ -480,7 +513,7 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
context,
|
||||
state,
|
||||
'MARRIED',
|
||||
'Married filing jointly or Qualifying surviving spouse',
|
||||
i18n.fields.married,
|
||||
null,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
@@ -488,8 +521,8 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
context,
|
||||
state,
|
||||
'HEAD',
|
||||
'Head of household',
|
||||
'Check only if you\'re unmarried and pay more than half the costs of keeping up a home',
|
||||
i18n.fields.head,
|
||||
i18n.fields.head_desc,
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -555,7 +588,11 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStep3(BuildContext context, FormW4State state) {
|
||||
Widget _buildStep3(
|
||||
BuildContext context,
|
||||
FormW4State state,
|
||||
TranslationsStaffComplianceTaxFormsW4En i18n,
|
||||
) {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
@@ -578,12 +615,12 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'When to complete this step?',
|
||||
i18n.fields.multiple_jobs_title,
|
||||
style: UiTypography.body2m.accent,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Complete this step only if you hold more than one job at a time, or are married filing jointly and your spouse also works.',
|
||||
i18n.fields.multiple_jobs_desc,
|
||||
style: UiTypography.body3r.accent,
|
||||
),
|
||||
],
|
||||
@@ -632,12 +669,12 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'I have multiple jobs or my spouse works',
|
||||
i18n.fields.multiple_jobs_check,
|
||||
style: UiTypography.body2m.textPrimary,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Check this box if there are only two jobs total',
|
||||
i18n.fields.two_jobs_desc,
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
),
|
||||
],
|
||||
@@ -649,7 +686,7 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'If this does not apply, you can continue to the next step',
|
||||
i18n.fields.multiple_jobs_not_apply,
|
||||
textAlign: TextAlign.center,
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
),
|
||||
@@ -657,7 +694,11 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStep4(BuildContext context, FormW4State state) {
|
||||
Widget _buildStep4(
|
||||
BuildContext context,
|
||||
FormW4State state,
|
||||
TranslationsStaffComplianceTaxFormsW4En i18n,
|
||||
) {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
@@ -672,7 +713,7 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'If your total income will be \$200,000 or less (\$400,000 if married filing jointly), you may claim credits for dependents.',
|
||||
i18n.fields.dependents_info,
|
||||
style: UiTypography.body2r.textPrimary,
|
||||
),
|
||||
),
|
||||
@@ -692,8 +733,8 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
_buildCounter(
|
||||
context,
|
||||
state,
|
||||
'Qualifying children under age 17',
|
||||
'\$2,000 each',
|
||||
i18n.fields.children_under_17,
|
||||
i18n.fields.children_each,
|
||||
(FormW4State s) => s.qualifyingChildren,
|
||||
(int val) => context.read<FormW4Cubit>().qualifyingChildrenChanged(val),
|
||||
),
|
||||
@@ -704,8 +745,8 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
_buildCounter(
|
||||
context,
|
||||
state,
|
||||
'Other dependents',
|
||||
'\$500 each',
|
||||
i18n.fields.other_dependents,
|
||||
i18n.fields.other_each,
|
||||
(FormW4State s) => s.otherDependents,
|
||||
(int val) => context.read<FormW4Cubit>().otherDependentsChanged(val),
|
||||
),
|
||||
@@ -723,9 +764,9 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
const Text(
|
||||
'Total credits (Step 3)',
|
||||
style: TextStyle(
|
||||
Text(
|
||||
i18n.fields.total_credits,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF166534),
|
||||
),
|
||||
@@ -824,56 +865,60 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStep5(BuildContext context, FormW4State state) {
|
||||
Widget _buildStep5(
|
||||
BuildContext context,
|
||||
FormW4State state,
|
||||
TranslationsStaffComplianceTaxFormsW4En i18n,
|
||||
) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'These adjustments are optional. You can skip them if they don\'t apply.',
|
||||
i18n.fields.adjustments_info,
|
||||
style: UiTypography.body2r.textSecondary,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
_buildTextField(
|
||||
'4(a) Other income (not from jobs)',
|
||||
i18n.fields.other_income,
|
||||
value: state.otherIncome,
|
||||
onChanged: (String val) => context.read<FormW4Cubit>().otherIncomeChanged(val),
|
||||
placeholder: '\$0',
|
||||
placeholder: i18n.fields.hints.zero,
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4, bottom: 16),
|
||||
child: Text(
|
||||
'Include interest, dividends, retirement income',
|
||||
i18n.fields.other_income_desc,
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
),
|
||||
),
|
||||
|
||||
_buildTextField(
|
||||
'4(b) Deductions',
|
||||
i18n.fields.deductions,
|
||||
value: state.deductions,
|
||||
onChanged: (String val) => context.read<FormW4Cubit>().deductionsChanged(val),
|
||||
placeholder: '\$0',
|
||||
placeholder: i18n.fields.hints.zero,
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4, bottom: 16),
|
||||
child: Text(
|
||||
'If you expect to claim deductions other than the standard deduction',
|
||||
i18n.fields.deductions_desc,
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
),
|
||||
),
|
||||
|
||||
_buildTextField(
|
||||
'4(c) Extra withholding',
|
||||
i18n.fields.extra_withholding,
|
||||
value: state.extraWithholding,
|
||||
onChanged: (String val) => context.read<FormW4Cubit>().extraWithholdingChanged(val),
|
||||
placeholder: '\$0',
|
||||
placeholder: i18n.fields.hints.zero,
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4, bottom: 16),
|
||||
child: Text(
|
||||
'Any additional tax you want withheld each pay period',
|
||||
i18n.fields.extra_withholding_desc,
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
),
|
||||
),
|
||||
@@ -881,7 +926,11 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStep6(BuildContext context, FormW4State state) {
|
||||
Widget _buildStep6(
|
||||
BuildContext context,
|
||||
FormW4State state,
|
||||
TranslationsStaffComplianceTaxFormsW4En i18n,
|
||||
) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
@@ -896,25 +945,25 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'Your W-4 Summary',
|
||||
i18n.fields.summary_title,
|
||||
style: UiTypography.headline4m.copyWith(fontSize: 14),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildSummaryRow(
|
||||
'Name',
|
||||
i18n.fields.summary_name,
|
||||
'${state.firstName} ${state.lastName}',
|
||||
),
|
||||
_buildSummaryRow(
|
||||
'SSN',
|
||||
i18n.fields.summary_ssn,
|
||||
'***-**-${state.ssn.length >= 4 ? state.ssn.substring(state.ssn.length - 4) : '****'}',
|
||||
),
|
||||
_buildSummaryRow(
|
||||
'Filing Status',
|
||||
i18n.fields.summary_filing,
|
||||
_getFilingStatusLabel(state.filingStatus),
|
||||
),
|
||||
if (_totalCredits(state) > 0)
|
||||
_buildSummaryRow(
|
||||
'Credits',
|
||||
i18n.fields.summary_credits,
|
||||
'\$${_totalCredits(state)}',
|
||||
valueColor: Colors.green[700],
|
||||
),
|
||||
@@ -929,13 +978,13 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
child: Text(
|
||||
'Under penalties of perjury, I declare that this certificate, to the best of my knowledge and belief, is true, correct, and complete.',
|
||||
i18n.fields.perjury_declaration,
|
||||
style: UiTypography.body3r.textWarning.copyWith(fontSize: 12),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: UiConstants.space6),
|
||||
Text(
|
||||
'Signature (type your full name) *',
|
||||
i18n.fields.signature_label,
|
||||
style: UiTypography.body3m.textSecondary,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
@@ -947,7 +996,7 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
onChanged: (String val) =>
|
||||
context.read<FormW4Cubit>().signatureChanged(val),
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Type your full name',
|
||||
hintText: i18n.fields.signature_hint,
|
||||
filled: true,
|
||||
fillColor: UiColors.bgPopup,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
@@ -971,7 +1020,7 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
Text(
|
||||
'Date',
|
||||
i18n.fields.date_label,
|
||||
style: UiTypography.body3m.textSecondary,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
@@ -1017,19 +1066,26 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
}
|
||||
|
||||
String _getFilingStatusLabel(String status) {
|
||||
final i18n = Translations.of(context).staff_compliance.tax_forms.w4.fields;
|
||||
switch (status) {
|
||||
case 'single':
|
||||
return 'Single';
|
||||
case 'married':
|
||||
return 'Married';
|
||||
case 'head_of_household':
|
||||
return 'Head of Household';
|
||||
case 'SINGLE':
|
||||
return i18n.status_single;
|
||||
case 'MARRIED':
|
||||
return i18n.status_married;
|
||||
case 'HEAD':
|
||||
return i18n.status_head;
|
||||
default:
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildFooter(BuildContext context, FormW4State state) {
|
||||
Widget _buildFooter(
|
||||
BuildContext context,
|
||||
FormW4State state,
|
||||
List<Map<String, String>> steps,
|
||||
) {
|
||||
final i18n = Translations.of(context).staff_compliance.tax_forms.w4;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: const BoxDecoration(
|
||||
@@ -1064,7 +1120,7 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Back',
|
||||
i18n.fields.back,
|
||||
style: UiTypography.body2r.textPrimary,
|
||||
),
|
||||
],
|
||||
@@ -1105,11 +1161,11 @@ class _FormW4PageState extends State<FormW4Page> {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
state.currentStep == _steps.length - 1
|
||||
? 'Submit Form'
|
||||
: 'Continue',
|
||||
state.currentStep == steps.length - 1
|
||||
? i18n.fields.submit
|
||||
: i18n.fields.kContinue,
|
||||
),
|
||||
if (state.currentStep < _steps.length - 1) ...<Widget>[
|
||||
if (state.currentStep < steps.length - 1) ...<Widget>[
|
||||
const SizedBox(width: 8),
|
||||
const Icon(UiIcons.arrowRight, size: 16, color: UiColors.white),
|
||||
],
|
||||
|
||||
@@ -48,29 +48,14 @@ class BankAccountPage extends StatelessWidget {
|
||||
bloc: cubit,
|
||||
listener: (BuildContext context, BankAccountState state) {
|
||||
if (state.status == BankAccountStatus.accountAdded) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
strings.account_added_success,
|
||||
style: UiTypography.body2r.textPrimary,
|
||||
),
|
||||
backgroundColor: UiColors.tagSuccess,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
} else if (state.status == BankAccountStatus.error) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
state.errorMessage != null
|
||||
? translateErrorKey(state.errorMessage!)
|
||||
: 'An error occurred',
|
||||
),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: strings.account_added_success,
|
||||
type: UiSnackbarType.success,
|
||||
margin: const EdgeInsets.only(bottom: 120, left: 16, right: 16),
|
||||
);
|
||||
}
|
||||
// Error is already shown on the page itself (lines 73-85), no need for snackbar
|
||||
},
|
||||
builder: (BuildContext context, BankAccountState state) {
|
||||
if (state.status == BankAccountStatus.loading && state.accounts.isEmpty) {
|
||||
|
||||
@@ -19,6 +19,25 @@ class _AddAccountFormState extends State<AddAccountForm> {
|
||||
final TextEditingController _routingController = TextEditingController();
|
||||
final TextEditingController _accountController = TextEditingController();
|
||||
String _selectedType = 'CHECKING';
|
||||
bool _isFormValid = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_bankNameController.addListener(_validateForm);
|
||||
_routingController.addListener(_validateForm);
|
||||
_accountController.addListener(_validateForm);
|
||||
}
|
||||
|
||||
void _validateForm() {
|
||||
setState(() {
|
||||
_isFormValid = _bankNameController.text.trim().isNotEmpty &&
|
||||
_routingController.text.trim().isNotEmpty &&
|
||||
_routingController.text.replaceAll(RegExp(r'\D'), '').length == 9 &&
|
||||
_accountController.text.trim().isNotEmpty &&
|
||||
_accountController.text.replaceAll(RegExp(r'\D'), '').length >= 4;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
@@ -96,14 +115,16 @@ class _AddAccountFormState extends State<AddAccountForm> {
|
||||
Expanded(
|
||||
child: UiButton.primary(
|
||||
text: widget.strings.save,
|
||||
onPressed: () {
|
||||
widget.onSubmit(
|
||||
_bankNameController.text,
|
||||
_routingController.text,
|
||||
_accountController.text,
|
||||
_selectedType,
|
||||
);
|
||||
},
|
||||
onPressed: _isFormValid
|
||||
? () {
|
||||
widget.onSubmit(
|
||||
_bankNameController.text.trim(),
|
||||
_routingController.text.trim(),
|
||||
_accountController.text.trim(),
|
||||
_selectedType,
|
||||
);
|
||||
}
|
||||
: null,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -51,13 +51,10 @@ class _TimeCardPageState extends State<TimeCardPage> {
|
||||
body: BlocConsumer<TimeCardBloc, TimeCardState>(
|
||||
listener: (context, state) {
|
||||
if (state is TimeCardError) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
translateErrorKey(state.message),
|
||||
),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: translateErrorKey(state.message),
|
||||
type: UiSnackbarType.error,
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -42,8 +42,11 @@ class AttirePage extends StatelessWidget {
|
||||
body: BlocConsumer<AttireCubit, AttireState>(
|
||||
listener: (BuildContext context, AttireState state) {
|
||||
if (state.status == AttireStatus.failure) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(state.errorMessage ?? 'Error')),
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: translateErrorKey(state.errorMessage ?? 'Error'),
|
||||
type: UiSnackbarType.error,
|
||||
margin: const EdgeInsets.only(bottom: 150, left: 16, right: 16),
|
||||
);
|
||||
}
|
||||
if (state.status == AttireStatus.saved) {
|
||||
|
||||
@@ -42,15 +42,13 @@ class EmergencyContactScreen extends StatelessWidget {
|
||||
|
||||
listener: (context, state) {
|
||||
if (state.status == EmergencyContactStatus.failure) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
state.errorMessage != null
|
||||
? translateErrorKey(state.errorMessage!)
|
||||
: 'An error occurred',
|
||||
),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: state.errorMessage != null
|
||||
? translateErrorKey(state.errorMessage!)
|
||||
: 'An error occurred',
|
||||
type: UiSnackbarType.error,
|
||||
margin: const EdgeInsets.only(bottom: 150, left: 16, right: 16),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -16,14 +16,11 @@ class EmergencyContactSaveButton extends StatelessWidget {
|
||||
listenWhen: (previous, current) => previous.status != current.status,
|
||||
listener: (context, state) {
|
||||
if (state.status == EmergencyContactStatus.saved) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'Emergency contacts saved successfully',
|
||||
style: UiTypography.body2r.textPrimary,
|
||||
),
|
||||
backgroundColor: UiColors.iconSuccess,
|
||||
),
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: 'Emergency contacts saved successfully',
|
||||
type: UiSnackbarType.success,
|
||||
margin: const EdgeInsets.only(bottom: 150, left: 16, right: 16),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -58,20 +58,21 @@ class ExperiencePage extends StatelessWidget {
|
||||
child: BlocConsumer<ExperienceBloc, ExperienceState>(
|
||||
listener: (context, state) {
|
||||
if (state.status == ExperienceStatus.success) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Experience saved successfully')),
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: 'Experience saved successfully',
|
||||
type: UiSnackbarType.success,
|
||||
margin: const EdgeInsets.only(bottom: 120, left: 16, right: 16),
|
||||
);
|
||||
Modular.to.pop();
|
||||
} else if (state.status == ExperienceStatus.failure) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
state.errorMessage != null
|
||||
? translateErrorKey(state.errorMessage!)
|
||||
: 'An error occurred',
|
||||
),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: state.errorMessage != null
|
||||
? translateErrorKey(state.errorMessage!)
|
||||
: 'An error occurred',
|
||||
type: UiSnackbarType.error,
|
||||
margin: const EdgeInsets.only(bottom: 120, left: 16, right: 16),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -14,7 +14,7 @@ import 'personal_info_state.dart';
|
||||
/// during onboarding or profile editing. It delegates business logic to
|
||||
/// use cases following Clean Architecture principles.
|
||||
class PersonalInfoBloc extends Bloc<PersonalInfoEvent, PersonalInfoState>
|
||||
with BlocErrorHandler<PersonalInfoState>
|
||||
with BlocErrorHandler<PersonalInfoState>, SafeBloc<PersonalInfoEvent, PersonalInfoState>
|
||||
implements Disposable {
|
||||
/// Creates a [PersonalInfoBloc].
|
||||
///
|
||||
@@ -54,8 +54,8 @@ class PersonalInfoBloc extends Bloc<PersonalInfoEvent, PersonalInfoState>
|
||||
'phone': staff.phone,
|
||||
'preferredLocations':
|
||||
staff.address != null
|
||||
? <String?>[staff.address]
|
||||
: <dynamic>[], // TODO: Map correctly when Staff entity supports list
|
||||
? <String>[staff.address!]
|
||||
: <String>[], // TODO: Map correctly when Staff entity supports list
|
||||
'avatar': staff.avatar,
|
||||
};
|
||||
|
||||
@@ -109,8 +109,8 @@ class PersonalInfoBloc extends Bloc<PersonalInfoEvent, PersonalInfoState>
|
||||
'phone': updatedStaff.phone,
|
||||
'preferredLocations':
|
||||
updatedStaff.address != null
|
||||
? <String?>[updatedStaff.address]
|
||||
: <dynamic>[],
|
||||
? <String>[updatedStaff.address!]
|
||||
: <String>[],
|
||||
'avatar': updatedStaff.avatar,
|
||||
};
|
||||
|
||||
|
||||
@@ -28,24 +28,19 @@ class PersonalInfoPage extends StatelessWidget {
|
||||
child: BlocListener<PersonalInfoBloc, PersonalInfoState>(
|
||||
listener: (BuildContext context, PersonalInfoState state) {
|
||||
if (state.status == PersonalInfoStatus.saved) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(i18n.save_success),
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: i18n.save_success,
|
||||
type: UiSnackbarType.success,
|
||||
);
|
||||
Modular.to.pop();
|
||||
} else if (state.status == PersonalInfoStatus.error) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
state.errorMessage != null
|
||||
? translateErrorKey(state.errorMessage!)
|
||||
: 'An error occurred',
|
||||
),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
UiSnackbar.show(
|
||||
context,
|
||||
message: state.errorMessage != null
|
||||
? translateErrorKey(state.errorMessage!)
|
||||
: 'An error occurred',
|
||||
type: UiSnackbarType.error,
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user