feat: Refactor code structure and optimize performance across multiple modules

This commit is contained in:
Achintha Isuru
2025-11-17 23:29:28 -05:00
parent 831570f2e0
commit a64cbd9edf
1508 changed files with 105319 additions and 0 deletions

View File

@@ -0,0 +1,24 @@
import 'package:injectable/injectable.dart';
import 'package:krow/features/profile/wages_forms/domain/entities/edd_form_entity.dart';
import 'package:krow/features/profile/wages_forms/domain/entities/i_nine_entity.dart';
import 'package:krow/features/profile/wages_forms/domain/entities/offer_letter_entity.dart';
import 'package:krow/features/profile/wages_forms/domain/entities/w_four_entity.dart';
import 'package:krow/features/profile/wages_forms/domain/entities/wage_form_entity.dart';
@injectable
class WagesFormApiProvider {
Future<List<WageFormEntity>> fetchActualForms() async {
await Future.delayed(const Duration(seconds: 1));
return [
const EddFormEntity.empty(),
const INineEntity.empty(),
const OfferLetterEntity.empty(),
const WFourEntity.empty(),
];
}
Future<void> submitWageForm(WageFormEntity formData) async {
await Future.delayed(const Duration(seconds: 1));
}
}

View File

@@ -0,0 +1,22 @@
import 'package:injectable/injectable.dart';
import 'package:krow/features/profile/wages_forms/data/wages_form_api_provider.dart';
import 'package:krow/features/profile/wages_forms/domain/entities/wage_form_entity.dart';
import 'package:krow/features/profile/wages_forms/domain/wages_forms_repository.dart';
@Injectable(as: WagesFormsRepository)
class WagesFormsRepositoryImpl implements WagesFormsRepository {
WagesFormsRepositoryImpl({required WagesFormApiProvider apiProvider})
: _apiProvider = apiProvider;
final WagesFormApiProvider _apiProvider;
@override
Future<List<WageFormEntity>> getActualForms() async {
return _apiProvider.fetchActualForms();
}
@override
Future<void> submitWageForm({required WageFormEntity form}) {
return _apiProvider.submitWageForm(form);
}
}

View File

@@ -0,0 +1,106 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow/core/application/di/injectable.dart';
import 'package:krow/core/data/enums/state_status.dart';
import 'package:krow/features/profile/wages_forms/domain/entities/wage_form_entity.dart';
import 'package:krow/features/profile/wages_forms/domain/utils/validator_wage_from_visitor.dart';
import 'package:krow/features/profile/wages_forms/domain/wages_forms_repository.dart';
part 'wages_form_event.dart';
part 'wages_form_state.dart';
part 'wages_form_fields.dart';
class WagesFormBloc extends Bloc<WagesFormEvent, WagesFormState> {
WagesFormBloc() : super(const WagesFormState()) {
on<InitializeWagesFormBloc>(_onInitial);
on<SetActiveForm>(_onSetActiveForm);
on<AddUserSignImage>(_onAddUserSign);
on<UpdateCurrentForm>(_onUpdateCurrentForm);
on<ValidateCurrentForm>(_onValidateCurrentForm);
on<SubmitCurrentForm>(_onSubmitCurrentForm);
}
final _repository = getIt<WagesFormsRepository>();
final _validatorVisitor = const ValidatorWageFromVisitor();
Future<void> _onInitial(
InitializeWagesFormBloc event,
Emitter<WagesFormState> emit,
) async {
final forms = await _repository.getActualForms();
emit(
state.copyWith(
status: StateStatus.idle,
actualForms: forms,
),
);
}
void _onSetActiveForm(SetActiveForm event, Emitter<WagesFormState> emit) {
emit(state.copyWith(activeForm: event.form));
}
void _onAddUserSign(AddUserSignImage event, Emitter<WagesFormState> emit) {
emit(state.copyWith(signImageData: event.imageData));
}
void _onUpdateCurrentForm(
UpdateCurrentForm event,
Emitter<WagesFormState> emit,
) {
emit(state.copyWith(activeForm: event.form));
final index = state.actualForms.indexWhere(
(item) => item.runtimeType == event.form.runtimeType,
);
if (index < 0) return;
emit(
state.copyWith(
actualForms: List.from(state.actualForms)..[index] = event.form,
),
);
}
void _onValidateCurrentForm(
ValidateCurrentForm event,
Emitter<WagesFormState> emit,
) {
final formErrors = state.activeForm?.visit(_validatorVisitor) ?? {};
final isSignAdded = state.signImageData != null;
emit(
state.copyWith(
status: formErrors.isEmpty && isSignAdded
? StateStatus.success
: StateStatus.error,
formErrors: formErrors,
),
);
emit(state.copyWith(status: StateStatus.idle, formErrors: {}));
}
void _onSubmitCurrentForm(
SubmitCurrentForm event,
Emitter<WagesFormState> emit,
) async {
final form = state.activeForm;
if (form == null) return;
emit(state.copyWith(status: StateStatus.loading));
await _repository.submitWageForm(form: form);
emit(state.copyWith(status: StateStatus.success));
}
}

View File

@@ -0,0 +1,36 @@
part of 'wages_form_bloc.dart';
@immutable
sealed class WagesFormEvent {
const WagesFormEvent();
}
class InitializeWagesFormBloc extends WagesFormEvent {
const InitializeWagesFormBloc();
}
class SetActiveForm extends WagesFormEvent {
const SetActiveForm({required this.form});
final WageFormEntity form;
}
class AddUserSignImage extends WagesFormEvent {
const AddUserSignImage({required this.imageData});
final Uint8List imageData;
}
class UpdateCurrentForm extends WagesFormEvent {
const UpdateCurrentForm({required this.form});
final WageFormEntity form;
}
class ValidateCurrentForm extends WagesFormEvent {
const ValidateCurrentForm();
}
class SubmitCurrentForm extends WagesFormEvent {
const SubmitCurrentForm();
}

View File

@@ -0,0 +1,70 @@
part of 'wages_form_bloc.dart';
abstract class WagesFormFields {
static const fullName = 'fullName';
static const city = 'city';
static const lastName = 'lastName';
static const firstName = 'firstName';
static const middleName = 'middleName';
static const address = 'address';
static const state = 'state';
static const zipCode = 'zipCode';
static const birthdate = 'birthdate';
static const socialNumber = 'socialNumber';
static const email = 'email';
static const phone = 'phone';
}
const usStates = [
'Alabama',
'Alaska',
'Arizona',
'Arkansas',
'California',
'Colorado',
'Connecticut',
'Delaware',
'District of Columbia',
'Florida',
'Georgia',
'Hawaii',
'Idaho',
'Illinois',
'Indiana',
'Iowa',
'Kansas',
'Kentucky',
'Louisiana',
'Maine',
'Montana',
'Nebraska',
'Nevada',
'New Hampshire',
'New Jersey',
'New Mexico',
'New York',
'North Carolina',
'North Dakota',
'Ohio',
'Oklahoma',
'Oregon',
'Maryland',
'Massachusetts',
'Michigan',
'Minnesota',
'Mississippi',
'Missouri',
'Pennsylvania',
'Rhode Island',
'South Carolina',
'South Dakota',
'Tennessee',
'Texas',
'Utah',
'Vermont',
'Virginia',
'Washington',
'West Virginia',
'Wisconsin',
'Wyoming',
];

View File

@@ -0,0 +1,34 @@
part of 'wages_form_bloc.dart';
@immutable
class WagesFormState {
const WagesFormState({
this.status = StateStatus.loading,
this.signImageData,
this.actualForms = const [],
this.activeForm,
this.formErrors = const {},
});
final StateStatus status;
final Uint8List? signImageData;
final List<WageFormEntity> actualForms;
final WageFormEntity? activeForm;
final Map<String, String> formErrors;
WagesFormState copyWith({
StateStatus? status,
Uint8List? signImageData,
List<WageFormEntity>? actualForms,
WageFormEntity? activeForm,
Map<String, String>? formErrors
}) {
return WagesFormState(
status: status ?? this.status,
signImageData: signImageData ?? this.signImageData,
actualForms: actualForms ?? this.actualForms,
activeForm: activeForm ?? this.activeForm,
formErrors: formErrors ?? this.formErrors,
);
}
}

View File

