refactor: centralize data connect error handling and resolve build issues across applications
This commit addresses several critical issues across the mobile monorepo:
1. Centralized Error Handling: Integrated DataErrorHandler mixin into all repository implementations, ensuring consistent mapping of Data Connect exceptions to domain AppExceptions.
2. Build Stabilization: Fixed numerous type mismatches, parameter signature errors in widgets (e.g., google_places_flutter itemBuilder), and naming conflicts (StaffSession, FirebaseAuth).
3. Code Quality: Applied 'dart fix' across all modified packages and manually cleared debug print statements and UI clutter.
4. Mono-repo alignment: Standardized Data Connect usage and aliasing ('dc.') for better maintainability.
Signed-off-by: Suriya <suriya@tenext.in>
This commit is contained in:
@@ -1,43 +0,0 @@
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../../domain/repositories/certificates_repository.dart';
|
||||
|
||||
class CertificatesRepositoryMock implements CertificatesRepository {
|
||||
@override
|
||||
Future<List<StaffDocument>> getCertificates() async {
|
||||
final DateTime now = DateTime.now();
|
||||
|
||||
// Create copies with dynamic dates
|
||||
final List<StaffDocument> dynamicDocuments = <StaffDocument>[
|
||||
StaffDocument(
|
||||
id: '1',
|
||||
documentId: 'background',
|
||||
staffId: 'current_user',
|
||||
name: 'Background Check',
|
||||
description: 'Required for all shifts',
|
||||
status: DocumentStatus.verified,
|
||||
expiryDate: now.add(const Duration(days: 365)),
|
||||
),
|
||||
StaffDocument(
|
||||
id: '2',
|
||||
documentId: 'food_handler',
|
||||
staffId: 'current_user',
|
||||
name: 'Food Handler',
|
||||
description: 'Required for food service',
|
||||
status: DocumentStatus.verified,
|
||||
expiryDate: now.add(const Duration(days: 15)),
|
||||
),
|
||||
const StaffDocument(
|
||||
id: '3',
|
||||
documentId: 'rbs',
|
||||
staffId: 'current_user',
|
||||
name: 'RBS Alcohol',
|
||||
description: 'Required for bar shifts',
|
||||
status: DocumentStatus.missing,
|
||||
expiryDate: null,
|
||||
),
|
||||
];
|
||||
|
||||
await Future<void>.delayed(const Duration(seconds: 1)); // Simulate network delay
|
||||
return dynamicDocuments;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import 'package:firebase_auth/firebase_auth.dart' as firebase_auth;
|
||||
import 'package:firebase_data_connect/firebase_data_connect.dart';
|
||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
@@ -14,21 +14,21 @@ import '../../domain/repositories/personal_info_repository_interface.dart';
|
||||
/// - Mapping between data_connect DTOs and domain entities
|
||||
/// - Containing no business logic
|
||||
class PersonalInfoRepositoryImpl implements PersonalInfoRepositoryInterface {
|
||||
final ExampleConnector _dataConnect;
|
||||
final FirebaseAuth _firebaseAuth;
|
||||
|
||||
/// Creates a [PersonalInfoRepositoryImpl].
|
||||
///
|
||||
/// Requires the Firebase Data Connect connector instance and Firebase Auth.
|
||||
PersonalInfoRepositoryImpl({
|
||||
required ExampleConnector dataConnect,
|
||||
required FirebaseAuth firebaseAuth,
|
||||
required firebase_auth.FirebaseAuth firebaseAuth,
|
||||
}) : _dataConnect = dataConnect,
|
||||
_firebaseAuth = firebaseAuth;
|
||||
final ExampleConnector _dataConnect;
|
||||
final firebase_auth.FirebaseAuth _firebaseAuth;
|
||||
|
||||
@override
|
||||
Future<Staff> getStaffProfile() async {
|
||||
final user = _firebaseAuth.currentUser;
|
||||
final firebase_auth.User? user = _firebaseAuth.currentUser;
|
||||
if (user == null) {
|
||||
throw Exception('User not authenticated');
|
||||
}
|
||||
@@ -41,7 +41,7 @@ class PersonalInfoRepositoryImpl implements PersonalInfoRepositoryInterface {
|
||||
throw Exception('Staff profile not found for User ID: ${user.uid}');
|
||||
}
|
||||
|
||||
final rawStaff = result.data.staffs.first;
|
||||
final GetStaffByUserIdStaffs rawStaff = result.data.staffs.first;
|
||||
|
||||
// Map from data_connect DTO to domain entity
|
||||
return _mapToStaffEntity(rawStaff);
|
||||
@@ -50,7 +50,7 @@ class PersonalInfoRepositoryImpl implements PersonalInfoRepositoryInterface {
|
||||
@override
|
||||
Future<Staff> updateStaffProfile({required String staffId, required Map<String, dynamic> data}) async {
|
||||
// Start building the update mutation
|
||||
var updateBuilder = _dataConnect.updateStaff(id: staffId);
|
||||
UpdateStaffVariablesBuilder updateBuilder = _dataConnect.updateStaff(id: staffId);
|
||||
|
||||
// Apply updates from map if present
|
||||
if (data.containsKey('name')) {
|
||||
|
||||
@@ -8,12 +8,12 @@ import '../repositories/personal_info_repository_interface.dart';
|
||||
/// which delegates to the data_connect layer for data access.
|
||||
class GetPersonalInfoUseCase
|
||||
implements NoInputUseCase<Staff> {
|
||||
final PersonalInfoRepositoryInterface _repository;
|
||||
|
||||
/// Creates a [GetPersonalInfoUseCase].
|
||||
///
|
||||
/// Requires a [PersonalInfoRepositoryInterface] to fetch data.
|
||||
GetPersonalInfoUseCase(this._repository);
|
||||
final PersonalInfoRepositoryInterface _repository;
|
||||
|
||||
@override
|
||||
Future<Staff> call() {
|
||||
|
||||
@@ -4,19 +4,19 @@ import '../repositories/personal_info_repository_interface.dart';
|
||||
|
||||
/// Arguments for updating staff profile information.
|
||||
class UpdatePersonalInfoParams extends UseCaseArgument {
|
||||
|
||||
const UpdatePersonalInfoParams({
|
||||
required this.staffId,
|
||||
required this.data,
|
||||
});
|
||||
/// The staff member's ID.
|
||||
final String staffId;
|
||||
|
||||
/// The fields to update.
|
||||
final Map<String, dynamic> data;
|
||||
|
||||
const UpdatePersonalInfoParams({
|
||||
required this.staffId,
|
||||
required this.data,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [staffId, data];
|
||||
List<Object?> get props => <Object?>[staffId, data];
|
||||
}
|
||||
|
||||
/// Use case for updating staff profile information.
|
||||
@@ -25,12 +25,12 @@ class UpdatePersonalInfoParams extends UseCaseArgument {
|
||||
/// through the repository, which delegates to the data_connect layer.
|
||||
class UpdatePersonalInfoUseCase
|
||||
implements UseCase<UpdatePersonalInfoParams, Staff> {
|
||||
final PersonalInfoRepositoryInterface _repository;
|
||||
|
||||
/// Creates an [UpdatePersonalInfoUseCase].
|
||||
///
|
||||
/// Requires a [PersonalInfoRepositoryInterface] to update data.
|
||||
UpdatePersonalInfoUseCase(this._repository);
|
||||
final PersonalInfoRepositoryInterface _repository;
|
||||
|
||||
@override
|
||||
Future<Staff> call(UpdatePersonalInfoParams params) {
|
||||
|
||||
@@ -14,8 +14,6 @@ import 'personal_info_state.dart';
|
||||
/// use cases following Clean Architecture principles.
|
||||
class PersonalInfoBloc extends Bloc<PersonalInfoEvent, PersonalInfoState>
|
||||
implements Disposable {
|
||||
final GetPersonalInfoUseCase _getPersonalInfoUseCase;
|
||||
final UpdatePersonalInfoUseCase _updatePersonalInfoUseCase;
|
||||
|
||||
/// Creates a [PersonalInfoBloc].
|
||||
///
|
||||
@@ -33,6 +31,8 @@ class PersonalInfoBloc extends Bloc<PersonalInfoEvent, PersonalInfoState>
|
||||
|
||||
add(const PersonalInfoLoadRequested());
|
||||
}
|
||||
final GetPersonalInfoUseCase _getPersonalInfoUseCase;
|
||||
final UpdatePersonalInfoUseCase _updatePersonalInfoUseCase;
|
||||
|
||||
/// Handles loading staff profile information.
|
||||
Future<void> _onLoadRequested(
|
||||
@@ -45,13 +45,13 @@ class PersonalInfoBloc extends Bloc<PersonalInfoEvent, PersonalInfoState>
|
||||
|
||||
// Initialize form values from staff entity
|
||||
// Note: Staff entity currently stores address as a string, but we want to map it to 'preferredLocations'
|
||||
final Map<String, dynamic> initialValues = {
|
||||
final Map<String, dynamic> initialValues = <String, dynamic>{
|
||||
'name': staff.name,
|
||||
'email': staff.email,
|
||||
'phone': staff.phone,
|
||||
'preferredLocations': staff.address != null
|
||||
? [staff.address]
|
||||
: [], // TODO: Map correctly when Staff entity supports list
|
||||
? <String?>[staff.address]
|
||||
: <dynamic>[], // TODO: Map correctly when Staff entity supports list
|
||||
'avatar': staff.avatar,
|
||||
};
|
||||
|
||||
@@ -95,13 +95,13 @@ class PersonalInfoBloc extends Bloc<PersonalInfoEvent, PersonalInfoState>
|
||||
);
|
||||
|
||||
// Update local state with the returned staff and keep form values in sync
|
||||
final Map<String, dynamic> newValues = {
|
||||
final Map<String, dynamic> newValues = <String, dynamic>{
|
||||
'name': updatedStaff.name,
|
||||
'email': updatedStaff.email,
|
||||
'phone': updatedStaff.phone,
|
||||
'preferredLocations': updatedStaff.address != null
|
||||
? [updatedStaff.address]
|
||||
: [],
|
||||
? <String?>[updatedStaff.address]
|
||||
: <dynamic>[],
|
||||
'avatar': updatedStaff.avatar,
|
||||
};
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ abstract class PersonalInfoEvent extends Equatable {
|
||||
const PersonalInfoEvent();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
List<Object?> get props => <Object?>[];
|
||||
}
|
||||
|
||||
/// Event to load personal information.
|
||||
@@ -15,16 +15,16 @@ class PersonalInfoLoadRequested extends PersonalInfoEvent {
|
||||
|
||||
/// Event to update a field value.
|
||||
class PersonalInfoFieldChanged extends PersonalInfoEvent {
|
||||
final String field;
|
||||
final dynamic value;
|
||||
|
||||
const PersonalInfoFieldChanged({
|
||||
required this.field,
|
||||
required this.value,
|
||||
});
|
||||
final String field;
|
||||
final dynamic value;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [field, value];
|
||||
List<Object?> get props => <Object?>[field, value];
|
||||
}
|
||||
|
||||
/// Event to submit the form.
|
||||
@@ -34,9 +34,9 @@ class PersonalInfoFormSubmitted extends PersonalInfoEvent {
|
||||
|
||||
/// Event when an address is selected from autocomplete.
|
||||
class PersonalInfoAddressSelected extends PersonalInfoEvent {
|
||||
final String address;
|
||||
const PersonalInfoAddressSelected(this.address);
|
||||
final String address;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [address];
|
||||
List<Object?> get props => <Object?>[address];
|
||||
}
|
||||
|
||||
@@ -29,6 +29,21 @@ enum PersonalInfoStatus {
|
||||
///
|
||||
/// Uses the shared [Staff] entity from the domain layer.
|
||||
class PersonalInfoState extends Equatable {
|
||||
|
||||
/// Creates a [PersonalInfoState].
|
||||
const PersonalInfoState({
|
||||
this.status = PersonalInfoStatus.initial,
|
||||
this.staff,
|
||||
this.formValues = const <String, dynamic>{},
|
||||
this.errorMessage,
|
||||
});
|
||||
|
||||
/// Initial state.
|
||||
const PersonalInfoState.initial()
|
||||
: status = PersonalInfoStatus.initial,
|
||||
staff = null,
|
||||
formValues = const <String, dynamic>{},
|
||||
errorMessage = null;
|
||||
/// The current status of the operation.
|
||||
final PersonalInfoStatus status;
|
||||
|
||||
@@ -41,21 +56,6 @@ class PersonalInfoState extends Equatable {
|
||||
/// Error message if an error occurred.
|
||||
final String? errorMessage;
|
||||
|
||||
/// Creates a [PersonalInfoState].
|
||||
const PersonalInfoState({
|
||||
this.status = PersonalInfoStatus.initial,
|
||||
this.staff,
|
||||
this.formValues = const {},
|
||||
this.errorMessage,
|
||||
});
|
||||
|
||||
/// Initial state.
|
||||
const PersonalInfoState.initial()
|
||||
: status = PersonalInfoStatus.initial,
|
||||
staff = null,
|
||||
formValues = const {},
|
||||
errorMessage = null;
|
||||
|
||||
/// Creates a copy of this state with the given fields replaced.
|
||||
PersonalInfoState copyWith({
|
||||
PersonalInfoStatus? status,
|
||||
@@ -72,5 +72,5 @@ class PersonalInfoState extends Equatable {
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [status, staff, formValues, errorMessage];
|
||||
List<Object?> get props => <Object?>[status, staff, formValues, errorMessage];
|
||||
}
|
||||
|
||||
@@ -18,14 +18,14 @@ import 'save_button.dart';
|
||||
/// following Clean Architecture's separation of concerns principle and the design system guidelines.
|
||||
/// Works with the shared [Staff] entity from the domain layer.
|
||||
class PersonalInfoContent extends StatefulWidget {
|
||||
/// The staff profile to display and edit.
|
||||
final Staff staff;
|
||||
|
||||
/// Creates a [PersonalInfoContent].
|
||||
const PersonalInfoContent({
|
||||
super.key,
|
||||
required this.staff,
|
||||
});
|
||||
/// The staff profile to display and edit.
|
||||
final Staff staff;
|
||||
|
||||
@override
|
||||
State<PersonalInfoContent> createState() => _PersonalInfoContentState();
|
||||
@@ -81,8 +81,8 @@ class _PersonalInfoContentState extends State<PersonalInfoContent> {
|
||||
// The backend expects List<AnyValue> (JSON/List) for preferredLocations
|
||||
final List<String> locations = _locationsController.text
|
||||
.split(',')
|
||||
.map((e) => e.trim())
|
||||
.where((e) => e.isNotEmpty)
|
||||
.map((String e) => e.trim())
|
||||
.where((String e) => e.isNotEmpty)
|
||||
.toList();
|
||||
|
||||
context.read<PersonalInfoBloc>().add(
|
||||
|
||||
@@ -9,6 +9,17 @@ import 'package:design_system/design_system.dart';
|
||||
/// and editable fields for phone and address.
|
||||
/// Uses only design system tokens for colors, typography, and spacing.
|
||||
class PersonalInfoForm extends StatelessWidget {
|
||||
|
||||
/// Creates a [PersonalInfoForm].
|
||||
const PersonalInfoForm({
|
||||
super.key,
|
||||
required this.fullName,
|
||||
required this.email,
|
||||
required this.emailController,
|
||||
required this.phoneController,
|
||||
required this.locationsController,
|
||||
this.enabled = true,
|
||||
});
|
||||
/// The staff member's full name (read-only).
|
||||
final String fullName;
|
||||
|
||||
@@ -27,17 +38,6 @@ class PersonalInfoForm extends StatelessWidget {
|
||||
/// Whether the form fields are enabled for editing.
|
||||
final bool enabled;
|
||||
|
||||
/// Creates a [PersonalInfoForm].
|
||||
const PersonalInfoForm({
|
||||
super.key,
|
||||
required this.fullName,
|
||||
required this.email,
|
||||
required this.emailController,
|
||||
required this.phoneController,
|
||||
required this.locationsController,
|
||||
this.enabled = true,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final TranslationsStaffOnboardingPersonalInfoEn i18n = t.staff.onboarding.personal_info;
|
||||
@@ -57,7 +57,7 @@ class PersonalInfoForm extends StatelessWidget {
|
||||
hint: i18n.email_label,
|
||||
enabled: enabled,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
autofillHints: const [AutofillHints.email],
|
||||
autofillHints: const <String>[AutofillHints.email],
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
|
||||
@@ -85,9 +85,9 @@ class PersonalInfoForm extends StatelessWidget {
|
||||
/// A label widget for form fields.
|
||||
/// A label widget for form fields.
|
||||
class _FieldLabel extends StatelessWidget {
|
||||
final String text;
|
||||
|
||||
const _FieldLabel({required this.text});
|
||||
final String text;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -101,9 +101,9 @@ class _FieldLabel extends StatelessWidget {
|
||||
/// A read-only field widget for displaying non-editable information.
|
||||
/// A read-only field widget for displaying non-editable information.
|
||||
class _ReadOnlyField extends StatelessWidget {
|
||||
final String value;
|
||||
|
||||
const _ReadOnlyField({required this.value});
|
||||
final String value;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -129,11 +129,6 @@ class _ReadOnlyField extends StatelessWidget {
|
||||
/// An editable text field widget.
|
||||
/// An editable text field widget.
|
||||
class _EditableField extends StatelessWidget {
|
||||
final TextEditingController controller;
|
||||
final String hint;
|
||||
final bool enabled;
|
||||
final TextInputType? keyboardType;
|
||||
final Iterable<String>? autofillHints;
|
||||
|
||||
const _EditableField({
|
||||
required this.controller,
|
||||
@@ -142,6 +137,11 @@ class _EditableField extends StatelessWidget {
|
||||
this.keyboardType,
|
||||
this.autofillHints,
|
||||
});
|
||||
final TextEditingController controller;
|
||||
final String hint;
|
||||
final bool enabled;
|
||||
final TextInputType? keyboardType;
|
||||
final Iterable<String>? autofillHints;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
@@ -9,14 +9,6 @@ import 'package:design_system/design_system.dart';
|
||||
/// Includes a camera icon button for changing the photo.
|
||||
/// Uses only design system tokens for colors, typography, and spacing.
|
||||
class ProfilePhotoWidget extends StatelessWidget {
|
||||
/// The URL of the staff member's photo.
|
||||
final String? photoUrl;
|
||||
|
||||
/// The staff member's full name (used for initial avatar).
|
||||
final String fullName;
|
||||
|
||||
/// Callback when the photo/camera button is tapped.
|
||||
final VoidCallback? onTap;
|
||||
|
||||
/// Creates a [ProfilePhotoWidget].
|
||||
const ProfilePhotoWidget({
|
||||
@@ -25,6 +17,14 @@ class ProfilePhotoWidget extends StatelessWidget {
|
||||
required this.fullName,
|
||||
required this.onTap,
|
||||
});
|
||||
/// The URL of the staff member's photo.
|
||||
final String? photoUrl;
|
||||
|
||||
/// The staff member's full name (used for initial avatar).
|
||||
final String fullName;
|
||||
|
||||
/// Callback when the photo/camera button is tapped.
|
||||
final VoidCallback? onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
@@ -7,14 +7,6 @@ import 'package:design_system/design_system.dart';
|
||||
/// Displays a full-width button with a save icon and customizable label.
|
||||
/// Uses only design system tokens for colors, typography, and spacing.
|
||||
class SaveButton extends StatelessWidget {
|
||||
/// Callback when the button is pressed.
|
||||
final VoidCallback? onPressed;
|
||||
|
||||
/// The button label text.
|
||||
final String label;
|
||||
|
||||
/// Whether to show a loading indicator.
|
||||
final bool isLoading;
|
||||
|
||||
/// Creates a [SaveButton].
|
||||
const SaveButton({
|
||||
@@ -23,6 +15,14 @@ class SaveButton extends StatelessWidget {
|
||||
required this.label,
|
||||
this.isLoading = false,
|
||||
});
|
||||
/// Callback when the button is pressed.
|
||||
final VoidCallback? onPressed;
|
||||
|
||||
/// The button label text.
|
||||
final String label;
|
||||
|
||||
/// Whether to show a loading indicator.
|
||||
final bool isLoading;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
/// Export the modular feature definition.
|
||||
library;
|
||||
export 'src/staff_profile_info_module.dart';
|
||||
|
||||
@@ -28,6 +28,8 @@ dependencies:
|
||||
krow_data_connect:
|
||||
path: ../../../../../data_connect
|
||||
|
||||
firebase_auth: any
|
||||
firebase_data_connect: any
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
Reference in New Issue
Block a user