diff --git a/apps/mobile/packages/data_connect/lib/krow_data_connect.dart b/apps/mobile/packages/data_connect/lib/krow_data_connect.dart index 277ad737..d512a29c 100644 --- a/apps/mobile/packages/data_connect/lib/krow_data_connect.dart +++ b/apps/mobile/packages/data_connect/lib/krow_data_connect.dart @@ -13,6 +13,7 @@ export 'src/session/client_session_store.dart'; // Export the generated Data Connect SDK export 'src/dataconnect_generated/generated.dart'; +export 'src/services/data_connect_service.dart'; export 'src/session/staff_session_store.dart'; export 'src/mixins/data_error_handler.dart'; diff --git a/apps/mobile/packages/data_connect/lib/src/services/data_connect_service.dart b/apps/mobile/packages/data_connect/lib/src/services/data_connect_service.dart new file mode 100644 index 00000000..cdfe2813 --- /dev/null +++ b/apps/mobile/packages/data_connect/lib/src/services/data_connect_service.dart @@ -0,0 +1,104 @@ +import 'dart:async'; + +import 'package:firebase_auth/firebase_auth.dart' as firebase_auth; +import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc; +import 'package:krow_core/core.dart'; + +import '../../krow_data_connect.dart' as dc; +import '../mixins/data_error_handler.dart'; + +/// A centralized service for interacting with Firebase Data Connect. +/// +/// This service provides common utilities and context management for all repositories. +class DataConnectService with DataErrorHandler { + DataConnectService._(); + + /// The singleton instance of the [DataConnectService]. + static final DataConnectService instance = DataConnectService._(); + + /// The Data Connect connector used for data operations. + final dc.ExampleConnector connector = dc.ExampleConnector.instance; + + /// The Firebase Auth instance. + final firebase_auth.FirebaseAuth _auth = firebase_auth.FirebaseAuth.instance; + + /// Cache for the current staff ID to avoid redundant lookups. + String? _cachedStaffId; + + /// Gets the current staff ID from session store or persistent storage. + Future getStaffId() async { + // 1. Check Session Store + final dc.StaffSession? session = dc.StaffSessionStore.instance.session; + if (session?.staff?.id != null) { + return session!.staff!.id; + } + + // 2. Check Cache + if (_cachedStaffId != null) return _cachedStaffId!; + + // 3. Fetch from Data Connect using Firebase UID + final firebase_auth.User? user = _auth.currentUser; + if (user == null) { + throw Exception('User is not authenticated'); + } + + try { + final fdc.QueryResult< + dc.GetStaffByUserIdData, + dc.GetStaffByUserIdVariables + > + response = await executeProtected( + () => connector.getStaffByUserId(userId: user.uid).execute(), + ); + + if (response.data.staffs.isNotEmpty) { + _cachedStaffId = response.data.staffs.first.id; + return _cachedStaffId!; + } + } catch (e) { + throw Exception('Failed to fetch staff ID from Data Connect: $e'); + } + + // 4. Fallback (should ideally not happen if DB is seeded) + return user.uid; + } + + /// Converts a Data Connect timestamp/string/json to a [DateTime]. + DateTime? toDateTime(dynamic t) { + if (t == null) return null; + DateTime? dt; + if (t is fdc.Timestamp) { + dt = t.toDateTime(); + } else if (t is String) { + dt = DateTime.tryParse(t); + } else { + try { + dt = DateTime.tryParse(t.toJson() as String); + } catch (_) { + try { + dt = DateTime.tryParse(t.toString()); + } catch (e) { + dt = null; + } + } + } + + if (dt != null) { + return DateTimeUtils.toDeviceTime(dt); + } + return null; + } + + /// Converts a [DateTime] to a Firebase Data Connect [Timestamp]. + fdc.Timestamp toTimestamp(DateTime dateTime) { + final DateTime utc = dateTime.toUtc(); + final int seconds = utc.millisecondsSinceEpoch ~/ 1000; + final int nanoseconds = (utc.microsecondsSinceEpoch % 1000000) * 1000; + return fdc.Timestamp(nanoseconds, seconds); + } + + /// Clears the internal cache (e.g., on logout). + void clearCache() { + _cachedStaffId = null; + } +} diff --git a/apps/mobile/packages/data_connect/pubspec.yaml b/apps/mobile/packages/data_connect/pubspec.yaml index d2b83d6a..48d0039b 100644 --- a/apps/mobile/packages/data_connect/pubspec.yaml +++ b/apps/mobile/packages/data_connect/pubspec.yaml @@ -16,3 +16,5 @@ dependencies: flutter_modular: ^6.3.0 firebase_data_connect: ^0.2.2+2 firebase_core: ^4.4.0 + firebase_auth: ^6.1.4 + krow_core: ^0.0.1 diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/data/repositories_impl/shifts_repository_impl.dart b/apps/mobile/packages/features/staff/shifts/lib/src/data/repositories_impl/shifts_repository_impl.dart index d500819a..64a112ca 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/data/repositories_impl/shifts_repository_impl.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/data/repositories_impl/shifts_repository_impl.dart @@ -1,85 +1,20 @@ import 'package:krow_data_connect/krow_data_connect.dart' as dc; import 'package:krow_domain/krow_domain.dart'; import 'package:intl/intl.dart'; -import 'package:firebase_auth/firebase_auth.dart' as firebase_auth; import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc; -import 'package:krow_core/core.dart'; import '../../domain/repositories/shifts_repository_interface.dart'; class ShiftsRepositoryImpl - with dc.DataErrorHandler implements ShiftsRepositoryInterface { - final dc.ExampleConnector _dataConnect; - final firebase_auth.FirebaseAuth _auth = firebase_auth.FirebaseAuth.instance; + final dc.DataConnectService _service; - ShiftsRepositoryImpl() : _dataConnect = dc.ExampleConnector.instance; + ShiftsRepositoryImpl() : _service = dc.DataConnectService.instance; // Cache: ShiftID -> ApplicationID (For Accept/Decline) final Map _shiftToAppIdMap = {}; // Cache: ApplicationID -> RoleID (For Accept/Decline w/ Update mutation) final Map _appToRoleIdMap = {}; - String? _cachedStaffId; - - Future _getStaffId() async { - // 1. Check Session Store - final dc.StaffSession? session = dc.StaffSessionStore.instance.session; - if (session?.staff?.id != null) { - return session!.staff!.id; - } - - // 2. Check Cache - if (_cachedStaffId != null) return _cachedStaffId!; - - // 3. Fetch from Data Connect using Firebase UID - final firebase_auth.User? user = _auth.currentUser; - if (user == null) { - throw Exception('User is not authenticated'); - } - - try { - final fdc.QueryResult response = await executeProtected(() => _dataConnect - .getStaffByUserId(userId: user.uid) - .execute()); - if (response.data.staffs.isNotEmpty) { - _cachedStaffId = response.data.staffs.first.id; - return _cachedStaffId!; - } - } catch (e) { - // Log or handle error - } - - // 4. Fallback (should ideally not happen if DB is seeded) - return user.uid; - } - - DateTime? _toDateTime(dynamic t) { - if (t == null) return null; - DateTime? dt; - if (t is fdc.Timestamp) { - dt = t.toDateTime(); - } else if (t is String) { - dt = DateTime.tryParse(t); - } else { - try { - dt = DateTime.tryParse(t.toJson() as String); - } catch (_) { - try { - dt = DateTime.tryParse(t.toString()); - } catch (e) { - dt = null; - } - } - } - - if (dt != null) { - final local = DateTimeUtils.toDeviceTime(dt); - - return local; - } - return null; - } - @override Future> getMyShifts({ required DateTime start, @@ -100,8 +35,8 @@ class ShiftsRepositoryImpl @override Future> getHistoryShifts() async { - final staffId = await _getStaffId(); - final fdc.QueryResult response = await executeProtected(() => _dataConnect + final staffId = await _service.getStaffId(); + final fdc.QueryResult response = await _service.executeProtected(() => _service.connector .listCompletedApplicationsByStaffId(staffId: staffId) .execute()); final List shifts = []; @@ -116,10 +51,10 @@ class ShiftsRepositoryImpl ? app.shift.order.eventName! : app.shift.order.business.businessName; final String title = '$roleName - $orderName'; - final DateTime? shiftDate = _toDateTime(app.shift.date); - final DateTime? startDt = _toDateTime(app.shiftRole.startTime); - final DateTime? endDt = _toDateTime(app.shiftRole.endTime); - final DateTime? createdDt = _toDateTime(app.createdAt); + final DateTime? shiftDate = _service.toDateTime(app.shift.date); + final DateTime? startDt = _service.toDateTime(app.shiftRole.startTime); + final DateTime? endDt = _service.toDateTime(app.shiftRole.endTime); + final DateTime? createdDt = _service.toDateTime(app.createdAt); shifts.add( Shift( @@ -157,12 +92,12 @@ class ShiftsRepositoryImpl DateTime? start, DateTime? end, }) async { - final staffId = await _getStaffId(); - var query = _dataConnect.getApplicationsByStaffId(staffId: staffId); + final staffId = await _service.getStaffId(); + var query = _service.connector.getApplicationsByStaffId(staffId: staffId); if (start != null && end != null) { - query = query.dayStart(_toTimestamp(start)).dayEnd(_toTimestamp(end)); + query = query.dayStart(_service.toTimestamp(start)).dayEnd(_service.toTimestamp(end)); } - final fdc.QueryResult response = await executeProtected(() => query.execute()); + final fdc.QueryResult response = await _service.executeProtected(() => query.execute()); final apps = response.data.applications; final List shifts = []; @@ -177,10 +112,10 @@ class ShiftsRepositoryImpl ? app.shift.order.eventName! : app.shift.order.business.businessName; final String title = '$roleName - $orderName'; - final DateTime? shiftDate = _toDateTime(app.shift.date); - final DateTime? startDt = _toDateTime(app.shiftRole.startTime); - final DateTime? endDt = _toDateTime(app.shiftRole.endTime); - final DateTime? createdDt = _toDateTime(app.createdAt); + final DateTime? shiftDate = _service.toDateTime(app.shift.date); + final DateTime? startDt = _service.toDateTime(app.shiftRole.startTime); + final DateTime? endDt = _service.toDateTime(app.shiftRole.endTime); + final DateTime? createdDt = _service.toDateTime(app.createdAt); // Override status to reflect the application state (e.g., CHECKED_OUT, CONFIRMED) final bool hasCheckIn = app.checkInTime != null; @@ -226,13 +161,6 @@ class ShiftsRepositoryImpl return shifts; } - fdc.Timestamp _toTimestamp(DateTime dateTime) { - final DateTime utc = dateTime.toUtc(); - final int seconds = utc.millisecondsSinceEpoch ~/ 1000; - final int nanoseconds = (utc.microsecondsSinceEpoch % 1000000) * 1000; - return fdc.Timestamp(nanoseconds, seconds); - } - String _mapStatus(dc.ApplicationStatus status) { switch (status) { case dc.ApplicationStatus.CONFIRMED: @@ -255,7 +183,7 @@ class ShiftsRepositoryImpl return []; } - final fdc.QueryResult result = await executeProtected(() => _dataConnect + final fdc.QueryResult result = await _service.executeProtected(() => _service.connector .listShiftRolesByVendorId(vendorId: vendorId) .execute()); final allShiftRoles = result.data.shiftRoles; @@ -263,10 +191,10 @@ class ShiftsRepositoryImpl final List mappedShifts = []; for (final sr in allShiftRoles) { - final DateTime? shiftDate = _toDateTime(sr.shift.date); - final startDt = _toDateTime(sr.startTime); - final endDt = _toDateTime(sr.endTime); - final createdDt = _toDateTime(sr.createdAt); + final DateTime? shiftDate = _service.toDateTime(sr.shift.date); + final startDt = _service.toDateTime(sr.startTime); + final endDt = _service.toDateTime(sr.endTime); + final createdDt = _service.toDateTime(sr.createdAt); mappedShifts.add( Shift( @@ -319,21 +247,21 @@ class ShiftsRepositoryImpl Future _getShiftDetails(String shiftId, {String? roleId}) async { if (roleId != null && roleId.isNotEmpty) { - final roleResult = await executeProtected(() => _dataConnect + final roleResult = await _service.executeProtected(() => _service.connector .getShiftRoleById(shiftId: shiftId, roleId: roleId) .execute()); final sr = roleResult.data.shiftRole; if (sr == null) return null; - final DateTime? startDt = _toDateTime(sr.startTime); - final DateTime? endDt = _toDateTime(sr.endTime); - final DateTime? createdDt = _toDateTime(sr.createdAt); + final DateTime? startDt = _service.toDateTime(sr.startTime); + final DateTime? endDt = _service.toDateTime(sr.endTime); + final DateTime? createdDt = _service.toDateTime(sr.createdAt); - final String staffId = await _getStaffId(); + final String staffId = await _service.getStaffId(); bool hasApplied = false; String status = 'open'; - final apps = await executeProtected(() => - _dataConnect.getApplicationsByStaffId(staffId: staffId).execute()); + final apps = await _service.executeProtected(() => + _service.connector.getApplicationsByStaffId(staffId: staffId).execute()); final app = apps.data.applications .where( (a) => a.shiftId == shiftId && a.shiftRole.roleId == roleId, @@ -378,7 +306,7 @@ class ShiftsRepositoryImpl } final fdc.QueryResult result = - await executeProtected(() => _dataConnect.getShiftById(id: shiftId).execute()); + await _service.executeProtected(() => _service.connector.getShiftById(id: shiftId).execute()); final s = result.data.shift; if (s == null) return null; @@ -386,8 +314,8 @@ class ShiftsRepositoryImpl int? filled; Break? breakInfo; try { - final rolesRes = await executeProtected(() => - _dataConnect.listShiftRolesByShiftId(shiftId: shiftId).execute()); + final rolesRes = await _service.executeProtected(() => + _service.connector.listShiftRolesByShiftId(shiftId: shiftId).execute()); if (rolesRes.data.shiftRoles.isNotEmpty) { required = 0; filled = 0; @@ -404,9 +332,9 @@ class ShiftsRepositoryImpl } } catch (_) {} - final startDt = _toDateTime(s.startTime); - final endDt = _toDateTime(s.endTime); - final createdDt = _toDateTime(s.createdAt); + final startDt = _service.toDateTime(s.startTime); + final endDt = _service.toDateTime(s.endTime); + final createdDt = _service.toDateTime(s.createdAt); return Shift( id: s.id, @@ -437,14 +365,14 @@ class ShiftsRepositoryImpl bool isInstantBook = false, String? roleId, }) async { - final staffId = await _getStaffId(); + final staffId = await _service.getStaffId(); String targetRoleId = roleId ?? ''; if (targetRoleId.isEmpty) { throw Exception('Missing role id.'); } - final roleResult = await executeProtected(() => _dataConnect + final roleResult = await _service.executeProtected(() => _service.connector .getShiftRoleById(shiftId: shiftId, roleId: targetRoleId) .execute()); final role = roleResult.data.shiftRole; @@ -452,12 +380,12 @@ class ShiftsRepositoryImpl throw Exception('Shift role not found'); } final shiftResult = - await executeProtected(() => _dataConnect.getShiftById(id: shiftId).execute()); + await _service.executeProtected(() => _service.connector.getShiftById(id: shiftId).execute()); final shift = shiftResult.data.shift; if (shift == null) { throw Exception('Shift not found'); } - final DateTime? shiftDate = _toDateTime(shift.date); + final DateTime? shiftDate = _service.toDateTime(shift.date); if (shiftDate != null) { final DateTime dayStartUtc = DateTime.utc( shiftDate.year, @@ -475,16 +403,16 @@ class ShiftsRepositoryImpl 999, ); - final dayApplications = await executeProtected(() => _dataConnect + final dayApplications = await _service.executeProtected(() => _service.connector .vaidateDayStaffApplication(staffId: staffId) - .dayStart(_toTimestamp(dayStartUtc)) - .dayEnd(_toTimestamp(dayEndUtc)) + .dayStart(_service.toTimestamp(dayStartUtc)) + .dayEnd(_service.toTimestamp(dayEndUtc)) .execute()); if (dayApplications.data.applications.isNotEmpty) { throw Exception('The user already has a shift that day.'); } } - final existingApplicationResult = await executeProtected(() => _dataConnect + final existingApplicationResult = await _service.executeProtected(() => _service.connector .getApplicationByStaffShiftAndRole( staffId: staffId, shiftId: shiftId, @@ -505,7 +433,7 @@ class ShiftsRepositoryImpl bool updatedRole = false; bool updatedShift = false; try { - final appResult = await executeProtected(() => _dataConnect + final appResult = await _service.executeProtected(() => _service.connector .createApplication( shiftId: shiftId, staffId: staffId, @@ -517,24 +445,24 @@ class ShiftsRepositoryImpl .execute()); appId = appResult.data.application_insert.id; - await executeProtected(() => _dataConnect + await _service.executeProtected(() => _service.connector .updateShiftRole(shiftId: shiftId, roleId: targetRoleId) .assigned(assigned + 1) .execute()); updatedRole = true; - await executeProtected( - () => _dataConnect.updateShift(id: shiftId).filled(filled + 1).execute()); + await _service.executeProtected( + () => _service.connector.updateShift(id: shiftId).filled(filled + 1).execute()); updatedShift = true; } catch (e) { if (updatedShift) { try { - await _dataConnect.updateShift(id: shiftId).filled(filled).execute(); + await _service.connector.updateShift(id: shiftId).filled(filled).execute(); } catch (_) {} } if (updatedRole) { try { - await _dataConnect + await _service.connector .updateShiftRole(shiftId: shiftId, roleId: targetRoleId) .assigned(assigned) .execute(); @@ -542,7 +470,7 @@ class ShiftsRepositoryImpl } if (appId != null) { try { - await _dataConnect.deleteApplication(id: appId).execute(); + await _service.connector.deleteApplication(id: appId).execute(); } catch (_) {} } rethrow; @@ -576,9 +504,9 @@ class ShiftsRepositoryImpl roleId = _appToRoleIdMap[appId]; } else { // Fallback fetch - final staffId = await _getStaffId(); - final apps = await executeProtected(() => - _dataConnect.getApplicationsByStaffId(staffId: staffId).execute()); + final staffId = await _service.getStaffId(); + final apps = await _service.executeProtected(() => + _service.connector.getApplicationsByStaffId(staffId: staffId).execute()); final app = apps.data.applications .where((a) => a.shiftId == shiftId) .firstOrNull; @@ -591,12 +519,12 @@ class ShiftsRepositoryImpl if (appId == null || roleId == null) { // If we are rejecting and can't find an application, create one as rejected (declining an available shift) if (newStatus == dc.ApplicationStatus.REJECTED) { - final rolesResult = await executeProtected(() => - _dataConnect.listShiftRolesByShiftId(shiftId: shiftId).execute()); + final rolesResult = await _service.executeProtected(() => + _service.connector.listShiftRolesByShiftId(shiftId: shiftId).execute()); if (rolesResult.data.shiftRoles.isNotEmpty) { final role = rolesResult.data.shiftRoles.first; - final staffId = await _getStaffId(); - await executeProtected(() => _dataConnect + final staffId = await _service.getStaffId(); + await _service.executeProtected(() => _service.connector .createApplication( shiftId: shiftId, staffId: staffId, @@ -611,7 +539,7 @@ class ShiftsRepositoryImpl throw Exception("Application not found for shift $shiftId"); } - await executeProtected(() => _dataConnect + await _service.executeProtected(() => _service.connector .updateApplicationStatus(id: appId!) .status(newStatus) .execute());