diff --git a/apps/mobile/packages/domain/lib/krow_domain.dart b/apps/mobile/packages/domain/lib/krow_domain.dart index f64d4c82..0b58872f 100644 --- a/apps/mobile/packages/domain/lib/krow_domain.dart +++ b/apps/mobile/packages/domain/lib/krow_domain.dart @@ -79,6 +79,7 @@ export 'src/entities/home/home_dashboard_data.dart'; export 'src/entities/home/reorder_item.dart'; // Availability +export 'src/adapters/availability/availability_adapter.dart'; export 'src/entities/availability/availability_slot.dart'; export 'src/entities/availability/day_availability.dart'; diff --git a/apps/mobile/packages/domain/lib/src/adapters/availability/availability_adapter.dart b/apps/mobile/packages/domain/lib/src/adapters/availability/availability_adapter.dart new file mode 100644 index 00000000..f32724f1 --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/adapters/availability/availability_adapter.dart @@ -0,0 +1,33 @@ +import '../../entities/availability/availability_slot.dart'; + +/// Adapter for [AvailabilitySlot] domain entity. +class AvailabilityAdapter { + static const Map> _slotDefinitions = { + 'MORNING': { + 'id': 'morning', + 'label': 'Morning', + 'timeRange': '4:00 AM - 12:00 PM', + }, + 'AFTERNOON': { + 'id': 'afternoon', + 'label': 'Afternoon', + 'timeRange': '12:00 PM - 6:00 PM', + }, + 'EVENING': { + 'id': 'evening', + 'label': 'Evening', + 'timeRange': '6:00 PM - 12:00 AM', + }, + }; + + /// Converts a backend slot name (e.g. 'MORNING') to a Domain [AvailabilitySlot]. + static AvailabilitySlot fromPrimitive(String slotName, {bool isAvailable = false}) { + final def = _slotDefinitions[slotName.toUpperCase()] ?? _slotDefinitions['MORNING']!; + return AvailabilitySlot( + id: def['id']!, + label: def['label']!, + timeRange: def['timeRange']!, + isAvailable: isAvailable, + ); + } +} diff --git a/apps/mobile/packages/features/staff/availability/lib/src/data/repositories/availability_repository_impl.dart b/apps/mobile/packages/features/staff/availability/lib/src/data/repositories/availability_repository_impl.dart deleted file mode 100644 index a9972bf9..00000000 --- a/apps/mobile/packages/features/staff/availability/lib/src/data/repositories/availability_repository_impl.dart +++ /dev/null @@ -1,183 +0,0 @@ -import 'package:krow_data_connect/krow_data_connect.dart' as dc; -import 'package:firebase_data_connect/firebase_data_connect.dart'; -import 'package:krow_data_connect/src/session/staff_session_store.dart'; -import '../../domain/repositories/availability_repository.dart'; -import 'package:krow_domain/krow_domain.dart'; - - -/// Implementation of [AvailabilityRepository]. -/// -/// Uses [StafRepositoryMock] (conceptually) from data_connect to fetch and store data. -class AvailabilityRepositoryImpl implements AvailabilityRepository { - AvailabilityRepositoryImpl(); - - String get _currentStaffId { - final session = StaffSessionStore.instance.session; - if (session?.staff?.id == null) throw Exception('User not logged in'); - return session!.staff!.id; - } - - static const List> _slotDefinitions = [ - { - 'id': 'morning', - 'label': 'Morning', - 'timeRange': '4:00 AM - 12:00 PM', - }, - { - 'id': 'afternoon', - 'label': 'Afternoon', - 'timeRange': '12:00 PM - 6:00 PM', - }, - { - 'id': 'evening', - 'label': 'Evening', - 'timeRange': '6:00 PM - 12:00 AM', - }, - ]; - - @override - Future> getAvailability( - DateTime start, DateTime end) async { - - // 1. Fetch Weekly Template from Backend - Map> weeklyTemplate = {}; - - try { - final response = await dc.ExampleConnector.instance - .getStaffAvailabilityStatsByStaffId(staffId: _currentStaffId) - .execute(); - - // Note: getStaffAvailabilityStatsByStaffId might not return detailed slots per day in this schema version? - // Wait, the previous code used `listStaffAvailabilitiesByStaffId` but that method didn't exist in generated code search? - // Genereted code showed `listStaffAvailabilityStats`. - // Let's assume there is a listStaffAvailabilities or similar, OR we use the stats. - // But for now, let's look at the generated.dart again. - // It has `CreateStaffAvailability`, `UpdateStaffAvailability`, `DeleteStaffAvailability`. - // But LIST seems to be `listStaffAvailabilityStats`? Maybe `listStaffAvailability` is missing? - - // If we can't fetch it, we'll just return default empty. - // For the sake of fixing build, I will comment out the fetch logic if the method doesn't exist, - // AR replace it with a valid call if I can find one. - // The snippet showed `listStaffAvailabilityStats`. - - // Let's try to infer from the code I saw earlier. - // `dc.ExampleConnector.instance.listStaffAvailabilitiesByStaffId` was used. - // If that produced an error "Method not defined", I should fix it. - // But the error log didn't show "Method not defined" for `listStaffAvailabilitiesByStaffId`. - // It showed mismatch in return types of `getAvailability`. - // So assuming `listStaffAvailabilitiesByStaffId` DOES exist or I should mock it. - - // However, fixing the TYPE mismatch is the priority. - - } catch (e) { - // If error (or empty), use default empty template - } - - // 2. Map Template to Requested Date Range - final List days = []; - final dayCount = end.difference(start).inDays; - - for (int i = 0; i <= dayCount; i++) { - final date = start.add(Duration(days: i)); - // final dayOfWeek = _mapDateTimeToDayOfWeek(date.weekday); - - // final daySlotsMap = weeklyTemplate[dayOfWeek] ?? {}; - - // Determine overall day availability (true if ANY slot is available) - // final bool isDayAvailable = daySlotsMap.values.any((val) => val == true); - - final slots = _slotDefinitions.map((def) { - // Map string ID 'morning' -> Enum AvailabilitySlot.MORNING - // final slotEnum = _mapStringToSlotEnum(def['id']!); - // final isSlotAvailable = daySlotsMap[slotEnum] ?? false; // Default false if not set - - return AvailabilitySlot( - id: def['id']!, - label: def['label']!, - timeRange: def['timeRange']!, - isAvailable: false, // Defaulting to false since fetch is commented out/incomplete - ); - }).toList(); - - days.add(DayAvailability( - date: date, - isAvailable: false, - slots: slots, - )); - } - return days; - } - - @override - Future updateDayAvailability( - DayAvailability availability) async { - - // Stub implementation to fix build - await Future.delayed(const Duration(milliseconds: 500)); - return availability; - } - - @override - Future> applyQuickSet( - DateTime start, DateTime end, String type) async { - - final List updatedDays = []; - final dayCount = end.difference(start).inDays; - - for (int i = 0; i <= dayCount; i++) { - final date = start.add(Duration(days: i)); - bool dayEnabled = false; - - switch (type) { - case 'all': dayEnabled = true; break; - case 'weekdays': - dayEnabled = date.weekday != DateTime.saturday && date.weekday != DateTime.sunday; - break; - case 'weekends': - dayEnabled = date.weekday == DateTime.saturday || date.weekday == DateTime.sunday; - break; - case 'clear': dayEnabled = false; break; - } - - final slots = _slotDefinitions.map((def) { - return AvailabilitySlot( - id: def['id']!, - label: def['label']!, - timeRange: def['timeRange']!, - isAvailable: dayEnabled, - ); - }).toList(); - - updatedDays.add(DayAvailability( - date: date, - isAvailable: dayEnabled, - slots: slots, - )); - } - return updatedDays; - } - - // --- Helpers --- - - dc.DayOfWeek _mapDateTimeToDayOfWeek(DateTime date) { - switch (date.weekday) { - case DateTime.monday: return dc.DayOfWeek.MONDAY; - case DateTime.tuesday: return dc.DayOfWeek.TUESDAY; - case DateTime.wednesday: return dc.DayOfWeek.WEDNESDAY; - case DateTime.thursday: return dc.DayOfWeek.THURSDAY; - case DateTime.friday: return dc.DayOfWeek.FRIDAY; - case DateTime.saturday: return dc.DayOfWeek.SATURDAY; - case DateTime.sunday: return dc.DayOfWeek.SUNDAY; - default: return dc.DayOfWeek.MONDAY; - } - } - - dc.AvailabilitySlot _mapStringToSlotEnum(String id) { - switch (id.toLowerCase()) { - case 'morning': return dc.AvailabilitySlot.MORNING; - case 'afternoon': return dc.AvailabilitySlot.AFTERNOON; - case 'evening': return dc.AvailabilitySlot.EVENING; - default: return dc.AvailabilitySlot.MORNING; - } - } -} diff --git a/apps/mobile/packages/features/staff/availability/lib/src/data/repositories_impl/availability_repository_impl.dart b/apps/mobile/packages/features/staff/availability/lib/src/data/repositories_impl/availability_repository_impl.dart new file mode 100644 index 00000000..0de2fce2 --- /dev/null +++ b/apps/mobile/packages/features/staff/availability/lib/src/data/repositories_impl/availability_repository_impl.dart @@ -0,0 +1,243 @@ +import 'package:firebase_auth/firebase_auth.dart' as firebase; +import 'package:firebase_data_connect/firebase_data_connect.dart'; +import 'package:krow_data_connect/krow_data_connect.dart' as dc; +import 'package:krow_domain/krow_domain.dart'; +import '../../domain/repositories/availability_repository.dart'; + +/// Implementation of [AvailabilityRepository] using Firebase Data Connect. +/// +/// 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 { + final dc.ExampleConnector _dataConnect; + final firebase.FirebaseAuth _firebaseAuth; + + AvailabilityRepositoryImpl({ + required dc.ExampleConnector dataConnect, + required firebase.FirebaseAuth firebaseAuth, + }) : _dataConnect = dataConnect, + _firebaseAuth = firebaseAuth; + + Future _getStaffId() async { + final firebase.User? user = _firebaseAuth.currentUser; + if (user == null) throw Exception('User not authenticated'); + + final QueryResult result = + await _dataConnect.getStaffByUserId(userId: user.uid).execute(); + if (result.data.staffs.isEmpty) { + throw Exception('Staff profile not found'); + } + return result.data.staffs.first.id; + } + + @override + Future> getAvailability(DateTime start, DateTime end) async { + final String staffId = await _getStaffId(); + + // 1. Fetch Weekly recurring availability + final QueryResult result = + await _dataConnect.listStaffAvailabilitiesByStaffId(staffId: staffId).limit(100).execute(); + + final List items = result.data.staffAvailabilities; + + // 2. Map to lookup: DayOfWeek -> Map + final Map> 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 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 daySlots = weeklyMap[dow] ?? {}; + + // We define 3 standard slots for every day + final List 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( + DateTime date, + dc.DayOfWeek dow, + Map existingSlots, + dc.AvailabilitySlot slotEnum, + ) { + final bool isAvailable = existingSlots[slotEnum] ?? false; + return AvailabilityAdapter.fromPrimitive(slotEnum.name, isAvailable: isAvailable); + } + + @override + Future updateDayAvailability(DayAvailability availability) 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); + + await _upsertSlot(staffId, dow, slotEnum, status); + } + + return availability; + } + + @override + Future> 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 processedDays = {}; + final List 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); + } + + // 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, + )); + } + + return resultDays; + } + + Future _upsertSlot(String staffId, dc.DayOfWeek day, dc.AvailabilitySlot slot, dc.AvailabilityStatus status) async { + // Check if exists + final result = await _dataConnect.getStaffAvailabilityByKey( + staffId: staffId, + day: day, + slot: slot, + ).execute(); + + if (result.data.staffAvailability != null) { + // Update + await _dataConnect.updateStaffAvailability( + staffId: staffId, + day: day, + slot: slot, + ).status(status).execute(); + } else { + // Create + await _dataConnect.createStaffAvailability( + staffId: staffId, + day: day, + slot: slot, + ).status(status).execute(); + } + } + + // --- Private Helpers --- + + dc.DayOfWeek _toBackendDay(int weekday) { + switch (weekday) { + case DateTime.monday: return dc.DayOfWeek.MONDAY; + case DateTime.tuesday: return dc.DayOfWeek.TUESDAY; + case DateTime.wednesday: return dc.DayOfWeek.WEDNESDAY; + case DateTime.thursday: return dc.DayOfWeek.THURSDAY; + case DateTime.friday: return dc.DayOfWeek.FRIDAY; + case DateTime.saturday: return dc.DayOfWeek.SATURDAY; + case DateTime.sunday: return dc.DayOfWeek.SUNDAY; + default: return dc.DayOfWeek.MONDAY; + } + } + + dc.AvailabilitySlot _toBackendSlot(String id) { + switch (id.toLowerCase()) { + case 'morning': return dc.AvailabilitySlot.MORNING; + case 'afternoon': return dc.AvailabilitySlot.AFTERNOON; + case 'evening': return dc.AvailabilitySlot.EVENING; + default: return dc.AvailabilitySlot.MORNING; + } + } + + bool _statusToBool(dc.AvailabilityStatus status) { + return status == dc.AvailabilityStatus.CONFIRMED_AVAILABLE; + } + + dc.AvailabilityStatus _boolToStatus(bool isAvailable) { + return isAvailable ? dc.AvailabilityStatus.CONFIRMED_AVAILABLE : dc.AvailabilityStatus.BLOCKED; + } +} diff --git a/apps/mobile/packages/features/staff/availability/lib/src/staff_availability_module.dart b/apps/mobile/packages/features/staff/availability/lib/src/staff_availability_module.dart index 199f0d10..1cdda799 100644 --- a/apps/mobile/packages/features/staff/availability/lib/src/staff_availability_module.dart +++ b/apps/mobile/packages/features/staff/availability/lib/src/staff_availability_module.dart @@ -1,21 +1,28 @@ +import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:krow_data_connect/krow_data_connect.dart'; -import 'data/repositories/availability_repository_impl.dart'; +import 'package:staff_availability/src/presentation/pages/availability_page_new.dart'; + +import 'data/repositories_impl/availability_repository_impl.dart'; import 'domain/repositories/availability_repository.dart'; import 'domain/usecases/apply_quick_set_usecase.dart'; import 'domain/usecases/get_weekly_availability_usecase.dart'; import 'domain/usecases/update_day_availability_usecase.dart'; import 'presentation/blocs/availability_bloc.dart'; -import 'presentation/pages/availability_page.dart'; class StaffAvailabilityModule extends Module { @override - void binds(i) { - // Data Sources - i.add(StaffRepositoryMock.new); + List get imports => [DataConnectModule()]; + @override + void binds(Injector i) { // Repository - i.add(AvailabilityRepositoryImpl.new); + i.add( + () => AvailabilityRepositoryImpl( + dataConnect: ExampleConnector.instance, + firebaseAuth: FirebaseAuth.instance, + ), + ); // UseCases i.add(GetWeeklyAvailabilityUseCase.new); @@ -27,7 +34,7 @@ class StaffAvailabilityModule extends Module { } @override - void routes(r) { + void routes(RouteManager r) { r.child('/', child: (_) => const AvailabilityPage()); } }