@@ -0,0 +1,73 @@
import 'package:krow/features/profile/wages_forms/domain/entities/wage_form_entity.dart';
import 'package:krow/features/profile/wages_forms/domain/utils/wage_form_visitor.dart';
class EddFormEntity extends WageFormEntity {
const EddFormEntity({
required super.isSigned,
required super.isSubmitted,
required this.firstName,
required this.lastName,
required this.address,
required this.aptNumber,
required this.city,
required this.state,
required this.zipCode,
required this.socialNumber,
});
const EddFormEntity.empty({
super.isSigned = false,
super.isSubmitted = false,
this.firstName = '',
this.lastName = '',
this.address = '',
this.aptNumber,
this.city = '',
this.state = '',
this.zipCode = '',
this.socialNumber = '',
});
final String firstName;
final String lastName;
final String address;
final int? aptNumber;
final String city;
final String state;
final String zipCode;
final String socialNumber;
@override
String get name => 'EDD';
@override
T visit<T>(WageFormVisitor<T> visitor) {
return visitor.visitEddForm(this);
}
EddFormEntity copyWith({
bool? isSigned,
bool? isSubmitted,
String? firstName,
String? lastName,
String? address,
int? aptNumber,
String? city,
String? state,
String? zipCode,
String? socialNumber,
}) {
return EddFormEntity(
isSigned: isSigned ?? this.isSigned,
isSubmitted: isSubmitted ?? this.isSubmitted,
firstName: firstName ?? this.firstName,
lastName: lastName ?? this.lastName,
address: address ?? this.address,
aptNumber: aptNumber ?? this.aptNumber,
city: city ?? this.city,
state: state ?? this.state,
zipCode: zipCode ?? this.zipCode,
socialNumber: socialNumber ?? this.socialNumber,
);
}
}

View File

@@ -0,0 +1,93 @@
import 'package:krow/features/profile/wages_forms/domain/entities/wage_form_entity.dart';
import 'package:krow/features/profile/wages_forms/domain/utils/wage_form_visitor.dart';
class INineEntity extends WageFormEntity {
const INineEntity({
required super.isSigned,
required super.isSubmitted,
required this.firstName,
required this.lastName,
required this.middleName,
required this.address,
required this.aptNumber,
required this.city,
required this.state,
required this.zipCode,
required this.dateOfBirth,
required this.socialNumber,
required this.email,
required this.phoneNumber,
});
const INineEntity.empty({
super.isSigned = false,
super.isSubmitted = false,
this.firstName = '',
this.lastName = '',
this.middleName = '',
this.address = '',
this.aptNumber,
this.city = '',
this.state = '',
this.zipCode = '',
this.dateOfBirth,
this.socialNumber = '',
this.email = '',
this.phoneNumber = '+1',
});
final String firstName;
final String lastName;
final String middleName;
final String address;
final int? aptNumber;
final String city;
final String state;
final String zipCode;
final DateTime? dateOfBirth;
final String socialNumber;
final String email;
final String phoneNumber;
@override
String get name => 'I-9';
@override
T visit<T>(WageFormVisitor<T> visitor) {
return visitor.visitINineForm(this);
}
INineEntity copyWith({
bool? isSigned,
bool? isSubmitted,
String? firstName,
String? middleName,
String? lastName,
String? address,
int? aptNumber,
String? city,
String? state,
String? zipCode,
String? socialNumber,
DateTime? dateOfBirth,
String? email,
String? phoneNumber,
}) {
return INineEntity(
isSigned: isSigned ?? this.isSigned,
isSubmitted: isSubmitted ?? this.isSubmitted,
firstName: firstName ?? this.firstName,
middleName: middleName ?? this.middleName,
lastName: lastName ?? this.lastName,
address: address ?? this.address,
aptNumber: aptNumber ?? this.aptNumber,
city: city ?? this.city,
state: state ?? this.state,
zipCode: zipCode ?? this.zipCode,
socialNumber: socialNumber ?? this.socialNumber,
dateOfBirth: dateOfBirth ?? this.dateOfBirth,
email: email ?? this.email,
phoneNumber: phoneNumber ?? this.phoneNumber,
);
}
}

View File

@@ -0,0 +1,43 @@
import 'package:krow/features/profile/wages_forms/domain/entities/wage_form_entity.dart';
import 'package:krow/features/profile/wages_forms/domain/utils/wage_form_visitor.dart';
class OfferLetterEntity extends WageFormEntity {
const OfferLetterEntity({
required super.isSigned,
required super.isSubmitted,
required this.fullName,
required this.city,
});
const OfferLetterEntity.empty({
super.isSigned = false,
super.isSubmitted = false,
this.fullName = '',
this.city = '',
});
final String fullName;
final String city;
@override
String get name => 'Offer Letter & Agreement';
@override
T visit<T>(WageFormVisitor<T> visitor) {
return visitor.visitOfferLetterFrom(this);
}
OfferLetterEntity copyWith({
bool? isSigned,
bool? isSubmitted,
String? fullName,
String? city,
}) {
return OfferLetterEntity(
isSigned: isSigned ?? this.isSigned,
isSubmitted: isSubmitted ?? this.isSubmitted,
fullName: fullName ?? this.fullName,
city: city ?? this.city,
);
}
}

View File

@@ -0,0 +1,73 @@
import 'package:krow/features/profile/wages_forms/domain/entities/wage_form_entity.dart';
import 'package:krow/features/profile/wages_forms/domain/utils/wage_form_visitor.dart';
class WFourEntity extends WageFormEntity {
const WFourEntity({
required super.isSigned,
required super.isSubmitted,
required this.firstName,
required this.lastName,
required this.middleName,
required this.address,
required this.city,
required this.state,
required this.zipCode,
required this.socialNumber,
});
const WFourEntity.empty({
super.isSigned = false,
super.isSubmitted = false,
this.firstName = '',
this.lastName = '',
this.middleName = '',
this.address = '',
this.city = '',
this.state = '',
this.zipCode = '',
this.socialNumber = '',
});
final String firstName;
final String lastName;
final String middleName;
final String address;
final String city;
final String state;
final String zipCode;
final String socialNumber;
@override
String get name => 'W-4';
@override
T visit<T>(WageFormVisitor<T> visitor) {
return visitor.visitWFourForm(this);
}
WFourEntity copyWith({
bool? isSigned,
bool? isSubmitted,
String? firstName,
String? middleName,
String? lastName,
String? address,
String? city,
String? state,
String? zipCode,
String? socialNumber,
}) {
return WFourEntity(
isSigned: isSigned ?? this.isSigned,
isSubmitted: isSubmitted ?? this.isSubmitted,
firstName: firstName ?? this.firstName,
middleName: middleName ?? this.middleName,
lastName: lastName ?? this.lastName,
address: address ?? this.address,
city: city ?? this.city,
state: state ?? this.state,
zipCode: zipCode ?? this.zipCode,
socialNumber: socialNumber ?? this.socialNumber,
);
}
}

View File

@@ -0,0 +1,14 @@
import 'package:flutter/foundation.dart';
import 'package:krow/features/profile/wages_forms/domain/utils/wage_form_visitor.dart';
@immutable
abstract class WageFormEntity {
const WageFormEntity({required this.isSigned, required this.isSubmitted});
final bool isSigned;
final bool isSubmitted;
String get name;
T visit<T>(WageFormVisitor<T> visitor);
}

View File

@@ -0,0 +1,56 @@
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:syncfusion_flutter_signaturepad/signaturepad.dart';
class SignImageService {
Future<Uint8List> getSignPngBytes(SfSignaturePadState state) async {
final image = await getSignImage(state);
final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
final bytes = byteData!.buffer.asUint8List();
return bytes;
}
Future<ui.Image> getSignImage(SfSignaturePadState state) async {
var paths = state.toPathList();
Rect? bounds;
for (final path in paths) {
final pathBounds = path.getBounds();
if (bounds == null) {
bounds = pathBounds;
} else {
bounds = bounds.expandToInclude(pathBounds);
}
}
if (bounds == null) {
throw ArgumentError('The paths list is empty!');
}
final offset = Offset(-bounds.left, -bounds.top);
final recorder = ui.PictureRecorder();
final canvas = Canvas(recorder);
var scale = 2.0;
canvas.scale(scale); // Масштабування для більшої деталізації
final paint = Paint()
..color = Colors.black
..style = PaintingStyle.stroke
..strokeWidth = 1.0
..isAntiAlias = true
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round;
for (final path in paths) {
canvas.drawPath(path.shift(offset), paint);
}
final picture = recorder.endRecording();
return await picture.toImage(
(bounds.width * scale).ceil(),
(bounds.height * scale).ceil(),
);
}
}

