feat: Centralized Error Handling & Crash Fixes

This commit is contained in:
2026-02-11 18:52:23 +05:30
parent ea06510474
commit c1112ac01c
51 changed files with 2104 additions and 960 deletions

View File

@@ -14,6 +14,7 @@ class AvailabilityRepositoryImpl
implements AvailabilityRepository {
final dc.ExampleConnector _dataConnect;
final firebase.FirebaseAuth _firebaseAuth;
String? _cachedStaffId;
AvailabilityRepositoryImpl({
required dc.ExampleConnector dataConnect,
@@ -22,6 +23,8 @@ class AvailabilityRepositoryImpl
_firebaseAuth = firebaseAuth;
Future<String> _getStaffId() async {
if (_cachedStaffId != null) return _cachedStaffId!;
final firebase.User? user = _firebaseAuth.currentUser;
if (user == null) {
throw NotAuthenticatedException(
@@ -33,7 +36,8 @@ class AvailabilityRepositoryImpl
if (result.data.staffs.isEmpty) {
throw const ServerException(technicalMessage: 'Staff profile not found');
}
return result.data.staffs.first.id;
_cachedStaffId = result.data.staffs.first.id;
return _cachedStaffId!;
}
@override
@@ -149,48 +153,51 @@ class AvailabilityRepositoryImpl
final Set<dc.DayOfWeek> processedDays = {};
final List<DayAvailability> resultDays = [];
final List<Future<void>> futures = [];
for (int i = 0; i <= dayCount; i++) {
final DateTime date = start.add(Duration(days: i));
final dc.DayOfWeek dow = _toBackendDay(date.weekday);
// Logic to determine if enabled based on type
bool enableDay = false;
if (type == 'all') enableDay = true;
else if (type == 'clear') enableDay = false;
else if (type == 'weekdays') {
enableDay = (dow != dc.DayOfWeek.SATURDAY && dow != dc.DayOfWeek.SUNDAY);
} else if (type == 'weekends') {
enableDay = (dow == dc.DayOfWeek.SATURDAY || dow == dc.DayOfWeek.SUNDAY);
}
final DateTime date = start.add(Duration(days: i));
final dc.DayOfWeek dow = _toBackendDay(date.weekday);
// Only update backend once per DayOfWeek (since it's recurring)
// to avoid redundant calls if range > 1 week.
if (!processedDays.contains(dow)) {
processedDays.add(dow);
final dc.AvailabilityStatus status = _boolToStatus(enableDay);
await Future.wait([
_upsertSlot(staffId, dow, dc.AvailabilitySlot.MORNING, status),
_upsertSlot(staffId, dow, dc.AvailabilitySlot.AFTERNOON, status),
_upsertSlot(staffId, dow, dc.AvailabilitySlot.EVENING, status),
]);
}
// Prepare return object
final slots = [
AvailabilityAdapter.fromPrimitive('MORNING', isAvailable: enableDay),
AvailabilityAdapter.fromPrimitive('AFTERNOON', isAvailable: enableDay),
AvailabilityAdapter.fromPrimitive('EVENING', isAvailable: enableDay),
];
// Logic to determine if enabled based on type
bool enableDay = false;
if (type == 'all') {
enableDay = true;
} else if (type == 'clear') {
enableDay = false;
} else if (type == 'weekdays') {
enableDay = (dow != dc.DayOfWeek.SATURDAY && dow != dc.DayOfWeek.SUNDAY);
} else if (type == 'weekends') {
enableDay = (dow == dc.DayOfWeek.SATURDAY || dow == dc.DayOfWeek.SUNDAY);
}
resultDays.add(DayAvailability(
date: date,
isAvailable: enableDay,
slots: slots,
));
// Only update backend once per DayOfWeek (since it's recurring)
if (!processedDays.contains(dow)) {
processedDays.add(dow);
final dc.AvailabilityStatus status = _boolToStatus(enableDay);
futures.add(_upsertSlot(staffId, dow, dc.AvailabilitySlot.MORNING, status));
futures.add(_upsertSlot(staffId, dow, dc.AvailabilitySlot.AFTERNOON, status));
futures.add(_upsertSlot(staffId, dow, dc.AvailabilitySlot.EVENING, status));
}
// Prepare return object
final slots = [
AvailabilityAdapter.fromPrimitive('MORNING', isAvailable: enableDay),
AvailabilityAdapter.fromPrimitive('AFTERNOON', isAvailable: enableDay),
AvailabilityAdapter.fromPrimitive('EVENING', isAvailable: enableDay),
];
resultDays.add(DayAvailability(
date: date,
isAvailable: enableDay,
slots: slots,
));
}
// Execute all updates in parallel
await Future.wait(futures);
return resultDays;
});
}

View File

@@ -42,12 +42,12 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
@override
Widget build(BuildContext context) {
Translations.of(context);
final i18n = Translations.of(context).staff.availability;
return BlocProvider.value(
value: _bloc,
child: Scaffold(
appBar: UiAppBar(
title: 'My Availability',
title: i18n.title,
centerTitle: false,
showBackButton: true,
),
@@ -64,17 +64,9 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
if (state is AvailabilityError) {
UiSnackbar.show(
context,
message: state.message,
message: translateErrorKey(state.message),
type: UiSnackbarType.error,
);
} else if (state is AvailabilityError) {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(translateErrorKey(state.message)),
behavior: SnackBarBehavior.floating,
),
);
}
},
child: BlocBuilder<AvailabilityBloc, AvailabilityState>(
@@ -110,7 +102,12 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
),
),
if (state.isActionInProgress)
const UiLoadingPage(), // Show loading overlay during actions
Positioned.fill(
child: Container(
color: UiColors.white.withValues(alpha: 0.5),
child: const Center(child: CircularProgressIndicator()),
),
),
],
);
} else if (state is AvailabilityError) {
@@ -128,7 +125,6 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
],
),
),
),
);
}
return const SizedBox.shrink();
@@ -140,6 +136,7 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
}
Widget _buildQuickSet(BuildContext context) {
final i18n = Translations.of(context).staff.availability;
return Container(
padding: const EdgeInsets.all(UiConstants.space4),
decoration: BoxDecoration(
@@ -150,26 +147,28 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Quick Set Availability',
i18n.quick_set_title,
style: UiTypography.body2b,
),
const SizedBox(height: UiConstants.space3),
Row(
children: [
Expanded(child: _buildQuickSetButton(context, 'All Week', 'all')),
const SizedBox(width: UiConstants.space2),
Expanded(
child: _buildQuickSetButton(context, 'Weekdays', 'weekdays'),
child: _buildQuickSetButton(context, i18n.all_week, 'all'),
),
const SizedBox(width: UiConstants.space2),
Expanded(
child: _buildQuickSetButton(context, 'Weekends', 'weekends'),
child: _buildQuickSetButton(context, i18n.weekdays, 'weekdays'),
),
const SizedBox(width: UiConstants.space2),
Expanded(
child: _buildQuickSetButton(context, i18n.weekends, 'weekends'),
),
const SizedBox(width: UiConstants.space2),
Expanded(
child: _buildQuickSetButton(
context,
'Clear All',
i18n.clear_all,
'clear',
isDestructive: true,
),
@@ -388,7 +387,15 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
style: UiTypography.title2b,
),
Text(
isAvailable ? 'You are available' : 'Not available',
isAvailable
? Translations.of(context)
.staff
.availability
.available_status
: Translations.of(context)
.staff
.availability
.not_available_status,
style: UiTypography.body2r.textSecondary,
),
],
@@ -560,6 +567,7 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
}
Widget _buildInfoCard() {
final i18n = Translations.of(context).staff.availability;
return Container(
padding: const EdgeInsets.all(UiConstants.space4),
decoration: BoxDecoration(
@@ -577,11 +585,11 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
spacing: UiConstants.space1,
children: [
Text(
'Auto-Match uses your availability',
i18n.auto_match_title,
style: UiTypography.body2m,
),
Text(
"When enabled, you'll only be matched with shifts during your available times.",
i18n.auto_match_description,
style: UiTypography.body3r.textSecondary,
),
],