feat: introduce TaxForm entity and adapter, refactor tax forms repository and use cases for improved data handling

This commit is contained in:
Achintha Isuru
2026-01-27 16:25:47 -05:00
parent 35c0b19d6b
commit 078f828aad
12 changed files with 253 additions and 166 deletions

View File

@@ -54,6 +54,7 @@ export 'src/entities/profile/staff_document.dart';
export 'src/entities/profile/attire_item.dart';
export 'src/entities/profile/relationship_type.dart';
export 'src/entities/profile/industry.dart';
export 'src/entities/profile/tax_form.dart';
// Ratings & Penalties
export 'src/entities/ratings/staff_rating.dart';
@@ -85,3 +86,4 @@ export 'src/adapters/profile/emergency_contact_adapter.dart';
export 'src/adapters/profile/experience_adapter.dart';
export 'src/entities/profile/experience_skill.dart';
export 'src/adapters/profile/bank_account_adapter.dart';
export 'src/adapters/profile/tax_form_adapter.dart';

View File

@@ -0,0 +1,86 @@
import '../../entities/profile/tax_form.dart';
/// Adapter for [TaxForm] to map data layer values to domain entity.
class TaxFormAdapter {
/// Maps primitive values to [TaxForm].
static TaxForm fromPrimitives({
required String id,
required String type,
required String title,
String? subtitle,
String? description,
required String status,
String? staffId,
dynamic formData,
DateTime? createdAt,
DateTime? updatedAt,
}) {
return TaxForm(
id: id,
type: _stringToType(type),
title: title,
subtitle: subtitle,
description: description,
status: _stringToStatus(status),
staffId: staffId,
formData: formData is Map ? Map<String, dynamic>.from(formData) : null,
createdAt: createdAt,
updatedAt: updatedAt,
);
}
static TaxFormType _stringToType(String? value) {
if (value == null) return TaxFormType.i9;
try {
return TaxFormType.values.firstWhere(
(TaxFormType e) => e.name.toLowerCase() == value.toLowerCase(),
orElse: () => TaxFormType.i9,
);
} catch (_) {
return TaxFormType.i9;
}
}
static TaxFormStatus _stringToStatus(String? value) {
if (value == null) return TaxFormStatus.notStarted;
try {
final String normalizedValue = value.replaceAll('_', '').toLowerCase();
// map DRAFT to inProgress
if (normalizedValue == 'draft') return TaxFormStatus.inProgress;
return TaxFormStatus.values.firstWhere(
(TaxFormStatus e) {
// Handle differences like not_started vs notStarted if any,
// but standardizing to lowercase is a good start.
// The enum names are camelCase in Dart, but might be SNAKE_CASE from backend.
final String normalizedEnum = e.name.toLowerCase();
return normalizedValue == normalizedEnum;
},
orElse: () => TaxFormStatus.notStarted,
);
} catch (_) {
return TaxFormStatus.notStarted;
}
}
/// Converts domain [TaxFormType] to string for backend.
static String typeToString(TaxFormType type) {
return type.name.toUpperCase();
}
/// Converts domain [TaxFormStatus] to string for backend.
static String statusToString(TaxFormStatus status) {
switch (status) {
case TaxFormStatus.notStarted:
return 'NOT_STARTED';
case TaxFormStatus.inProgress:
return 'DRAFT';
case TaxFormStatus.submitted:
return 'SUBMITTED';
case TaxFormStatus.approved:
return 'APPROVED';
case TaxFormStatus.rejected:
return 'REJECTED';
}
}
}

View File

@@ -0,0 +1,45 @@
import 'package:equatable/equatable.dart';
enum TaxFormType { i9, w4 }
enum TaxFormStatus { notStarted, inProgress, submitted, approved, rejected }
class TaxForm extends Equatable {
final String id;
final TaxFormType type;
final String title;
final String? subtitle;
final String? description;
final TaxFormStatus status;
final String? staffId;
final Map<String, dynamic>? formData;
final DateTime? createdAt;
final DateTime? updatedAt;
const TaxForm({
required this.id,
required this.type,
required this.title,
this.subtitle,
this.description,
this.status = TaxFormStatus.notStarted,
this.staffId,
this.formData,
this.createdAt,
this.updatedAt,
});
@override
List<Object?> get props => [
id,
type,
title,
subtitle,
description,
status,
staffId,
formData,
createdAt,
updatedAt,
];
}

View File

