feat: complete centralized error handling system with documentation
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user