View File

@@ -0,0 +1,101 @@
import 'package:krow/core/application/common/validators/email_validator.dart';
import 'package:krow/core/application/common/validators/phone_validator.dart';
import 'package:krow/core/application/common/validators/social_number_validator.dart';
import 'package:krow/features/profile/wages_forms/domain/bloc/wages_form_bloc.dart';
import 'package:krow/features/profile/wages_forms/domain/entities/edd_form_entity.dart';
import 'package:krow/features/profile/wages_forms/domain/entities/i_nine_entity.dart';
import 'package:krow/features/profile/wages_forms/domain/entities/offer_letter_entity.dart';
import 'package:krow/features/profile/wages_forms/domain/entities/w_four_entity.dart';
import 'package:krow/features/profile/wages_forms/domain/utils/wage_form_visitor.dart';
/// Returns a Map of keys that represent the form fields and values that
/// represent corresponding error
class ValidatorWageFromVisitor implements WageFormVisitor<Map<String, String>> {
const ValidatorWageFromVisitor();
@override
Map<String, String> visitEddForm(EddFormEntity entity) {
final ssnError = SocialNumberValidator.validate(
entity.socialNumber,
isRequired: true,
);
return <String, String>{
if (entity.firstName.isEmpty)
WagesFormFields.firstName: 'Required to fill',
if (entity.lastName.isEmpty) WagesFormFields.lastName: 'Required to fill',
if (entity.address.isEmpty) WagesFormFields.address: 'Required to fill',
if (entity.city.isEmpty) WagesFormFields.city: 'Required to fill',
if (entity.state.isEmpty) WagesFormFields.state: 'Required to fill',
if (entity.zipCode.isEmpty) WagesFormFields.zipCode: 'Required to fill',
if (ssnError != null) WagesFormFields.socialNumber: ssnError,
};
}
@override
Map<String, String> visitINineForm(INineEntity entity) {
final emailError = EmailValidator.validate(entity.email, isRequired: true);
final phoneError = PhoneValidator.validate(
entity.phoneNumber,
isRequired: true,
);
final ssnError = SocialNumberValidator.validate(
entity.socialNumber,
isRequired: true,
);
final birthdate = entity.dateOfBirth;
String? birthDateError;
if (birthdate == null) {
birthDateError = 'This field is required';
} else {
final now = DateTime.now();
if (birthdate.isAfter(now)) {
birthDateError = 'Future date selected';
} else if (birthdate.difference(now).inDays.abs() < 5840) {
birthDateError = 'Invalid date selected';
}
}
return <String, String>{
if (entity.firstName.isEmpty)
WagesFormFields.firstName: 'Required to fill',
if (entity.lastName.isEmpty) WagesFormFields.lastName: 'Required to fill',
if (entity.address.isEmpty) WagesFormFields.address: 'Required to fill',
if (entity.city.isEmpty) WagesFormFields.city: 'Required to fill',
if (entity.state.isEmpty) WagesFormFields.state: 'Required to fill',
if (entity.zipCode.isEmpty) WagesFormFields.zipCode: 'Required to fill',
if (birthDateError != null) WagesFormFields.birthdate: birthDateError,
if (ssnError != null) WagesFormFields.socialNumber: ssnError,
if (emailError != null) WagesFormFields.email: emailError,
if (phoneError != null) WagesFormFields.phone: phoneError,
};
}
@override
Map<String, String> visitOfferLetterFrom(OfferLetterEntity entity) {
return <String, String>{
if (entity.fullName.isEmpty) WagesFormFields.fullName: 'Required to fill',
if (entity.city.isEmpty) WagesFormFields.city: 'Required to fill'
};
}
@override
Map<String, String> visitWFourForm(WFourEntity entity) {
final ssnError = SocialNumberValidator.validate(
entity.socialNumber,
isRequired: true,
);
return <String, String>{
if (entity.firstName.isEmpty)
WagesFormFields.firstName: 'Required to fill',
if (entity.lastName.isEmpty) WagesFormFields.lastName: 'Required to fill',
if (entity.address.isEmpty) WagesFormFields.address: 'Required to fill',
if (entity.city.isEmpty) WagesFormFields.city: 'Required to fill',
if (entity.state.isEmpty) WagesFormFields.state: 'Required to fill',
if (entity.zipCode.isEmpty) WagesFormFields.zipCode: 'Required to fill',
if (ssnError != null) WagesFormFields.socialNumber: ssnError,
};
}
}

View File

@@ -0,0 +1,16 @@
import 'package:krow/features/profile/wages_forms/domain/entities/edd_form_entity.dart';
import 'package:krow/features/profile/wages_forms/domain/entities/i_nine_entity.dart';
import 'package:krow/features/profile/wages_forms/domain/entities/offer_letter_entity.dart';
import 'package:krow/features/profile/wages_forms/domain/entities/w_four_entity.dart';
abstract interface class WageFormVisitor<T> {
const WageFormVisitor();
T visitEddForm(EddFormEntity entity);
T visitINineForm(INineEntity entity);
T visitOfferLetterFrom(OfferLetterEntity entity);
T visitWFourForm(WFourEntity entity);
}

View File

@@ -0,0 +1,7 @@
import 'package:krow/features/profile/wages_forms/domain/entities/wage_form_entity.dart';
abstract interface class WagesFormsRepository {
Future<List<WageFormEntity>> getActualForms();
Future<void> submitWageForm({required WageFormEntity form});
}

View File

@@ -0,0 +1,66 @@
import 'package:intl/intl.dart';
import 'package:krow/features/profile/wages_forms/domain/entities/edd_form_entity.dart';
import 'package:krow/features/profile/wages_forms/domain/entities/i_nine_entity.dart';
import 'package:krow/features/profile/wages_forms/domain/entities/offer_letter_entity.dart';
import 'package:krow/features/profile/wages_forms/domain/entities/w_four_entity.dart';
import 'package:krow/features/profile/wages_forms/domain/utils/wage_form_visitor.dart';
class PreviewWageFormVisitor implements WageFormVisitor<Map<String, String>> {
const PreviewWageFormVisitor();
@override
Map<String, String> visitEddForm(EddFormEntity entity) {
return {
'Last Name': entity.lastName,
'First Name': entity.firstName,
'Address': entity.address,
'Apt. number': entity.aptNumber?.toString() ?? '',
'City': entity.city,
'State': entity.state,
'ZIP code': entity.zipCode,
'Social Number': entity.socialNumber,
};
}
@override
Map<String, String> visitINineForm(INineEntity entity) {
return {
'Last Name': entity.lastName,
'First Name': entity.firstName,
'Middle Name': entity.middleName,
'Address': entity.address,
'Apt. number': entity.aptNumber?.toString() ?? '',
'City': entity.city,
'State': entity.state,
'ZIP code': entity.zipCode,
'Date of birth': entity.dateOfBirth != null
? DateFormat('M/d/y').format(entity.dateOfBirth!)
: '',
'Social Number': entity.socialNumber,
'Email': entity.email,
'Phone Number': entity.phoneNumber,
};
}
@override
Map<String, String> visitOfferLetterFrom(OfferLetterEntity entity) {
return {
'Full Name': entity.fullName,
'City': entity.city,
};
}
@override
Map<String, String> visitWFourForm(WFourEntity entity) {
return {
'Last Name': entity.lastName,
'First Name': entity.firstName,
'Middle Name': entity.middleName,
'Address': entity.address,
'City': entity.city,
'State': entity.state,
'ZIP code': entity.zipCode,
'Social Number': entity.socialNumber,
};
}
}

View File

@@ -0,0 +1,48 @@
import 'package:flutter/material.dart';
import 'package:krow/core/application/routing/routes.gr.dart';
import 'package:krow/features/profile/wages_forms/domain/entities/edd_form_entity.dart';
import 'package:krow/features/profile/wages_forms/domain/entities/i_nine_entity.dart';
import 'package:krow/features/profile/wages_forms/domain/entities/offer_letter_entity.dart';
import 'package:krow/features/profile/wages_forms/domain/entities/w_four_entity.dart';
import 'package:krow/features/profile/wages_forms/domain/utils/wage_form_visitor.dart';
import 'package:krow/features/profile/wages_forms/presentation/widgets/form_item_widget.dart';
class RoutingWageFormVisitor implements WageFormVisitor<Widget> {
const RoutingWageFormVisitor();
@override
Widget visitEddForm(EddFormEntity entity) {
return FormItemWidget(
formRoute: const EddFormRoute(),
wageForm: entity,
label: 'EDD',
);
}
@override
Widget visitINineForm(INineEntity entity) {
return FormItemWidget(
formRoute: const INineFormRoute(),
wageForm: entity,
label: 'I-9',
);
}
@override
Widget visitOfferLetterFrom(OfferLetterEntity entity) {
return FormItemWidget(
formRoute: const OfferLetterFormRoute(),
wageForm: entity,
label: 'Offer Letter & Agreement',
);
}
@override
Widget visitWFourForm(WFourEntity entity) {
return FormItemWidget(
formRoute: const WFourFormRoute(),
wageForm: entity,
label: 'W-4',
);
}
}

