feat: complete centralized error handling system with documentation

This commit is contained in:
2026-02-11 10:36:08 +05:30
parent 7570ffa3b9
commit 3e212220c7
43 changed files with 1144 additions and 2858 deletions

View File

@@ -14,7 +14,9 @@ import '../../domain/repositories/profile_repository.dart';
///
/// Currently uses [ProfileRepositoryMock] from data_connect.
/// When Firebase Data Connect is ready, this will be swapped with a real implementation.
class ProfileRepositoryImpl implements ProfileRepositoryInterface {
class ProfileRepositoryImpl
with DataErrorHandler
implements ProfileRepositoryInterface {
/// Creates a [ProfileRepositoryImpl].
///
/// Requires a [ExampleConnector] from the data_connect package and [FirebaseAuth].
@@ -31,37 +33,39 @@ class ProfileRepositoryImpl implements ProfileRepositoryInterface {
@override
Future<Staff> getStaffProfile() async {
final user = firebaseAuth.currentUser;
if (user == null) {
throw Exception('User not authenticated');
}
return executeProtected(() async {
final user = firebaseAuth.currentUser;
if (user == null) {
throw NotAuthenticatedException(
technicalMessage: 'User not authenticated');
}
final response = await connector.getStaffByUserId(userId: user.uid).execute();
if (response.data.staffs.isEmpty) {
// TODO: Handle user not found properly with domain exception
throw Exception('Staff not found');
}
final response = await connector.getStaffByUserId(userId: user.uid).execute();
if (response.data.staffs.isEmpty) {
throw const ServerException(technicalMessage: 'Staff not found');
}
final GetStaffByUserIdStaffs rawStaff = response.data.staffs.first;
final GetStaffByUserIdStaffs rawStaff = response.data.staffs.first;
// Map the raw data connect object to the Domain Entity
return Staff(
id: rawStaff.id,
authProviderId: rawStaff.userId,
name: rawStaff.fullName,
email: rawStaff.email ?? '',
phone: rawStaff.phone,
avatar: rawStaff.photoUrl,
status: StaffStatus.active,
address: rawStaff.addres,
totalShifts: rawStaff.totalShifts,
averageRating: rawStaff.averageRating,
onTimeRate: rawStaff.onTimeRate,
noShowCount: rawStaff.noShowCount,
cancellationCount: rawStaff.cancellationCount,
reliabilityScore: rawStaff.reliabilityScore,
);
// Map the raw data connect object to the Domain Entity
return Staff(
id: rawStaff.id,
authProviderId: rawStaff.userId,
name: rawStaff.fullName,
email: rawStaff.email ?? '',
phone: rawStaff.phone,
avatar: rawStaff.photoUrl,
status: StaffStatus.active,
address: rawStaff.addres,
totalShifts: rawStaff.totalShifts,
averageRating: rawStaff.averageRating,
onTimeRate: rawStaff.onTimeRate,
noShowCount: rawStaff.noShowCount,
cancellationCount: rawStaff.cancellationCount,
reliabilityScore: rawStaff.reliabilityScore,
);
});
}
@override

View File

@@ -50,7 +50,7 @@ class StaffProfilePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final TranslationsStaffProfileEn i18n = t.staff.profile;
final i18n = Translations.of(context).staff.profile;
final ProfileCubit cubit = Modular.get<ProfileCubit>();
// Load profile data on first build
@@ -64,6 +64,14 @@ class StaffProfilePage extends StatelessWidget {
listener: (context, state) {
if (state.status == ProfileStatus.signedOut) {
Modular.to.toGetStarted();
} else if (state.status == ProfileStatus.error &&
state.errorMessage != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(translateErrorKey(state.errorMessage!)),
behavior: SnackBarBehavior.floating,
),
);
}
},
builder: (context, state) {
@@ -74,10 +82,16 @@ class StaffProfilePage extends StatelessWidget {
if (state.status == ProfileStatus.error) {
return Center(
child: Text(
state.errorMessage ?? 'An error occurred',
style: UiTypography.body1r.copyWith(
color: UiColors.destructive,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
state.errorMessage != null
? translateErrorKey(state.errorMessage!)
: 'An error occurred',
textAlign: TextAlign.center,
style: UiTypography.body1r.copyWith(
color: UiColors.textSecondary,
),
),
),
);

View File

@@ -20,29 +20,31 @@ class LanguageSelectorBottomSheet extends StatelessWidget {
color: UiColors.background,
borderRadius: BorderRadius.vertical(top: Radius.circular(UiConstants.radiusBase)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
t.settings.change_language,
style: UiTypography.headline4m,
textAlign: TextAlign.center,
),
SizedBox(height: UiConstants.space6),
_buildLanguageOption(
context,
label: 'English',
locale: AppLocale.en,
),
SizedBox(height: UiConstants.space4),
_buildLanguageOption(
context,
label: 'Español',
locale: AppLocale.es,
),
SizedBox(height: UiConstants.space6),
],
child: SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
t.settings.change_language,
style: UiTypography.headline4m,
textAlign: TextAlign.center,
),
SizedBox(height: UiConstants.space6),
_buildLanguageOption(
context,
label: 'English',
locale: AppLocale.en,
),
SizedBox(height: UiConstants.space4),
_buildLanguageOption(
context,
label: 'Español',
locale: AppLocale.es,
),
SizedBox(height: UiConstants.space6),
],
),
),
);
}