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

@@ -9,7 +9,9 @@ import '../../domain/repositories/availability_repository.dart';
/// Note: The backend schema supports recurring availablity (Weekly/DayOfWeek),
/// not specific date availability. Therefore, updating availability for a specific
/// date will update the availability for that Day of Week globally (Recurring).
class AvailabilityRepositoryImpl implements AvailabilityRepository {
class AvailabilityRepositoryImpl
with dc.DataErrorHandler
implements AvailabilityRepository {
final dc.ExampleConnector _dataConnect;
final firebase.FirebaseAuth _firebaseAuth;
@@ -21,84 +23,89 @@ class AvailabilityRepositoryImpl implements AvailabilityRepository {
Future<String> _getStaffId() async {
final firebase.User? user = _firebaseAuth.currentUser;
if (user == null) throw Exception('User not authenticated');
if (user == null) {
throw NotAuthenticatedException(
technicalMessage: 'User not authenticated');
}
final QueryResult<dc.GetStaffByUserIdData, dc.GetStaffByUserIdVariables> result =
await _dataConnect.getStaffByUserId(userId: user.uid).execute();
if (result.data.staffs.isEmpty) {
throw Exception('Staff profile not found');
throw const ServerException(technicalMessage: 'Staff profile not found');
}
return result.data.staffs.first.id;
}
@override
Future<List<DayAvailability>> getAvailability(DateTime start, DateTime end) async {
final String staffId = await _getStaffId();
// 1. Fetch Weekly recurring availability
final QueryResult<dc.ListStaffAvailabilitiesByStaffIdData, dc.ListStaffAvailabilitiesByStaffIdVariables> result =
await _dataConnect.listStaffAvailabilitiesByStaffId(staffId: staffId).limit(100).execute();
final List<dc.ListStaffAvailabilitiesByStaffIdStaffAvailabilities> items = result.data.staffAvailabilities;
// 2. Map to lookup: DayOfWeek -> Map<SlotName, IsAvailable>
final Map<dc.DayOfWeek, Map<dc.AvailabilitySlot, bool>> weeklyMap = {};
for (final item in items) {
dc.DayOfWeek day;
try {
day = dc.DayOfWeek.values.byName(item.day.stringValue);
} catch (_) {
continue;
}
dc.AvailabilitySlot slot;
try {
slot = dc.AvailabilitySlot.values.byName(item.slot.stringValue);
} catch (_) {
continue;
}
bool isAvailable = false;
try {
final dc.AvailabilityStatus status = dc.AvailabilityStatus.values.byName(item.status.stringValue);
isAvailable = _statusToBool(status);
} catch (_) {
isAvailable = false;
}
if (!weeklyMap.containsKey(day)) {
weeklyMap[day] = {};
}
weeklyMap[day]![slot] = isAvailable;
}
// 3. Generate DayAvailability for requested range
final List<DayAvailability> days = [];
final int dayCount = end.difference(start).inDays;
for (int i = 0; i <= dayCount; i++) {
final DateTime date = start.add(Duration(days: i));
final dc.DayOfWeek dow = _toBackendDay(date.weekday);
final Map<dc.AvailabilitySlot, bool> daySlots = weeklyMap[dow] ?? {};
return executeProtected(() async {
final String staffId = await _getStaffId();
// We define 3 standard slots for every day
final List<AvailabilitySlot> slots = [
_createSlot(date, dow, daySlots, dc.AvailabilitySlot.MORNING),
_createSlot(date, dow, daySlots, dc.AvailabilitySlot.AFTERNOON),
_createSlot(date, dow, daySlots, dc.AvailabilitySlot.EVENING),
];
// 1. Fetch Weekly recurring availability
final QueryResult<dc.ListStaffAvailabilitiesByStaffIdData, dc.ListStaffAvailabilitiesByStaffIdVariables> result =
await _dataConnect.listStaffAvailabilitiesByStaffId(staffId: staffId).limit(100).execute();
final bool isDayAvailable = slots.any((s) => s.isAvailable);
final List<dc.ListStaffAvailabilitiesByStaffIdStaffAvailabilities> items = result.data.staffAvailabilities;
days.add(DayAvailability(
date: date,
isAvailable: isDayAvailable,
slots: slots,
));
}
return days;
// 2. Map to lookup: DayOfWeek -> Map<SlotName, IsAvailable>
final Map<dc.DayOfWeek, Map<dc.AvailabilitySlot, bool>> weeklyMap = {};
for (final item in items) {
dc.DayOfWeek day;
try {
day = dc.DayOfWeek.values.byName(item.day.stringValue);
} catch (_) {
continue;
}
dc.AvailabilitySlot slot;
try {
slot = dc.AvailabilitySlot.values.byName(item.slot.stringValue);
} catch (_) {
continue;
}
bool isAvailable = false;
try {
final dc.AvailabilityStatus status = dc.AvailabilityStatus.values.byName(item.status.stringValue);
isAvailable = _statusToBool(status);
} catch (_) {
isAvailable = false;
}
if (!weeklyMap.containsKey(day)) {
weeklyMap[day] = {};
}
weeklyMap[day]![slot] = isAvailable;
}
// 3. Generate DayAvailability for requested range
final List<DayAvailability> days = [];
final int dayCount = end.difference(start).inDays;
for (int i = 0; i <= dayCount; i++) {
final DateTime date = start.add(Duration(days: i));
final dc.DayOfWeek dow = _toBackendDay(date.weekday);
final Map<dc.AvailabilitySlot, bool> daySlots = weeklyMap[dow] ?? {};
// We define 3 standard slots for every day
final List<AvailabilitySlot> slots = [
_createSlot(date, dow, daySlots, dc.AvailabilitySlot.MORNING),
_createSlot(date, dow, daySlots, dc.AvailabilitySlot.AFTERNOON),
_createSlot(date, dow, daySlots, dc.AvailabilitySlot.EVENING),
];
final bool isDayAvailable = slots.any((s) => s.isAvailable);
days.add(DayAvailability(
date: date,
isAvailable: isDayAvailable,
slots: slots,
));
}
return days;
});
}
AvailabilitySlot _createSlot(
@@ -113,75 +120,79 @@ class AvailabilityRepositoryImpl implements AvailabilityRepository {
@override
Future<DayAvailability> updateDayAvailability(DayAvailability availability) async {
final String staffId = await _getStaffId();
final dc.DayOfWeek dow = _toBackendDay(availability.date.weekday);
return executeProtected(() async {
final String staffId = await _getStaffId();
final dc.DayOfWeek dow = _toBackendDay(availability.date.weekday);
// Update each slot in the backend.
// This updates the recurring rule for this DayOfWeek.
for (final AvailabilitySlot slot in availability.slots) {
final dc.AvailabilitySlot slotEnum = _toBackendSlot(slot.id);
final dc.AvailabilityStatus status = _boolToStatus(slot.isAvailable);
// Update each slot in the backend.
// This updates the recurring rule for this DayOfWeek.
for (final AvailabilitySlot slot in availability.slots) {
final dc.AvailabilitySlot slotEnum = _toBackendSlot(slot.id);
final dc.AvailabilityStatus status = _boolToStatus(slot.isAvailable);
await _upsertSlot(staffId, dow, slotEnum, status);
}
await _upsertSlot(staffId, dow, slotEnum, status);
}
return availability;
return availability;
});
}
@override
Future<List<DayAvailability>> applyQuickSet(DateTime start, DateTime end, String type) async {
final String staffId = await _getStaffId();
// QuickSet updates the Recurring schedule for all days involved.
// However, if the user selects a range that covers e.g. Mon-Fri, we update Mon-Fri.
final int dayCount = end.difference(start).inDays;
final Set<dc.DayOfWeek> processedDays = {};
final List<DayAvailability> resultDays = [];
return executeProtected(() async {
final String staffId = await _getStaffId();
// QuickSet updates the Recurring schedule for all days involved.
// However, if the user selects a range that covers e.g. Mon-Fri, we update Mon-Fri.
final int dayCount = end.difference(start).inDays;
final Set<dc.DayOfWeek> processedDays = {};
final List<DayAvailability> resultDays = [];
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);
}
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);
}
// 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),
];
// 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),
];
resultDays.add(DayAvailability(
date: date,
isAvailable: enableDay,
slots: slots,
));
}
resultDays.add(DayAvailability(
date: date,
isAvailable: enableDay,
slots: slots,
));
}
return resultDays;
return resultDays;
});
}
Future<void> _upsertSlot(String staffId, dc.DayOfWeek day, dc.AvailabilitySlot slot, dc.AvailabilityStatus status) async {

View File

@@ -1,3 +1,4 @@
import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -41,6 +42,7 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
@override
Widget build(BuildContext context) {
Translations.of(context);
return BlocProvider.value(
value: _bloc,
child: Scaffold(
@@ -61,6 +63,14 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
behavior: SnackBarBehavior.floating,
),
);
} 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>(
@@ -105,7 +115,24 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
],
);
} else if (state is AvailabilityError) {
return Center(child: Text('Error: ${state.message}'));
return Center(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
translateErrorKey(state.message),
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 16,
color: AppColors.krowMuted,
),
),
],
),
),
);
}
return const SizedBox.shrink();
},