View File

@@ -0,0 +1,31 @@
import 'package:krow/features/profile/wages_forms/domain/entities/edd_form_entity.dart';
import 'package:krow/features/profile/wages_forms/domain/entities/i_nine_entity.dart';
import 'package:krow/features/profile/wages_forms/domain/entities/offer_letter_entity.dart';
import 'package:krow/features/profile/wages_forms/domain/entities/w_four_entity.dart';
import 'package:krow/features/profile/wages_forms/domain/utils/wage_form_visitor.dart';
class TitleWageFormVisitor implements WageFormVisitor<String> {
const TitleWageFormVisitor({this.isPreview = false});
final bool isPreview;
@override
String visitEddForm(EddFormEntity entity) {
return 'Form EDD${isPreview ? ' Preview' : ''}';
}
@override
String visitINineForm(INineEntity entity) {
return 'Form I-9${isPreview ? ' Preview' : ''}';
}
@override
String visitOfferLetterFrom(OfferLetterEntity entity) {
return isPreview ? 'Form Offer Let... Preview' : 'Offer Letter & Agreement';
}
@override
String visitWFourForm(WFourEntity entity) {
return 'Form W-4${isPreview ? ' Preview' : ''}';
}
}

View File

@@ -0,0 +1,29 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow/features/profile/wages_forms/domain/bloc/wages_form_bloc.dart';
import 'package:krow/features/profile/wages_forms/domain/entities/wage_form_entity.dart';
mixin FormUpdaterMixin<T extends StatefulWidget, F extends WageFormEntity>
on State<T> {
late final formBloc = context.read<WagesFormBloc>();
late F form;
Timer? updateTimer;
void updateCurrentForm(F updatedForm) {
updateTimer?.cancel();
form = updatedForm;
updateTimer = Timer(
const Duration(milliseconds: 500),
() {
formBloc.add(UpdateCurrentForm(form: form));
},
);
}
void handleSubmit() {
formBloc.add(const ValidateCurrentForm());
}
}

View File

@@ -0,0 +1,216 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:gap/gap.dart';
import 'package:krow/core/application/common/str_extensions.dart';
import 'package:krow/core/application/routing/routes.gr.dart';
import 'package:krow/core/data/enums/state_status.dart';
import 'package:krow/core/presentation/gen/assets.gen.dart';
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
import 'package:krow/core/presentation/styles/theme.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_app_bar.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_button.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_date_selector.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_dropdown.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_input.dart';
import 'package:krow/features/profile/wages_forms/domain/bloc/wages_form_bloc.dart';
import 'package:krow/features/profile/wages_forms/domain/entities/edd_form_entity.dart';
import 'package:krow/features/profile/wages_forms/presentation/mixins/form_updater_mixin.dart';
import 'package:krow/features/profile/wages_forms/presentation/widgets/form_sign_holder_widget.dart';
@RoutePage()
class EddFormScreen extends StatefulWidget {
const EddFormScreen({super.key});
@override
State<EddFormScreen> createState() => _EddFormScreenState();
}
class _EddFormScreenState extends State<EddFormScreen>
with FormUpdaterMixin<EddFormScreen, EddFormEntity> {
@override
void initState() {
super.initState();
final activeForm = context.read<WagesFormBloc>().state.activeForm;
form =
activeForm is EddFormEntity ? activeForm : const EddFormEntity.empty();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: KwAppBar(
titleText: 'EDD',
showNotification: true,
),
body: BlocConsumer<WagesFormBloc, WagesFormState>(
listenWhen: (previous, current) =>
current.status == StateStatus.success,
buildWhen: (previous, current) => current.status == StateStatus.error,
listener: (context, state) {
context.pushRoute(const SignedFormRoute());
updateCurrentForm(form.copyWith(isSigned: true));
},
builder: (context, state) {
final formErrors = state.formErrors;
return ListView(
primary: false,
padding: const EdgeInsets.all(16),
children: [
const Text(
'Fill the form below:',
style: AppTextStyles.bodySmallRegBlackGrey,
),
const Gap(24),
KwTextInput(
initialValue: form.lastName,
onChanged: (lastName) {
updateCurrentForm(form.copyWith(lastName: lastName));
},
title: 'Last Name (Family Name)',
hintText: 'Enter your Last name',
borderColor: AppColors.grayStroke,
textInputAction: TextInputAction.next,
keyboardType: TextInputType.name,
textCapitalization: TextCapitalization.sentences,
showError: formErrors.containsKey(WagesFormFields.lastName),
helperText: formErrors[WagesFormFields.lastName],
),
const Gap(8),
KwTextInput(
initialValue: form.firstName,
onChanged: (firstName) {
updateCurrentForm(form.copyWith(firstName: firstName));
},
title: 'First Name (Given Name)',
hintText: 'Enter your First name',
borderColor: AppColors.grayStroke,
textInputAction: TextInputAction.next,
keyboardType: TextInputType.name,
textCapitalization: TextCapitalization.sentences,
showError: formErrors.containsKey(WagesFormFields.firstName),
helperText: formErrors[WagesFormFields.firstName],
),
const Gap(8),
KwTextInput(
initialValue: form.address,
onChanged: (address) {
updateCurrentForm(form.copyWith(address: address));
},
title: 'Address',
hintText: 'Enter your Address name',
borderColor: AppColors.grayStroke,
textInputAction: TextInputAction.next,
keyboardType: TextInputType.name,
textCapitalization: TextCapitalization.sentences,
showError: formErrors.containsKey(WagesFormFields.address),
helperText: formErrors[WagesFormFields.address],
),
const Gap(8),
KwTextInput(
initialValue: form.aptNumber?.toString(),
onChanged: (apt) {
updateCurrentForm(
form.copyWith(aptNumber: int.tryParse(apt)),
);
},
title: 'Apt. Number (if any)',
hintText: 'Enter your Apt. Number',
borderColor: AppColors.grayStroke,
textInputAction: TextInputAction.next,
keyboardType: TextInputType.number,
showError: false,
),
const Gap(8),
KwTextInput(
initialValue: form.city,
onChanged: (city) {
updateCurrentForm(form.copyWith(city: city));
},
title: 'City or Town',
hintText: 'Enter your City',
borderColor: AppColors.grayStroke,
textInputAction: TextInputAction.next,
keyboardType: TextInputType.name,
textCapitalization: TextCapitalization.sentences,
showError: formErrors.containsKey(WagesFormFields.city),
helperText: formErrors[WagesFormFields.city],
),
const Gap(8),
KwDropdown<String>(
title: 'State',
horizontalPadding: 16,
borderColor: AppColors.grayStroke,
hintText: 'Select state',
selectedItem: form.state.toDropDownItem(),
items: [
for (final state in usStates)
KwDropDownItem(data: state, title: state),
],
onSelected: (state) {
updateCurrentForm(form.copyWith(state: state));
},
showError: formErrors.containsKey(WagesFormFields.state),
helperText: formErrors[WagesFormFields.state],
),
const Gap(8),
KwTextInput(
initialValue: form.zipCode,
onChanged: (zipCode) {
updateCurrentForm(form.copyWith(zipCode: zipCode));
},
title: 'ZIP Code',
hintText: 'Enter your City\'s ZIP code',
borderColor: AppColors.grayStroke,
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
showError: formErrors.containsKey(WagesFormFields.zipCode),
helperText: formErrors[WagesFormFields.zipCode],
),
const Gap(8),
KwTextInput(
initialValue: form.socialNumber,
onChanged: (socialNumber) {
updateCurrentForm(form.copyWith(socialNumber: socialNumber));
},
title: 'U.S. Social Number',
hintText: 'Enter your U.S. Social Number',
borderColor: AppColors.grayStroke,
textInputAction: TextInputAction.done,
showError: formErrors.containsKey(WagesFormFields.socialNumber),
helperText: formErrors[WagesFormFields.socialNumber],
),
const Gap(24),
const FormSignHolderWidget(signingPageTitle: 'EDD'),
const Gap(24),
KwDateSelector(
readOnly: true,
title: 'Todays Date',
hintText: 'mm/dd/yyyy',
initialValue: DateTime.now(),
onSelect: (date) {},
),
const Gap(24),
KwButton.outlinedPrimary(
label: 'Document Preview',
leftIcon: Assets.images.icons.eye,
onPressed: () {
context.pushRoute(const FormPreviewRoute());
},
),
],
);
},
),
bottomNavigationBar: SafeArea(
top: false,
child: Padding(
padding: const EdgeInsets.all(16),
child: KwButton.primary(label: 'Submit', onPressed: handleSubmit),
),
),
);
}
}

