feat(data-connect): Implement DataConnectService for centralized data operations and refactor ShiftsRepositoryImpl to utilize the new service

This commit is contained in:
Achintha Isuru
2026-02-16 14:57:47 -05:00
parent 51d53f658b
commit c3abb819c9
4 changed files with 164 additions and 129 deletions

View File

@@ -13,6 +13,7 @@ export 'src/session/client_session_store.dart';
// Export the generated Data Connect SDK // Export the generated Data Connect SDK
export 'src/dataconnect_generated/generated.dart'; export 'src/dataconnect_generated/generated.dart';
export 'src/services/data_connect_service.dart';
export 'src/session/staff_session_store.dart'; export 'src/session/staff_session_store.dart';
export 'src/mixins/data_error_handler.dart'; export 'src/mixins/data_error_handler.dart';

View File

@@ -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<String> 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;
}
}

View File

@@ -16,3 +16,5 @@ dependencies:
flutter_modular: ^6.3.0 flutter_modular: ^6.3.0
firebase_data_connect: ^0.2.2+2 firebase_data_connect: ^0.2.2+2
firebase_core: ^4.4.0 firebase_core: ^4.4.0
firebase_auth: ^6.1.4
krow_core: ^0.0.1

View File

@@ -1,85 +1,20 @@
import 'package:krow_data_connect/krow_data_connect.dart' as dc; import 'package:krow_data_connect/krow_data_connect.dart' as dc;
import 'package:krow_domain/krow_domain.dart'; import 'package:krow_domain/krow_domain.dart';
import 'package:intl/intl.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:firebase_data_connect/firebase_data_connect.dart' as fdc;
import 'package:krow_core/core.dart';
import '../../domain/repositories/shifts_repository_interface.dart'; import '../../domain/repositories/shifts_repository_interface.dart';
class ShiftsRepositoryImpl class ShiftsRepositoryImpl
with dc.DataErrorHandler
implements ShiftsRepositoryInterface { implements ShiftsRepositoryInterface {
final dc.ExampleConnector _dataConnect; final dc.DataConnectService _service;
final firebase_auth.FirebaseAuth _auth = firebase_auth.FirebaseAuth.instance;
ShiftsRepositoryImpl() : _dataConnect = dc.ExampleConnector.instance; ShiftsRepositoryImpl() : _service = dc.DataConnectService.instance;
// Cache: ShiftID -> ApplicationID (For Accept/Decline) // Cache: ShiftID -> ApplicationID (For Accept/Decline)
final Map<String, String> _shiftToAppIdMap = {}; final Map<String, String> _shiftToAppIdMap = {};
// Cache: ApplicationID -> RoleID (For Accept/Decline w/ Update mutation) // Cache: ApplicationID -> RoleID (For Accept/Decline w/ Update mutation)
final Map<String, String> _appToRoleIdMap = {}; final Map<String, String> _appToRoleIdMap = {};
String? _cachedStaffId;
Future<String> _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(() => _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 @override
Future<List<Shift>> getMyShifts({ Future<List<Shift>> getMyShifts({
required DateTime start, required DateTime start,
@@ -100,8 +35,8 @@ class ShiftsRepositoryImpl
@override @override
Future<List<Shift>> getHistoryShifts() async { Future<List<Shift>> getHistoryShifts() async {
final staffId = await _getStaffId(); final staffId = await _service.getStaffId();
final fdc.QueryResult<dc.ListCompletedApplicationsByStaffIdData, dc.ListCompletedApplicationsByStaffIdVariables> response = await executeProtected(() => _dataConnect final fdc.QueryResult<dc.ListCompletedApplicationsByStaffIdData, dc.ListCompletedApplicationsByStaffIdVariables> response = await _service.executeProtected(() => _service.connector
.listCompletedApplicationsByStaffId(staffId: staffId) .listCompletedApplicationsByStaffId(staffId: staffId)
.execute()); .execute());
final List<Shift> shifts = []; final List<Shift> shifts = [];
@@ -116,10 +51,10 @@ class ShiftsRepositoryImpl
? app.shift.order.eventName! ? app.shift.order.eventName!
: app.shift.order.business.businessName; : app.shift.order.business.businessName;
final String title = '$roleName - $orderName'; final String title = '$roleName - $orderName';
final DateTime? shiftDate = _toDateTime(app.shift.date); final DateTime? shiftDate = _service.toDateTime(app.shift.date);
final DateTime? startDt = _toDateTime(app.shiftRole.startTime); final DateTime? startDt = _service.toDateTime(app.shiftRole.startTime);
final DateTime? endDt = _toDateTime(app.shiftRole.endTime); final DateTime? endDt = _service.toDateTime(app.shiftRole.endTime);
final DateTime? createdDt = _toDateTime(app.createdAt); final DateTime? createdDt = _service.toDateTime(app.createdAt);
shifts.add( shifts.add(
Shift( Shift(
@@ -157,12 +92,12 @@ class ShiftsRepositoryImpl
DateTime? start, DateTime? start,
DateTime? end, DateTime? end,
}) async { }) async {
final staffId = await _getStaffId(); final staffId = await _service.getStaffId();
var query = _dataConnect.getApplicationsByStaffId(staffId: staffId); var query = _service.connector.getApplicationsByStaffId(staffId: staffId);
if (start != null && end != null) { 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<dc.GetApplicationsByStaffIdData, dc.GetApplicationsByStaffIdVariables> response = await executeProtected(() => query.execute()); final fdc.QueryResult<dc.GetApplicationsByStaffIdData, dc.GetApplicationsByStaffIdVariables> response = await _service.executeProtected(() => query.execute());
final apps = response.data.applications; final apps = response.data.applications;
final List<Shift> shifts = []; final List<Shift> shifts = [];
@@ -177,10 +112,10 @@ class ShiftsRepositoryImpl
? app.shift.order.eventName! ? app.shift.order.eventName!
: app.shift.order.business.businessName; : app.shift.order.business.businessName;
final String title = '$roleName - $orderName'; final String title = '$roleName - $orderName';
final DateTime? shiftDate = _toDateTime(app.shift.date); final DateTime? shiftDate = _service.toDateTime(app.shift.date);
final DateTime? startDt = _toDateTime(app.shiftRole.startTime); final DateTime? startDt = _service.toDateTime(app.shiftRole.startTime);
final DateTime? endDt = _toDateTime(app.shiftRole.endTime); final DateTime? endDt = _service.toDateTime(app.shiftRole.endTime);
final DateTime? createdDt = _toDateTime(app.createdAt); final DateTime? createdDt = _service.toDateTime(app.createdAt);
// Override status to reflect the application state (e.g., CHECKED_OUT, CONFIRMED) // Override status to reflect the application state (e.g., CHECKED_OUT, CONFIRMED)
final bool hasCheckIn = app.checkInTime != null; final bool hasCheckIn = app.checkInTime != null;
@@ -226,13 +161,6 @@ class ShiftsRepositoryImpl
return shifts; 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) { String _mapStatus(dc.ApplicationStatus status) {
switch (status) { switch (status) {
case dc.ApplicationStatus.CONFIRMED: case dc.ApplicationStatus.CONFIRMED:
@@ -255,7 +183,7 @@ class ShiftsRepositoryImpl
return <Shift>[]; return <Shift>[];
} }
final fdc.QueryResult<dc.ListShiftRolesByVendorIdData, dc.ListShiftRolesByVendorIdVariables> result = await executeProtected(() => _dataConnect final fdc.QueryResult<dc.ListShiftRolesByVendorIdData, dc.ListShiftRolesByVendorIdVariables> result = await _service.executeProtected(() => _service.connector
.listShiftRolesByVendorId(vendorId: vendorId) .listShiftRolesByVendorId(vendorId: vendorId)
.execute()); .execute());
final allShiftRoles = result.data.shiftRoles; final allShiftRoles = result.data.shiftRoles;
@@ -263,10 +191,10 @@ class ShiftsRepositoryImpl
final List<Shift> mappedShifts = []; final List<Shift> mappedShifts = [];
for (final sr in allShiftRoles) { for (final sr in allShiftRoles) {
final DateTime? shiftDate = _toDateTime(sr.shift.date); final DateTime? shiftDate = _service.toDateTime(sr.shift.date);
final startDt = _toDateTime(sr.startTime); final startDt = _service.toDateTime(sr.startTime);
final endDt = _toDateTime(sr.endTime); final endDt = _service.toDateTime(sr.endTime);
final createdDt = _toDateTime(sr.createdAt); final createdDt = _service.toDateTime(sr.createdAt);
mappedShifts.add( mappedShifts.add(
Shift( Shift(
@@ -319,21 +247,21 @@ class ShiftsRepositoryImpl
Future<Shift?> _getShiftDetails(String shiftId, {String? roleId}) async { Future<Shift?> _getShiftDetails(String shiftId, {String? roleId}) async {
if (roleId != null && roleId.isNotEmpty) { if (roleId != null && roleId.isNotEmpty) {
final roleResult = await executeProtected(() => _dataConnect final roleResult = await _service.executeProtected(() => _service.connector
.getShiftRoleById(shiftId: shiftId, roleId: roleId) .getShiftRoleById(shiftId: shiftId, roleId: roleId)
.execute()); .execute());
final sr = roleResult.data.shiftRole; final sr = roleResult.data.shiftRole;
if (sr == null) return null; if (sr == null) return null;
final DateTime? startDt = _toDateTime(sr.startTime); final DateTime? startDt = _service.toDateTime(sr.startTime);
final DateTime? endDt = _toDateTime(sr.endTime); final DateTime? endDt = _service.toDateTime(sr.endTime);
final DateTime? createdDt = _toDateTime(sr.createdAt); final DateTime? createdDt = _service.toDateTime(sr.createdAt);
final String staffId = await _getStaffId(); final String staffId = await _service.getStaffId();
bool hasApplied = false; bool hasApplied = false;
String status = 'open'; String status = 'open';
final apps = await executeProtected(() => final apps = await _service.executeProtected(() =>
_dataConnect.getApplicationsByStaffId(staffId: staffId).execute()); _service.connector.getApplicationsByStaffId(staffId: staffId).execute());
final app = apps.data.applications final app = apps.data.applications
.where( .where(
(a) => a.shiftId == shiftId && a.shiftRole.roleId == roleId, (a) => a.shiftId == shiftId && a.shiftRole.roleId == roleId,
@@ -378,7 +306,7 @@ class ShiftsRepositoryImpl
} }
final fdc.QueryResult<dc.GetShiftByIdData, dc.GetShiftByIdVariables> result = final fdc.QueryResult<dc.GetShiftByIdData, dc.GetShiftByIdVariables> result =
await executeProtected(() => _dataConnect.getShiftById(id: shiftId).execute()); await _service.executeProtected(() => _service.connector.getShiftById(id: shiftId).execute());
final s = result.data.shift; final s = result.data.shift;
if (s == null) return null; if (s == null) return null;
@@ -386,8 +314,8 @@ class ShiftsRepositoryImpl
int? filled; int? filled;
Break? breakInfo; Break? breakInfo;
try { try {
final rolesRes = await executeProtected(() => final rolesRes = await _service.executeProtected(() =>
_dataConnect.listShiftRolesByShiftId(shiftId: shiftId).execute()); _service.connector.listShiftRolesByShiftId(shiftId: shiftId).execute());
if (rolesRes.data.shiftRoles.isNotEmpty) { if (rolesRes.data.shiftRoles.isNotEmpty) {
required = 0; required = 0;
filled = 0; filled = 0;
@@ -404,9 +332,9 @@ class ShiftsRepositoryImpl
} }
} catch (_) {} } catch (_) {}
final startDt = _toDateTime(s.startTime); final startDt = _service.toDateTime(s.startTime);
final endDt = _toDateTime(s.endTime); final endDt = _service.toDateTime(s.endTime);
final createdDt = _toDateTime(s.createdAt); final createdDt = _service.toDateTime(s.createdAt);
return Shift( return Shift(
id: s.id, id: s.id,
@@ -437,14 +365,14 @@ class ShiftsRepositoryImpl
bool isInstantBook = false, bool isInstantBook = false,
String? roleId, String? roleId,
}) async { }) async {
final staffId = await _getStaffId(); final staffId = await _service.getStaffId();
String targetRoleId = roleId ?? ''; String targetRoleId = roleId ?? '';
if (targetRoleId.isEmpty) { if (targetRoleId.isEmpty) {
throw Exception('Missing role id.'); throw Exception('Missing role id.');
} }
final roleResult = await executeProtected(() => _dataConnect final roleResult = await _service.executeProtected(() => _service.connector
.getShiftRoleById(shiftId: shiftId, roleId: targetRoleId) .getShiftRoleById(shiftId: shiftId, roleId: targetRoleId)
.execute()); .execute());
final role = roleResult.data.shiftRole; final role = roleResult.data.shiftRole;
@@ -452,12 +380,12 @@ class ShiftsRepositoryImpl
throw Exception('Shift role not found'); throw Exception('Shift role not found');
} }
final shiftResult = final shiftResult =
await executeProtected(() => _dataConnect.getShiftById(id: shiftId).execute()); await _service.executeProtected(() => _service.connector.getShiftById(id: shiftId).execute());
final shift = shiftResult.data.shift; final shift = shiftResult.data.shift;
if (shift == null) { if (shift == null) {
throw Exception('Shift not found'); throw Exception('Shift not found');
} }
final DateTime? shiftDate = _toDateTime(shift.date); final DateTime? shiftDate = _service.toDateTime(shift.date);
if (shiftDate != null) { if (shiftDate != null) {
final DateTime dayStartUtc = DateTime.utc( final DateTime dayStartUtc = DateTime.utc(
shiftDate.year, shiftDate.year,
@@ -475,16 +403,16 @@ class ShiftsRepositoryImpl
999, 999,
); );
final dayApplications = await executeProtected(() => _dataConnect final dayApplications = await _service.executeProtected(() => _service.connector
.vaidateDayStaffApplication(staffId: staffId) .vaidateDayStaffApplication(staffId: staffId)
.dayStart(_toTimestamp(dayStartUtc)) .dayStart(_service.toTimestamp(dayStartUtc))
.dayEnd(_toTimestamp(dayEndUtc)) .dayEnd(_service.toTimestamp(dayEndUtc))
.execute()); .execute());
if (dayApplications.data.applications.isNotEmpty) { if (dayApplications.data.applications.isNotEmpty) {
throw Exception('The user already has a shift that day.'); throw Exception('The user already has a shift that day.');
} }
} }
final existingApplicationResult = await executeProtected(() => _dataConnect final existingApplicationResult = await _service.executeProtected(() => _service.connector
.getApplicationByStaffShiftAndRole( .getApplicationByStaffShiftAndRole(
staffId: staffId, staffId: staffId,
shiftId: shiftId, shiftId: shiftId,
@@ -505,7 +433,7 @@ class ShiftsRepositoryImpl
bool updatedRole = false; bool updatedRole = false;
bool updatedShift = false; bool updatedShift = false;
try { try {
final appResult = await executeProtected(() => _dataConnect final appResult = await _service.executeProtected(() => _service.connector
.createApplication( .createApplication(
shiftId: shiftId, shiftId: shiftId,
staffId: staffId, staffId: staffId,
@@ -517,24 +445,24 @@ class ShiftsRepositoryImpl
.execute()); .execute());
appId = appResult.data.application_insert.id; appId = appResult.data.application_insert.id;
await executeProtected(() => _dataConnect await _service.executeProtected(() => _service.connector
.updateShiftRole(shiftId: shiftId, roleId: targetRoleId) .updateShiftRole(shiftId: shiftId, roleId: targetRoleId)
.assigned(assigned + 1) .assigned(assigned + 1)
.execute()); .execute());
updatedRole = true; updatedRole = true;
await executeProtected( await _service.executeProtected(
() => _dataConnect.updateShift(id: shiftId).filled(filled + 1).execute()); () => _service.connector.updateShift(id: shiftId).filled(filled + 1).execute());
updatedShift = true; updatedShift = true;
} catch (e) { } catch (e) {
if (updatedShift) { if (updatedShift) {
try { try {
await _dataConnect.updateShift(id: shiftId).filled(filled).execute(); await _service.connector.updateShift(id: shiftId).filled(filled).execute();
} catch (_) {} } catch (_) {}
} }
if (updatedRole) { if (updatedRole) {
try { try {
await _dataConnect await _service.connector
.updateShiftRole(shiftId: shiftId, roleId: targetRoleId) .updateShiftRole(shiftId: shiftId, roleId: targetRoleId)
.assigned(assigned) .assigned(assigned)
.execute(); .execute();
@@ -542,7 +470,7 @@ class ShiftsRepositoryImpl
} }
if (appId != null) { if (appId != null) {
try { try {
await _dataConnect.deleteApplication(id: appId).execute(); await _service.connector.deleteApplication(id: appId).execute();
} catch (_) {} } catch (_) {}
} }
rethrow; rethrow;
@@ -576,9 +504,9 @@ class ShiftsRepositoryImpl
roleId = _appToRoleIdMap[appId]; roleId = _appToRoleIdMap[appId];
} else { } else {
// Fallback fetch // Fallback fetch
final staffId = await _getStaffId(); final staffId = await _service.getStaffId();
final apps = await executeProtected(() => final apps = await _service.executeProtected(() =>
_dataConnect.getApplicationsByStaffId(staffId: staffId).execute()); _service.connector.getApplicationsByStaffId(staffId: staffId).execute());
final app = apps.data.applications final app = apps.data.applications
.where((a) => a.shiftId == shiftId) .where((a) => a.shiftId == shiftId)
.firstOrNull; .firstOrNull;
@@ -591,12 +519,12 @@ class ShiftsRepositoryImpl
if (appId == null || roleId == null) { if (appId == null || roleId == null) {
// If we are rejecting and can't find an application, create one as rejected (declining an available shift) // If we are rejecting and can't find an application, create one as rejected (declining an available shift)
if (newStatus == dc.ApplicationStatus.REJECTED) { if (newStatus == dc.ApplicationStatus.REJECTED) {
final rolesResult = await executeProtected(() => final rolesResult = await _service.executeProtected(() =>
_dataConnect.listShiftRolesByShiftId(shiftId: shiftId).execute()); _service.connector.listShiftRolesByShiftId(shiftId: shiftId).execute());
if (rolesResult.data.shiftRoles.isNotEmpty) { if (rolesResult.data.shiftRoles.isNotEmpty) {
final role = rolesResult.data.shiftRoles.first; final role = rolesResult.data.shiftRoles.first;
final staffId = await _getStaffId(); final staffId = await _service.getStaffId();
await executeProtected(() => _dataConnect await _service.executeProtected(() => _service.connector
.createApplication( .createApplication(
shiftId: shiftId, shiftId: shiftId,
staffId: staffId, staffId: staffId,
@@ -611,7 +539,7 @@ class ShiftsRepositoryImpl
throw Exception("Application not found for shift $shiftId"); throw Exception("Application not found for shift $shiftId");
} }
await executeProtected(() => _dataConnect await _service.executeProtected(() => _service.connector
.updateApplicationStatus(id: appId!) .updateApplicationStatus(id: appId!)
.status(newStatus) .status(newStatus)
.execute()); .execute());