@@ -1,10 +1,10 @@
import 'dart:async';
import 'package:firebase_auth/firebase_auth.dart' hide User;
import 'package:firebase_auth/firebase_auth.dart' as auth;
import 'package:firebase_data_connect/firebase_data_connect.dart';
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
import 'package:krow_domain/krow_domain.dart';
import '../../domain/entities/tax_form_entity.dart';
import '../../domain/repositories/tax_forms_repository.dart';
class TaxFormsRepositoryImpl implements TaxFormsRepository {
@@ -13,42 +13,34 @@ class TaxFormsRepositoryImpl implements TaxFormsRepository {
required this.dataConnect,
});
final FirebaseAuth firebaseAuth;
final auth.FirebaseAuth firebaseAuth;
final dc.ExampleConnector dataConnect;
String? _staffId;
Future<String> _getStaffId() async {
if (_staffId != null) return _staffId!;
final user = firebaseAuth.currentUser;
/// Helper to get the logged-in staff ID.
String _getStaffId() {
final auth.User? user = firebaseAuth.currentUser;
if (user == null) {
throw Exception('User not logged in');
throw Exception('User not authenticated');
}
final result =
await dataConnect.getStaffByUserId(userId: user.uid).execute();
final staffs = result.data.staffs;
if (staffs.isEmpty) {
// This might happen if the user is logged in but staff profile isn't created yet or not synced.
// For now, fail hard. Code can be improved to handle this case properly.
throw Exception('No staff profile found for user');
final String? staffId = dc.StaffSessionStore.instance.session?.staff?.id;
if (staffId == null || staffId.isEmpty) {
throw Exception('Staff profile is missing or session not initialized.');
}
_staffId = staffs.first.id;
return _staffId!;
return staffId;
}
@override
Future<List<TaxFormEntity>> getTaxForms() async {
final staffId = await _getStaffId();
final result =
Future<List<TaxForm>> getTaxForms() async {
final String staffId = _getStaffId();
final QueryResult<dc.GetTaxFormsBystaffIdData, dc.GetTaxFormsBystaffIdVariables>
result =
await dataConnect.getTaxFormsBystaffId(staffId: staffId).execute();
final forms = result.data.taxForms.map((e) => _mapToEntity(e)).toList();
final List<TaxForm> forms = result.data.taxForms.map((dc.GetTaxFormsBystaffIdTaxForms e) => _mapToEntity(e)).toList();
// Check if required forms exist, create if not.
final typesPresent = forms.map((f) => f.type).toSet();
final Set<TaxFormType> typesPresent = forms.map((TaxForm f) => f.type).toSet();
bool createdNew = false;
if (!typesPresent.contains(TaxFormType.i9)) {
@@ -61,9 +53,10 @@ class TaxFormsRepositoryImpl implements TaxFormsRepository {
}
if (createdNew) {
final result2 =
final QueryResult<dc.GetTaxFormsBystaffIdData, dc.GetTaxFormsBystaffIdVariables>
result2 =
await dataConnect.getTaxFormsBystaffId(staffId: staffId).execute();
return result2.data.taxForms.map((e) => _mapToEntity(e)).toList();
return result2.data.taxForms.map((dc.GetTaxFormsBystaffIdTaxForms e) => _mapToEntity(e)).toList();
}
return forms;
@@ -87,7 +80,7 @@ class TaxFormsRepositoryImpl implements TaxFormsRepository {
await dataConnect
.createTaxForm(
staffId: staffId,
formType: _mapTypeToGenerated(type),
formType: dc.TaxFormType.values.byName(TaxFormAdapter.typeToString(type)),
title: title,
)
.subtitle(subtitle)
@@ -98,13 +91,14 @@ class TaxFormsRepositoryImpl implements TaxFormsRepository {
@override
Future<void> submitForm(TaxFormType type, Map<String, dynamic> data) async {
final staffId = await _getStaffId();
final result =
final String staffId = _getStaffId();
final QueryResult<dc.GetTaxFormsBystaffIdData, dc.GetTaxFormsBystaffIdVariables>
result =
await dataConnect.getTaxFormsBystaffId(staffId: staffId).execute();
final targetTypeString = _mapTypeToGenerated(type).name;
final String targetTypeString = TaxFormAdapter.typeToString(type);
final form = result.data.taxForms.firstWhere(
(e) => e.formType.stringValue == targetTypeString,
final dc.GetTaxFormsBystaffIdTaxForms form = result.data.taxForms.firstWhere(
(dc.GetTaxFormsBystaffIdTaxForms e) => e.formType.stringValue == targetTypeString,
orElse: () => throw Exception('Form not found for submission'),
);
@@ -120,13 +114,14 @@ class TaxFormsRepositoryImpl implements TaxFormsRepository {
@override
Future<void> updateFormStatus(TaxFormType type, TaxFormStatus status) async {
final staffId = await _getStaffId();
final result =
final String staffId = _getStaffId();
final QueryResult<dc.GetTaxFormsBystaffIdData, dc.GetTaxFormsBystaffIdVariables>
result =
await dataConnect.getTaxFormsBystaffId(staffId: staffId).execute();
final targetTypeString = _mapTypeToGenerated(type).name;
final String targetTypeString = TaxFormAdapter.typeToString(type);
final form = result.data.taxForms.firstWhere(
(e) => e.formType.stringValue == targetTypeString,
final dc.GetTaxFormsBystaffIdTaxForms form = result.data.taxForms.firstWhere(
(dc.GetTaxFormsBystaffIdTaxForms e) => e.formType.stringValue == targetTypeString,
orElse: () => throw Exception('Form not found for update'),
);
@@ -134,73 +129,22 @@ class TaxFormsRepositoryImpl implements TaxFormsRepository {
.updateTaxForm(
id: form.id,
)
.status(_mapStatusToGenerated(status))
.status(dc.TaxFormStatus.values.byName(TaxFormAdapter.statusToString(status)))
.execute();
}
TaxFormEntity _mapToEntity(dc.GetTaxFormsBystaffIdTaxForms form) {
return TaxFormEntity(
type: _mapTypeFromString(form.formType.stringValue),
TaxForm _mapToEntity(dc.GetTaxFormsBystaffIdTaxForms form) {
return TaxFormAdapter.fromPrimitives(
id: form.id,
type: form.formType.stringValue,
title: form.title,
subtitle: form.subtitle ?? '',
description: form.description ?? '',
status: _mapStatusFromString(form.status.stringValue),
lastUpdated: form.updatedAt?.toDateTime(),
subtitle: form.subtitle,
description: form.description,
status: form.status.stringValue,
staffId: form.staffId,
formData: form.formData, // Adapter expects dynamic
updatedAt: form.updatedAt?.toDateTime(),
);
}
TaxFormType _mapTypeFromString(String value) {
switch (value) {
case 'I9':
return TaxFormType.i9;
case 'W4':
return TaxFormType.w4;
default:
// Handle unexpected types gracefully if needed, or throw.
// Assuming database integrity for now.
if (value == 'I-9') return TaxFormType.i9; // Fallback just in case
throw Exception('Unknown TaxFormType string: $value');
}
}
TaxFormStatus _mapStatusFromString(String value) {
switch (value) {
case 'NOT_STARTED':
return TaxFormStatus.notStarted;
case 'DRAFT':
return TaxFormStatus.inProgress;
case 'SUBMITTED':
return TaxFormStatus.submitted;
case 'APPROVED':
return TaxFormStatus.approved;
case 'REJECTED':
return TaxFormStatus.rejected;
default:
return TaxFormStatus.notStarted; // Default fallback
}
}
dc.TaxFormType _mapTypeToGenerated(TaxFormType type) {
switch (type) {
case TaxFormType.i9:
return dc.TaxFormType.I9;
case TaxFormType.w4:
return dc.TaxFormType.W4;
}
}
dc.TaxFormStatus _mapStatusToGenerated(TaxFormStatus status) {
switch (status) {
case TaxFormStatus.notStarted:
return dc.TaxFormStatus.NOT_STARTED;
case TaxFormStatus.inProgress:
return dc.TaxFormStatus.DRAFT;
case TaxFormStatus.submitted:
return dc.TaxFormStatus.SUBMITTED;
case TaxFormStatus.approved:
return dc.TaxFormStatus.APPROVED;
case TaxFormStatus.rejected:
return dc.TaxFormStatus.REJECTED;
}
}
}

View File

@@ -1,7 +1,7 @@
import '../entities/tax_form_entity.dart';
import 'package:krow_domain/krow_domain.dart';
abstract class TaxFormsRepository {
Future<List<TaxFormEntity>> getTaxForms();
Future<List<TaxForm>> getTaxForms();
Future<void> submitForm(TaxFormType type, Map<String, dynamic> data);
Future<void> updateFormStatus(TaxFormType type, TaxFormStatus status);
}

View File

@@ -1,4 +1,4 @@
import '../entities/tax_form_entity.dart';
import 'package:krow_domain/krow_domain.dart';
import '../repositories/tax_forms_repository.dart';
class GetTaxFormsUseCase {
@@ -6,7 +6,7 @@ class GetTaxFormsUseCase {
GetTaxFormsUseCase(this._repository);
Future<List<TaxFormEntity>> call() async {
Future<List<TaxForm>> call() async {
return _repository.getTaxForms();
}
}

View File

@@ -1,4 +1,4 @@
import '../entities/tax_form_entity.dart';
import 'package:krow_domain/krow_domain.dart';
import '../repositories/tax_forms_repository.dart';
class SubmitTaxFormUseCase {

View File

@@ -1,5 +1,6 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../domain/entities/tax_form_entity.dart';
import 'package:krow_domain/krow_domain.dart';
import '../../../domain/usecases/submit_tax_form_usecase.dart';
import 'form_i9_state.dart';

View File

@@ -1,5 +1,5 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../domain/entities/tax_form_entity.dart';
import 'package:krow_domain/krow_domain.dart';
import '../../../domain/usecases/get_tax_forms_usecase.dart';
import 'tax_forms_state.dart';
@@ -11,7 +11,7 @@ class TaxFormsCubit extends Cubit<TaxFormsState> {
Future<void> loadTaxForms() async {
emit(state.copyWith(status: TaxFormsStatus.loading));
try {
final List<TaxFormEntity> forms = await _getTaxFormsUseCase();
final List<TaxForm> forms = await _getTaxFormsUseCase();
emit(state.copyWith(
status: TaxFormsStatus.success,
forms: forms,

View File

@@ -1,22 +1,22 @@
import 'package:equatable/equatable.dart';
import '../../../domain/entities/tax_form_entity.dart';
import 'package:krow_domain/krow_domain.dart';
enum TaxFormsStatus { initial, loading, success, failure }
class TaxFormsState extends Equatable {
final TaxFormsStatus status;
final List<TaxFormEntity> forms;
final List<TaxForm> forms;
final String? errorMessage;
const TaxFormsState({
this.status = TaxFormsStatus.initial,
this.forms = const <TaxFormEntity>[],
this.forms = const <TaxForm>[],
this.errorMessage,
});
TaxFormsState copyWith({
TaxFormsStatus? status,
List<TaxFormEntity>? forms,
List<TaxForm>? forms,
String? errorMessage,
}) {
return TaxFormsState(

View File

@@ -1,5 +1,6 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../domain/entities/tax_form_entity.dart';
import 'package:krow_domain/krow_domain.dart';
import '../../../domain/usecases/submit_tax_form_usecase.dart';
import 'form_w4_state.dart';

View File

@@ -2,7 +2,7 @@ import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import '../../domain/entities/tax_form_entity.dart';
import 'package:krow_domain/krow_domain.dart';
import '../blocs/tax_forms/tax_forms_cubit.dart';
import '../blocs/tax_forms/tax_forms_state.dart';
@@ -11,13 +11,7 @@ class TaxFormsPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final TaxFormsCubit cubit = Modular.get<TaxFormsCubit>();
if (cubit.state.status == TaxFormsStatus.initial) {
cubit.loadTaxForms();
}
return Scaffold(
backgroundColor: UiColors.background,
appBar: AppBar(
backgroundColor: UiColors.primary,
elevation: 0,
@@ -27,9 +21,7 @@ class TaxFormsPage extends StatelessWidget {
),
title: Text(
'Tax Documents',
style: UiTypography.headline3m.copyWith(
color: UiColors.bgPopup,
),
style: UiTypography.headline3m.copyWith(color: UiColors.bgPopup),
),
bottom: PreferredSize(
preferredSize: const Size.fromHeight(24),
@@ -54,40 +46,53 @@ class TaxFormsPage extends StatelessWidget {
),
),
),
body: BlocBuilder<TaxFormsCubit, TaxFormsState>(
bloc: Modular.get<TaxFormsCubit>(),
builder: (BuildContext context, TaxFormsState state) {
if (state.status == TaxFormsStatus.loading) {
return const Center(child: CircularProgressIndicator());
body: BlocProvider<TaxFormsCubit>(
create: (BuildContext context) {
final TaxFormsCubit cubit = Modular.get<TaxFormsCubit>();
if (cubit.state.status == TaxFormsStatus.initial) {
cubit.loadTaxForms();
}
if (state.status == TaxFormsStatus.failure) {
return Center(child: Text(state.errorMessage ?? 'Error loading forms'));
}
return SingleChildScrollView(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space5,
vertical: UiConstants.space6,
),
child: Column(
children: <Widget>[
_buildProgressOverview(state.forms),
const SizedBox(height: UiConstants.space6),
...state.forms.map(_buildFormCard),
const SizedBox(height: UiConstants.space6),
_buildInfoCard(),
],
),
);
return cubit;
},
child: BlocBuilder<TaxFormsCubit, TaxFormsState>(
builder: (BuildContext context, TaxFormsState state) {
if (state.status == TaxFormsStatus.loading) {
return const Center(child: CircularProgressIndicator());
}
if (state.status == TaxFormsStatus.failure) {
return Center(
child: Text(state.errorMessage ?? 'Error loading forms'),
);
}
return SingleChildScrollView(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space5,
vertical: UiConstants.space6,
),
child: Column(
spacing: UiConstants.space6,
children: <Widget>[
_buildProgressOverview(state.forms),
...state.forms.map(_buildFormCard),
_buildInfoCard(),
],
),
);
},
),
),
);
}
Widget _buildProgressOverview(List<TaxFormEntity> forms) {
Widget _buildProgressOverview(List<TaxForm> forms) {
final int completedCount = forms
.where((TaxFormEntity f) => f.status == TaxFormStatus.submitted || f.status == TaxFormStatus.approved)
.where(
(TaxForm f) =>
f.status == TaxFormStatus.submitted ||
f.status == TaxFormStatus.approved,
)
.length;
final int totalCount = forms.length;
final double progress = totalCount > 0 ? completedCount / totalCount : 0.0;
@@ -112,8 +117,9 @@ class TaxFormsPage extends StatelessWidget {
),
Text(
'$completedCount/$totalCount',
style:
UiTypography.body2m.copyWith(color: UiColors.textSecondary),
style: UiTypography.body2m.copyWith(
color: UiColors.textSecondary,
),
),
],
),
@@ -124,9 +130,7 @@ class TaxFormsPage extends StatelessWidget {
value: progress,
minHeight: 8,
backgroundColor: UiColors.background,
valueColor: const AlwaysStoppedAnimation<Color>(
UiColors.primary,
),
valueColor: const AlwaysStoppedAnimation<Color>(UiColors.primary),
),
),
],
@@ -134,10 +138,10 @@ class TaxFormsPage extends StatelessWidget {
);
}
Widget _buildFormCard(TaxFormEntity form) {
Widget _buildFormCard(TaxForm form) {
// Helper to get icon based on type (could be in entity or a mapper)
final String icon = form.type == TaxFormType.i9 ? '🛂' : '📋';
return GestureDetector(
onTap: () {
if (form.type == TaxFormType.i9) {
@@ -164,9 +168,7 @@ class TaxFormsPage extends StatelessWidget {
color: UiColors.primary.withOpacity(0.1),
borderRadius: UiConstants.radiusLg,
),
child: Center(
child: Text(icon, style: UiTypography.headline1m),
),
child: Center(child: Text(icon, style: UiTypography.headline1m)),
),
const SizedBox(width: UiConstants.space4),
Expanded(
@@ -187,7 +189,7 @@ class TaxFormsPage extends StatelessWidget {
),
const SizedBox(height: UiConstants.space1),
Text(
form.subtitle,
form.subtitle ?? '',
style: UiTypography.body2m.copyWith(
fontWeight: FontWeight.w500,
color: UiColors.textSecondary,
@@ -195,7 +197,7 @@ class TaxFormsPage extends StatelessWidget {
),
const SizedBox(height: UiConstants.space1),
Text(
form.description,
form.description ?? '',
style: UiTypography.body3r.copyWith(
color: UiColors.textSecondary,
),
@@ -231,7 +233,11 @@ class TaxFormsPage extends StatelessWidget {
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
const Icon(UiIcons.success, size: 12, color: UiColors.textSuccess),
const Icon(
UiIcons.success,
size: 12,
color: UiColors.textSuccess,
),
const SizedBox(width: UiConstants.space1),
Text(
'Completed',
@@ -311,7 +317,9 @@ class TaxFormsPage extends StatelessWidget {
const SizedBox(height: UiConstants.space1),
Text(
'I-9 and W-4 forms are required by federal law to verify your employment eligibility and set up correct tax withholding.',
style: UiTypography.body3r.copyWith(color: UiColors.textSecondary),
style: UiTypography.body3r.copyWith(
color: UiColors.textSecondary,
),
),
],
),