View File

@@ -0,0 +1,68 @@
import 'package:auto_route/annotations.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:gap/gap.dart';
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
import 'package:krow/core/presentation/styles/theme.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_app_bar.dart';
import 'package:krow/features/profile/wages_forms/domain/bloc/wages_form_bloc.dart';
import 'package:krow/features/profile/wages_forms/presentation/components/preview_wage_from_visitor.dart';
import 'package:krow/features/profile/wages_forms/presentation/components/title_wage_form_visitor.dart';
@RoutePage()
class FormPreviewScreen extends StatelessWidget {
const FormPreviewScreen({super.key});
@override
Widget build(BuildContext context) {
final form = context.read<WagesFormBloc>().state.activeForm;
const titleVisitor = TitleWageFormVisitor(isPreview: true);
final previewData =
form != null ? form.visit(const PreviewWageFormVisitor()) : {};
return Scaffold(
appBar: KwAppBar(
titleText: form != null ? form.visit(titleVisitor) : 'Preview',
showNotification: true,
),
body: Container(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 24),
decoration: const BoxDecoration(
color: AppColors.grayPrimaryFrame,
borderRadius: BorderRadius.all(Radius.circular(12)),
),
child: SingleChildScrollView(
primary: false,
padding: const EdgeInsets.all(16),
child: Column(
spacing: 16,
children: [
for (final entry in previewData.entries)
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${entry.key}:',
textAlign: TextAlign.start,
style: AppTextStyles.bodyMediumMed.copyWith(
color: AppColors.blackGray,
),
),
const Gap(12),
Expanded(
child: Text(
entry.value,
textAlign: TextAlign.end,
style: AppTextStyles.bodyMediumMed,
overflow: TextOverflow.ellipsis,
),
),
],
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,151 @@
import 'dart:async';
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:gap/gap.dart';
import 'package:krow/core/presentation/styles/kw_box_decorations.dart';
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
import 'package:krow/core/presentation/styles/theme.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_app_bar.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_button.dart';
import 'package:krow/features/profile/wages_forms/domain/bloc/wages_form_bloc.dart';
import 'package:krow/features/profile/wages_forms/domain/sign_image_service.dart';
import 'package:krow/features/profile/wages_forms/presentation/widgets/sign_notice_widget.dart';
import 'package:syncfusion_flutter_signaturepad/signaturepad.dart';
@RoutePage()
class FormSignScreen extends StatefulWidget {
const FormSignScreen({super.key, required this.title});
final String title;
@override
State<FormSignScreen> createState() => _FormSignScreenState();
}
class _FormSignScreenState extends State<FormSignScreen> {
final GlobalKey<SfSignaturePadState> _signaturePadKey = GlobalKey();
final _signNotifier = ValueNotifier<bool>(false);
Timer? _signTrackingTimer;
void _startSignTracking() {
_signNotifier.value = false;
_signTrackingTimer?.cancel();
_signTrackingTimer = Timer.periodic(const Duration(milliseconds: 300), (_) {
_signNotifier.value =
_signaturePadKey.currentState?.toPathList().isNotEmpty ?? false;
if (_signNotifier.value) _signTrackingTimer?.cancel();
});
}
void _handleOnSubmit() {
final wagesBloc = context.read<WagesFormBloc>();
SignImageService()
.getSignPngBytes(_signaturePadKey.currentState!)
.then((imageData) {
wagesBloc.add(AddUserSignImage(imageData: imageData));
});
context.router.maybePop();
}
@override
void initState() {
super.initState();
_startSignTracking();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: KwAppBar(
titleText: widget.title,
showNotification: true,
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Sign the document below:',
style: AppTextStyles.bodySmallRegBlackGrey,
),
const Gap(24),
Expanded(
child: Stack(
alignment: AlignmentDirectional.topEnd,
children: [
DecoratedBox(
decoration: KwBoxDecorations.primaryLight8,
child: SfSignaturePad(
key: _signaturePadKey,
),
),
ListenableBuilder(
listenable: _signNotifier,
builder: (context, _) {
return _signNotifier.value
? IconButton(
onPressed: () {
_signaturePadKey.currentState?.clear();
_startSignTracking();
},
icon: const Icon(
Icons.close,
color: AppColors.bgColorDark,
size: 24,
),
)
: const SizedBox.shrink();
},
),
Align(
alignment: Alignment.bottomCenter,
child: ListenableBuilder(
listenable: _signNotifier,
builder: (context, _) {
return _signNotifier.value
? const SizedBox.shrink()
: const Padding(
padding: EdgeInsets.all(12),
child: SignNoticeWidget(),
);
},
),
)
],
),
),
const Gap(28),
],
),
),
bottomNavigationBar: SafeArea(
top: false,
child: Padding(
padding: const EdgeInsets.all(16),
child: ListenableBuilder(
listenable: _signNotifier,
builder: (context, _) {
return KwButton.primary(
label: 'Submit and Back to Form',
disabled: !_signNotifier.value,
onPressed: _handleOnSubmit,
);
},
),
),
),
);
}
@override
void dispose() {
_signTrackingTimer?.cancel();
super.dispose();
}
}

View File

@@ -0,0 +1,53 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:gap/gap.dart';
import 'package:krow/core/data/enums/state_status.dart';
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
import 'package:krow/core/presentation/styles/theme.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_app_bar.dart';
import 'package:krow/features/profile/wages_forms/domain/bloc/wages_form_bloc.dart';
import 'package:krow/features/profile/wages_forms/presentation/components/routing_wage_form_visitor.dart';
@RoutePage()
class FormsListScreen extends StatelessWidget {
const FormsListScreen({super.key});
final _routingVisitor = const RoutingWageFormVisitor();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: KwAppBar(titleText: 'Wages Form', showNotification: true),
body: BlocBuilder<WagesFormBloc, WagesFormState>(
buildWhen: (previous, current) =>
previous.actualForms != current.actualForms,
builder: (context, state) {
return ListView(
primary: false,
padding: const EdgeInsets.all(16),
children: [
Text(
'View and manage your wage forms below. '
'Select a form to review or update details.',
style: AppTextStyles.bodySmallReg.copyWith(
color: AppColors.blackGray,
),
),
const Gap(24),
if (state.status == StateStatus.loading &&
state.actualForms.isEmpty)
const Align(
alignment: AlignmentDirectional.bottomCenter,
child: CircularProgressIndicator(),
),
for (final entry in state.actualForms)
entry.visit(_routingVisitor),
const Gap(90),
],
);
},
),
);
}
}

View File

@@ -0,0 +1,270 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:gap/gap.dart';
import 'package:krow/core/application/common/str_extensions.dart';
import 'package:krow/core/application/routing/routes.gr.dart';
import 'package:krow/core/data/enums/state_status.dart';
import 'package:krow/core/presentation/gen/assets.gen.dart';
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
import 'package:krow/core/presentation/styles/theme.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_app_bar.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_button.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_date_selector.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_dropdown.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_input.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_phone_input.dart';
import 'package:krow/features/profile/wages_forms/domain/bloc/wages_form_bloc.dart';
import 'package:krow/features/profile/wages_forms/domain/entities/i_nine_entity.dart';
import 'package:krow/features/profile/wages_forms/presentation/mixins/form_updater_mixin.dart';
import 'package:krow/features/profile/wages_forms/presentation/widgets/form_sign_holder_widget.dart';
@RoutePage()
class INineFormScreen extends StatefulWidget {
const INineFormScreen({super.key});
@override
State<INineFormScreen> createState() => _INineFormScreenState();
}
class _INineFormScreenState extends State<INineFormScreen>
with FormUpdaterMixin<INineFormScreen, INineEntity> {
@override
void initState() {
super.initState();
final activeForm = context.read<WagesFormBloc>().state.activeForm;
form = activeForm is INineEntity ? activeForm : const INineEntity.empty();
}
@override
Widget build(BuildContext context) {
final currentData = DateTime.now();
return Scaffold(
appBar: KwAppBar(
titleText: 'I-9',
showNotification: true,
),
body: BlocConsumer<WagesFormBloc, WagesFormState>(
listenWhen: (previous, current) =>
current.status == StateStatus.success,
buildWhen: (previous, current) => current.status == StateStatus.error,
listener: (context, state) {
context.pushRoute(const SignedFormRoute());
updateCurrentForm(form.copyWith(isSigned: true));
},
builder: (context, state) {
final formErrors = state.formErrors;
return ListView(
primary: false,
padding: const EdgeInsets.all(16),
children: [
const Text(
'Fill the form below:',
style: AppTextStyles.bodySmallRegBlackGrey,
),
const Gap(24),
KwTextInput(
initialValue: form.lastName,
onChanged: (lastName) {
updateCurrentForm(form.copyWith(lastName: lastName));
},
title: 'Last Name (Family Name)',
hintText: 'Enter your Last name',
borderColor: AppColors.grayStroke,
textInputAction: TextInputAction.next,
keyboardType: TextInputType.name,
textCapitalization: TextCapitalization.sentences,
showError: formErrors.containsKey(WagesFormFields.lastName),
helperText: formErrors[WagesFormFields.lastName],
),
const Gap(8),
KwTextInput(
initialValue: form.firstName,
onChanged: (firstName) {
updateCurrentForm(form.copyWith(firstName: firstName));
},
title: 'First Name (Given Name)',
hintText: 'Enter your First name',
borderColor: AppColors.grayStroke,
textInputAction: TextInputAction.next,
keyboardType: TextInputType.name,
textCapitalization: TextCapitalization.sentences,
showError: formErrors.containsKey(WagesFormFields.firstName),
helperText: formErrors[WagesFormFields.firstName],
),
const Gap(8),
KwTextInput(
initialValue: form.middleName,
onChanged: (middleName) {
updateCurrentForm(form.copyWith(middleName: middleName));
},
title: 'Middle Initial (if any)',
hintText: 'Enter your Middle name',
borderColor: AppColors.grayStroke,
textInputAction: TextInputAction.next,
keyboardType: TextInputType.name,
textCapitalization: TextCapitalization.sentences,
),
const Gap(8),
KwTextInput(
initialValue: form.address,
onChanged: (address) {
updateCurrentForm(form.copyWith(address: address));
},
title: 'Address',
hintText: 'Enter your Address name',
borderColor: AppColors.grayStroke,
textInputAction: TextInputAction.next,
keyboardType: TextInputType.name,
textCapitalization: TextCapitalization.sentences,
showError: formErrors.containsKey(WagesFormFields.address),
helperText: formErrors[WagesFormFields.address],
),
const Gap(8),
KwTextInput(
initialValue: form.aptNumber?.toString(),
onChanged: (apt) {
updateCurrentForm(
form.copyWith(aptNumber: int.tryParse(apt)),
);
},
title: 'Apt. Number (if any)',
hintText: 'Enter your Apt. Number',
borderColor: AppColors.grayStroke,
textInputAction: TextInputAction.next,
keyboardType: TextInputType.number,
showError: false,
),
const Gap(8),
KwTextInput(
initialValue: form.city,
onChanged: (city) {
updateCurrentForm(form.copyWith(city: city));
},
title: 'City or Town',
hintText: 'Enter your City',
borderColor: AppColors.grayStroke,
textInputAction: TextInputAction.next,
keyboardType: TextInputType.name,
textCapitalization: TextCapitalization.sentences,
showError: formErrors.containsKey(WagesFormFields.city),
helperText: formErrors[WagesFormFields.city],
),
const Gap(8),
KwDropdown<String>(
title: 'State',
horizontalPadding: 16,
borderColor: AppColors.grayStroke,
hintText: 'Select state',
selectedItem: form.state.toDropDownItem(),
items: [
for (final state in usStates)
KwDropDownItem(data: state, title: state),
],
onSelected: (state) {
updateCurrentForm(form.copyWith(state: state));
},
showError: formErrors.containsKey(WagesFormFields.state),
helperText: formErrors[WagesFormFields.state],
),
const Gap(8),
KwTextInput(
initialValue: form.zipCode,
onChanged: (zipCode) {
updateCurrentForm(form.copyWith(zipCode: zipCode));
},
title: 'ZIP Code',
hintText: 'Enter your City\'s ZIP code',
borderColor: AppColors.grayStroke,
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
showError: formErrors.containsKey(WagesFormFields.zipCode),
helperText: formErrors[WagesFormFields.zipCode],
),
const Gap(8),
KwDateSelector(
title: 'Date of Birth',
hintText: 'mm/dd/yyyy',
initialValue: form.dateOfBirth,
firstDate: currentData.subtract(const Duration(days: 36500)),
lastDate: currentData.subtract(const Duration(days: 3650)),
initialDate: currentData.subtract(const Duration(days: 6570)),
onSelect: (date) {
updateCurrentForm(form.copyWith(dateOfBirth: date));
},
showError: formErrors.containsKey(WagesFormFields.birthdate),
helperText: formErrors[WagesFormFields.birthdate],
),
const Gap(8),
KwTextInput(
initialValue: form.socialNumber,
onChanged: (socialNumber) {
updateCurrentForm(form.copyWith(socialNumber: socialNumber));
},
title: 'U.S. Social Number',
hintText: 'Enter your U.S. Social Number',
borderColor: AppColors.grayStroke,
textInputAction: TextInputAction.next,
showError: formErrors.containsKey(WagesFormFields.socialNumber),
helperText: formErrors[WagesFormFields.socialNumber],
),
const Gap(8),
KwTextInput(
initialValue: form.email,
onChanged: (email) {
updateCurrentForm(form.copyWith(email: email));
},
title: 'Employees Email Address',
hintText: 'Enter your Email',
borderColor: AppColors.grayStroke,
textInputAction: TextInputAction.next,
keyboardType: TextInputType.emailAddress,
showError: formErrors.containsKey(WagesFormFields.email),
helperText: formErrors[WagesFormFields.email],
),
const Gap(8),
KwPhoneInput(
title: 'Phone Number',
initialValue: form.phoneNumber,
onChanged: (phone) {
updateCurrentForm(form.copyWith(phoneNumber: phone));
},
borderColor: AppColors.grayStroke,
showError: formErrors.containsKey(WagesFormFields.phone),
helperText: formErrors[WagesFormFields.phone],
),
const Gap(24),
const FormSignHolderWidget(signingPageTitle: 'I-9'),
const Gap(24),
KwDateSelector(
readOnly: true,
title: 'Todays Date',
hintText: 'mm/dd/yyyy',
initialValue: currentData,
onSelect: (date) {},
),
const Gap(24),
KwButton.outlinedPrimary(
label: 'Document Preview',
leftIcon: Assets.images.icons.eye,
onPressed: () {
context.pushRoute(const FormPreviewRoute());
},
),
],
);
},
),
bottomNavigationBar: SafeArea(
top: false,
child: Padding(
padding: const EdgeInsets.all(16),
child: KwButton.primary(label: 'Submit', onPressed: handleSubmit),
),
),
);
}
}

View File

@@ -0,0 +1,148 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:gap/gap.dart';
import 'package:krow/core/application/routing/routes.gr.dart';
import 'package:krow/core/data/enums/state_status.dart';
import 'package:krow/core/presentation/gen/assets.gen.dart';
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
import 'package:krow/core/presentation/styles/theme.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_app_bar.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_button.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_input.dart';
import 'package:krow/features/profile/wages_forms/domain/bloc/wages_form_bloc.dart';
import 'package:krow/features/profile/wages_forms/domain/entities/offer_letter_entity.dart';
import 'package:krow/features/profile/wages_forms/presentation/mixins/form_updater_mixin.dart';
import 'package:krow/features/profile/wages_forms/presentation/widgets/form_sign_holder_widget.dart';
@RoutePage()
class OfferLetterFormScreen extends StatefulWidget {
const OfferLetterFormScreen({super.key});
@override
State<OfferLetterFormScreen> createState() => _OfferLetterFormScreenState();
}
class _OfferLetterFormScreenState extends State<OfferLetterFormScreen>
with FormUpdaterMixin<OfferLetterFormScreen, OfferLetterEntity> {
@override
void initState() {
super.initState();
final activeForm = context.read<WagesFormBloc>().state.activeForm;
form = activeForm is OfferLetterEntity
? activeForm
: const OfferLetterEntity.empty();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: KwAppBar(
titleText: 'Offer Letter & Agreement',
showNotification: true,
),
body: BlocConsumer<WagesFormBloc, WagesFormState>(
listenWhen: (previous, current) =>
current.status == StateStatus.success,
buildWhen: (previous, current) => current.status == StateStatus.error,
listener: (context, state) {
context.pushRoute(
const SignedFormRoute(),
);
updateCurrentForm(form.copyWith(isSigned: true));
},
builder: (context, state) {
final formErrors = state.formErrors;
return ListView(
primary: false,
padding: const EdgeInsets.all(16),
children: [
const Text(
'Fill the form below:',
style: AppTextStyles.bodySmallRegBlackGrey,
),
const Gap(24),
KwTextInput(
initialValue: form.fullName,
onChanged: (fullName) {
updateCurrentForm(form.copyWith(fullName: fullName));
},
title: 'Full Name',
hintText: 'Enter your full name',
textInputAction: TextInputAction.next,
textCapitalization: TextCapitalization.sentences,
keyboardType: TextInputType.name,
borderColor: AppColors.grayStroke,
showError: formErrors.containsKey(WagesFormFields.fullName),
helperText: formErrors[WagesFormFields.fullName],
),
const Gap(8),
KwTextInput(
initialValue: form.city,
onChanged: (city) {
updateCurrentForm(form.copyWith(city: city));
},
title: 'City',
hintText: 'Enter your City',
textInputAction: TextInputAction.next,
textCapitalization: TextCapitalization.sentences,
keyboardType: TextInputType.name,
borderColor: AppColors.grayStroke,
showError: formErrors.containsKey(WagesFormFields.city),
helperText: formErrors[WagesFormFields.city],
),
const Gap(24),
const Text(
'By signing below, you agree you have read and '
'understood the following documents:',
style: AppTextStyles.bodySmallMed,
),
const Gap(12),
Text(
' • Advantage Workforce Services (“AWS”) Offer Letter',
style: AppTextStyles.bodySmallMed.copyWith(
color: AppColors.primaryBlue,
),
),
const Gap(12),
Text(
' • Arbitration Agreements',
style: AppTextStyles.bodySmallMed.copyWith(
color: AppColors.primaryBlue,
),
),
const Gap(12),
Text(
' • Employee Policies',
style: AppTextStyles.bodySmallMed.copyWith(
color: AppColors.primaryBlue,
),
),
const Gap(24),
const FormSignHolderWidget(
signingPageTitle: 'Offer Letter & Agreement',
),
const Gap(24),
KwButton.outlinedPrimary(
label: 'Document Preview',
leftIcon: Assets.images.icons.eye,
onPressed: () {
context.pushRoute(const FormPreviewRoute());
},
),
],
);
},
),
bottomNavigationBar: SafeArea(
top: false,
child: Padding(
padding: const EdgeInsets.all(16),
child: KwButton.primary(label: 'Submit', onPressed: handleSubmit),
),
),
);
}
}

View File

@@ -0,0 +1,159 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:gap/gap.dart';
import 'package:intl/intl.dart';
import 'package:krow/core/application/routing/routes.gr.dart';
import 'package:krow/core/data/enums/state_status.dart';
import 'package:krow/core/presentation/gen/assets.gen.dart';
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
import 'package:krow/core/presentation/styles/theme.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_app_bar.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_button.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_loading_overlay.dart';
import 'package:krow/features/profile/wages_forms/domain/bloc/wages_form_bloc.dart';
import 'package:krow/features/profile/wages_forms/presentation/components/title_wage_form_visitor.dart';
@RoutePage()
class SignedFormScreen extends StatelessWidget {
const SignedFormScreen({
super.key,
});
@override
Widget build(BuildContext context) {
final form = context.read<WagesFormBloc>().state.activeForm;
const titleVisitor = TitleWageFormVisitor();
return Scaffold(
appBar: KwAppBar(
titleText: form != null ? form.visit(titleVisitor) : 'Form',
showNotification: true,
),
body: ListView(
primary: false,
padding: const EdgeInsetsDirectional.fromSTEB(16, 4, 16, 60),
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: const BoxDecoration(
color: AppColors.grayPrimaryFrame,
borderRadius: BorderRadius.all(
Radius.circular(12),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
form?.name ?? 'Form',
style: AppTextStyles.headingH3,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const Gap(12),
DecoratedBox(
decoration: const BoxDecoration(
color: AppColors.statusSuccess,
borderRadius: BorderRadius.all(Radius.circular(32)),
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 6,
),
child: Text(
'Signed',
style: AppTextStyles.bodySmallMed
.copyWith(color: AppColors.grayWhite),
),
),
)
],
),
const Gap(12),
const Divider(color: AppColors.grayTintStroke),
const Gap(12),
const Text('SIGNED DATE', style: AppTextStyles.captionReg),
const Gap(8),
Text(
DateFormat('M/d/y').format(DateTime.now()),
style: AppTextStyles.bodyMediumMed,
),
const Gap(24),
const Text('SIGNATURE', style: AppTextStyles.captionReg),
const Gap(6),
Container(
height: 106,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColors.graySecondaryFrame,
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
Expanded(
child: Align(
alignment: AlignmentDirectional.centerStart,
child: Image.memory(
context.read<WagesFormBloc>().state.signImageData ??
Uint8List(0),
),
),
),
const Gap(6),
const Divider(
height: 1,
color: AppColors.grayTintStroke,
),
],
),
),
const Gap(12),
KwButton.outlinedPrimary(
label: 'Document Preview',
leftIcon: Assets.images.icons.eye,
onPressed: () {
context.pushRoute(const FormPreviewRoute());
},
),
],
),
),
],
),
bottomNavigationBar: SafeArea(
top: false,
child: Padding(
padding: const EdgeInsets.all(16),
child: BlocConsumer<WagesFormBloc, WagesFormState>(
listenWhen: (previous, current) =>
current.status == StateStatus.success,
buildWhen: (previous, current) => current.status != previous.status,
listener: (context, state) {
context.router.popUntilRoot();
},
builder: (context, state) {
return KwLoadingOverlay(
shouldShowLoading: state.status == StateStatus.loading,
child: KwButton.primary(
label: 'Submit',
onPressed: () {
context
.read<WagesFormBloc>()
.add(const SubmitCurrentForm());
},
),
);
},
),
),
),
);
}
}

View File

@@ -0,0 +1,213 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:gap/gap.dart';
import 'package:krow/core/application/common/str_extensions.dart';
import 'package:krow/core/application/routing/routes.gr.dart';
import 'package:krow/core/data/enums/state_status.dart';
import 'package:krow/core/presentation/gen/assets.gen.dart';
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
import 'package:krow/core/presentation/styles/theme.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_app_bar.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_button.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_date_selector.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_dropdown.dart';
import 'package:krow/core/presentation/widgets/ui_kit/kw_input.dart';
import 'package:krow/features/profile/wages_forms/domain/bloc/wages_form_bloc.dart';
import 'package:krow/features/profile/wages_forms/domain/entities/w_four_entity.dart';
import 'package:krow/features/profile/wages_forms/presentation/mixins/form_updater_mixin.dart';
import 'package:krow/features/profile/wages_forms/presentation/widgets/form_sign_holder_widget.dart';
@RoutePage()
class WFourFormScreen extends StatefulWidget {
const WFourFormScreen({super.key});
@override
State<WFourFormScreen> createState() => _WFourFormScreenState();
}
class _WFourFormScreenState extends State<WFourFormScreen>
with FormUpdaterMixin<WFourFormScreen, WFourEntity> {
@override
void initState() {
super.initState();
final activeForm = context.read<WagesFormBloc>().state.activeForm;
form = activeForm is WFourEntity ? activeForm : const WFourEntity.empty();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: KwAppBar(
titleText: 'W-4',
showNotification: true,
),
body: BlocConsumer<WagesFormBloc, WagesFormState>(
listenWhen: (previous, current) =>
current.status == StateStatus.success,
buildWhen: (previous, current) => current.status == StateStatus.error,
listener: (context, state) {
context.pushRoute(const SignedFormRoute());
updateCurrentForm(form.copyWith(isSigned: true));
},
builder: (context, state) {
final formErrors = state.formErrors;
return ListView(
primary: false,
padding: const EdgeInsets.all(16),
children: [
const Text(
'Fill the form below:',
style: AppTextStyles.bodySmallRegBlackGrey,
),
const Gap(24),
KwTextInput(
initialValue: form.lastName,
onChanged: (lastName) {
updateCurrentForm(form.copyWith(lastName: lastName));
},
title: 'Last Name (Family Name)',
hintText: 'Enter your Last name',
borderColor: AppColors.grayStroke,
textInputAction: TextInputAction.next,
keyboardType: TextInputType.name,
textCapitalization: TextCapitalization.sentences,
showError: formErrors.containsKey(WagesFormFields.lastName),
helperText: formErrors[WagesFormFields.lastName],
),
const Gap(8),
KwTextInput(
initialValue: form.firstName,
onChanged: (firstName) {
updateCurrentForm(form.copyWith(firstName: firstName));
},
title: 'First Name (Given Name)',
hintText: 'Enter your First name',
borderColor: AppColors.grayStroke,
textInputAction: TextInputAction.next,
keyboardType: TextInputType.name,
textCapitalization: TextCapitalization.sentences,
showError: formErrors.containsKey(WagesFormFields.firstName),
helperText: formErrors[WagesFormFields.firstName],
),
const Gap(8),
KwTextInput(
initialValue: form.middleName,
onChanged: (middleName) {
updateCurrentForm(form.copyWith(middleName: middleName));
},
title: 'Middle Initial (if any)',
hintText: 'Enter your Middle name',
borderColor: AppColors.grayStroke,
textInputAction: TextInputAction.next,
keyboardType: TextInputType.name,
textCapitalization: TextCapitalization.sentences,
),
const Gap(8),
KwTextInput(
initialValue: form.address,
onChanged: (address) {
updateCurrentForm(form.copyWith(address: address));
},
title: 'Address',
hintText: 'Enter your Address name',
borderColor: AppColors.grayStroke,
textInputAction: TextInputAction.next,
keyboardType: TextInputType.name,
textCapitalization: TextCapitalization.sentences,
showError: formErrors.containsKey(WagesFormFields.address),
helperText: formErrors[WagesFormFields.address],
),
const Gap(8),
KwTextInput(
initialValue: form.city,
onChanged: (city) {
updateCurrentForm(form.copyWith(city: city));
},
title: 'City or Town',
hintText: 'Enter your City',
borderColor: AppColors.grayStroke,
textInputAction: TextInputAction.next,
keyboardType: TextInputType.name,
textCapitalization: TextCapitalization.sentences,
showError: formErrors.containsKey(WagesFormFields.city),
helperText: formErrors[WagesFormFields.city],
),
const Gap(8),
KwDropdown<String>(
title: 'State',
horizontalPadding: 16,
borderColor: AppColors.grayStroke,
hintText: 'Select state',
selectedItem: form.state.toDropDownItem(),
items: [
for (final state in usStates)
KwDropDownItem(data: state, title: state),
],
onSelected: (state) {
updateCurrentForm(form.copyWith(state: state));
},
showError: formErrors.containsKey(WagesFormFields.state),
helperText: formErrors[WagesFormFields.state],
),
const Gap(8),
KwTextInput(
initialValue: form.zipCode,
onChanged: (zipCode) {
updateCurrentForm(form.copyWith(zipCode: zipCode));
},
title: 'ZIP Code',
hintText: 'Enter your City\'s ZIP code',
borderColor: AppColors.grayStroke,
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
showError: formErrors.containsKey(WagesFormFields.zipCode),
helperText: formErrors[WagesFormFields.zipCode],
),
const Gap(8),
KwTextInput(
initialValue: form.socialNumber,
onChanged: (socialNumber) {
updateCurrentForm(form.copyWith(socialNumber: socialNumber));
},
title: 'U.S. Social Number',
hintText: 'Enter your U.S. Social Number',
borderColor: AppColors.grayStroke,
textInputAction: TextInputAction.next,
showError: formErrors.containsKey(WagesFormFields.socialNumber),
helperText: formErrors[WagesFormFields.socialNumber],
),
const Gap(24),
const FormSignHolderWidget(signingPageTitle: 'W-4'),
const Gap(24),
KwDateSelector(
readOnly: true,
title: 'Todays Date',
hintText: 'mm/dd/yyyy',
initialValue: DateTime.now(),
onSelect: (date) {},
),
const Gap(24),
KwButton.outlinedPrimary(
label: 'Document Preview',
leftIcon: Assets.images.icons.eye,
onPressed: () {
context.pushRoute(const FormPreviewRoute());
},
),
],
);
},
),
bottomNavigationBar: SafeArea(
top: false,
child: Padding(
padding: const EdgeInsets.all(16),
child: KwButton.primary(label: 'Submit', onPressed: handleSubmit),
),
),
);
}
}

View File

@@ -0,0 +1,18 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow/features/profile/wages_forms/domain/bloc/wages_form_bloc.dart';
@RoutePage()
class WagesFormsFlowScreen extends StatelessWidget {
const WagesFormsFlowScreen({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) =>
WagesFormBloc()..add(const InitializeWagesFormBloc()),
child: const AutoRouter(),
);
}
}

View File

@@ -0,0 +1,48 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow/core/presentation/gen/assets.gen.dart';
import 'package:krow/core/presentation/styles/kw_box_decorations.dart';
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
import 'package:krow/features/profile/wages_forms/domain/bloc/wages_form_bloc.dart';
import 'package:krow/features/profile/wages_forms/domain/entities/wage_form_entity.dart';
class FormItemWidget extends StatelessWidget {
const FormItemWidget({
super.key,
required this.formRoute,
required this.label,
required this.wageForm,
});
final PageRouteInfo<Object?> formRoute;
final WageFormEntity wageForm;
final String label;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
context.pushRoute(formRoute);
context.read<WagesFormBloc>().add(SetActiveForm(form: wageForm));
},
child: Container(
height: 56,
decoration: KwBoxDecorations.primaryLight8,
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsetsDirectional.fromSTEB(12, 16, 12, 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: AppTextStyles.headingH3,
),
Assets.images.icons.caretRight.svg(width: 24, height: 24)
],
),
),
);
}
}

View File

@@ -0,0 +1,61 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:gap/gap.dart';
import 'package:krow/core/application/routing/routes.gr.dart';
import 'package:krow/core/data/enums/state_status.dart';
import 'package:krow/core/presentation/styles/theme.dart';
import 'package:krow/features/profile/wages_forms/domain/bloc/wages_form_bloc.dart';
import 'package:krow/features/profile/wages_forms/presentation/widgets/sign_notice_widget.dart';
class FormSignHolderWidget extends StatelessWidget {
const FormSignHolderWidget({super.key, required this.signingPageTitle});
final String signingPageTitle;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => context.router.push(FormSignRoute(title: signingPageTitle)),
child: BlocBuilder<WagesFormBloc, WagesFormState>(
buildWhen: (previous, current) =>
previous.signImageData != current.signImageData ||
current.status == StateStatus.error,
builder: (context, state) {
final signImage = state.signImageData;
final shouldShowError =
state.status == StateStatus.error && signImage == null;
return Container(
decoration: BoxDecoration(
color: AppColors.grayPrimaryFrame,
borderRadius: BorderRadius.circular(12),
border: shouldShowError
? Border.all(color: AppColors.statusError)
: null,
),
height: 106,
padding: const EdgeInsets.all(12),
child: signImage == null
? const SignNoticeWidget()
: Column(
children: [
Expanded(
child: Align(
alignment: AlignmentDirectional.centerStart,
child: Image.memory(signImage),
),
),
const Gap(6),
const Divider(
height: 1,
color: AppColors.grayTintStroke,
),
],
),
);
},
),
);
}
}

View File

@@ -0,0 +1,34 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:krow/core/presentation/gen/assets.gen.dart';
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
import 'package:krow/core/presentation/styles/theme.dart';
class SignNoticeWidget extends StatelessWidget {
const SignNoticeWidget({super.key});
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Assets.images.icons.edit.svg(),
const Gap(6),
const Text(
'Tap to Sign',
style: AppTextStyles.bodyMediumMed,
)
],
),
const Gap(12),
const Divider(
height: 1,
color: AppColors.grayTintStroke,
),
],
);
}
}