Merge pull request #426 from Oloodi/408-feature-implement-paidunpaid-breaks---client-app-frontend-development

Implement a centralized DataConnectService to centralize the DataConnectCalls
This commit is contained in:
Achintha Isuru
2026-02-16 17:00:31 -05:00
committed by GitHub
34 changed files with 650 additions and 939 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

@@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:krow_domain/krow_domain.dart'; import 'package:krow_domain/krow_domain.dart';
/// Mixin to handle Data Layer errors and map them to Domain Failures. /// Mixin to handle Data Layer errors and map them to Domain Failures.
@@ -62,7 +63,7 @@ mixin DataErrorHandler {
if (e is AppException) rethrow; if (e is AppException) rethrow;
// Debugging: Log unexpected errors // Debugging: Log unexpected errors
print('DataErrorHandler: Unhandled exception caught: $e'); debugPrint('DataErrorHandler: Unhandled exception caught: $e');
throw UnknownException(technicalMessage: e.toString()); throw UnknownException(technicalMessage: e.toString());
} }

View File

@@ -0,0 +1,120 @@
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 'package:krow_domain/krow_domain.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.
firebase_auth.FirebaseAuth get auth => _auth;
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);
}
// --- 3. Unified Execution ---
// Repositories call this to benefit from centralized error handling/logging
Future<T> run<T>(
Future<T> Function() action, {
bool requiresAuthentication = true,
}) {
if (requiresAuthentication && auth.currentUser == null) {
throw const NotAuthenticatedException(
technicalMessage: 'User must be authenticated to perform this action',
);
}
return executeProtected(action);
}
/// Clears the internal cache (e.g., on logout).
void clearCache() {
_cachedStaffId = null;
}
}

View File

@@ -15,3 +15,6 @@ dependencies:
path: ../domain path: ../domain
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_auth: ^6.1.4
krow_core: ^0.0.1

View File

@@ -10,20 +10,14 @@ import '../../domain/ui_entities/auth_mode.dart';
import '../../domain/repositories/auth_repository_interface.dart'; import '../../domain/repositories/auth_repository_interface.dart';
/// Implementation of [AuthRepositoryInterface]. /// Implementation of [AuthRepositoryInterface].
class AuthRepositoryImpl class AuthRepositoryImpl implements AuthRepositoryInterface {
with DataErrorHandler AuthRepositoryImpl() : _service = DataConnectService.instance;
implements AuthRepositoryInterface {
AuthRepositoryImpl({
required this.firebaseAuth,
required this.dataConnect,
});
final FirebaseAuth firebaseAuth; final DataConnectService _service;
final ExampleConnector dataConnect;
Completer<String?>? _pendingVerification; Completer<String?>? _pendingVerification;
@override @override
Stream<domain.User?> get currentUser => firebaseAuth Stream<domain.User?> get currentUser => _service.auth
.authStateChanges() .authStateChanges()
.map((User? firebaseUser) { .map((User? firebaseUser) {
if (firebaseUser == null) { if (firebaseUser == null) {
@@ -44,7 +38,7 @@ class AuthRepositoryImpl
final Completer<String?> completer = Completer<String?>(); final Completer<String?> completer = Completer<String?>();
_pendingVerification = completer; _pendingVerification = completer;
await firebaseAuth.verifyPhoneNumber( await _service.auth.verifyPhoneNumber(
phoneNumber: phoneNumber, phoneNumber: phoneNumber,
verificationCompleted: (PhoneAuthCredential credential) { verificationCompleted: (PhoneAuthCredential credential) {
// Skip auto-verification for test numbers to allow manual code entry // Skip auto-verification for test numbers to allow manual code entry
@@ -101,7 +95,8 @@ class AuthRepositoryImpl
@override @override
Future<void> signOut() { Future<void> signOut() {
StaffSessionStore.instance.clear(); StaffSessionStore.instance.clear();
return firebaseAuth.signOut(); _service.clearCache();
return _service.auth.signOut();
} }
/// Verifies an OTP code and returns the authenticated user. /// Verifies an OTP code and returns the authenticated user.
@@ -115,10 +110,10 @@ class AuthRepositoryImpl
verificationId: verificationId, verificationId: verificationId,
smsCode: smsCode, smsCode: smsCode,
); );
final UserCredential userCredential = await executeProtected( final UserCredential userCredential = await _service.run(
() async { () async {
try { try {
return await firebaseAuth.signInWithCredential(credential); return await _service.auth.signInWithCredential(credential);
} on FirebaseAuthException catch (e) { } on FirebaseAuthException catch (e) {
if (e.code == 'invalid-verification-code') { if (e.code == 'invalid-verification-code') {
throw const domain.InvalidCredentialsException( throw const domain.InvalidCredentialsException(
@@ -128,45 +123,56 @@ class AuthRepositoryImpl
rethrow; rethrow;
} }
}, },
requiresAuthentication: false,
); );
final User? firebaseUser = userCredential.user; final User? firebaseUser = userCredential.user;
if (firebaseUser == null) { if (firebaseUser == null) {
throw const domain.SignInFailedException( throw const domain.SignInFailedException(
technicalMessage: 'Phone verification failed, no Firebase user received.', technicalMessage:
'Phone verification failed, no Firebase user received.',
); );
} }
final QueryResult<GetUserByIdData, GetUserByIdVariables> response = final QueryResult<GetUserByIdData, GetUserByIdVariables> response =
await executeProtected(() => dataConnect await _service.run(
.getUserById( () => _service.connector
id: firebaseUser.uid, .getUserById(
) id: firebaseUser.uid,
.execute()); )
.execute(),
requiresAuthentication: false,
);
final GetUserByIdUser? user = response.data.user; final GetUserByIdUser? user = response.data.user;
GetStaffByUserIdStaffs? staffRecord; GetStaffByUserIdStaffs? staffRecord;
if (mode == AuthMode.signup) { if (mode == AuthMode.signup) {
if (user == null) { if (user == null) {
await executeProtected(() => dataConnect await _service.run(
.createUser( () => _service.connector
id: firebaseUser.uid, .createUser(
role: UserBaseRole.USER, id: firebaseUser.uid,
) role: UserBaseRole.USER,
.userRole('STAFF') )
.execute()); .userRole('STAFF')
.execute(),
requiresAuthentication: false,
);
} else { } else {
// User exists in PostgreSQL. Check if they have a STAFF profile. // User exists in PostgreSQL. Check if they have a STAFF profile.
final QueryResult<GetStaffByUserIdData, GetStaffByUserIdVariables> final QueryResult<GetStaffByUserIdData, GetStaffByUserIdVariables>
staffResponse = await executeProtected(() => dataConnect staffResponse = await _service.run(
.getStaffByUserId( () => _service.connector
userId: firebaseUser.uid, .getStaffByUserId(
) userId: firebaseUser.uid,
.execute()); )
.execute(),
requiresAuthentication: false,
);
if (staffResponse.data.staffs.isNotEmpty) { if (staffResponse.data.staffs.isNotEmpty) {
// If profile exists, they should use Login mode. // If profile exists, they should use Login mode.
await firebaseAuth.signOut(); await _service.auth.signOut();
throw const domain.AccountExistsException( throw const domain.AccountExistsException(
technicalMessage: technicalMessage:
'This user already has a staff profile. Please log in.', 'This user already has a staff profile. Please log in.',
@@ -177,35 +183,44 @@ class AuthRepositoryImpl
// they are allowed to "Sign Up" for Staff. // they are allowed to "Sign Up" for Staff.
// We update their userRole to 'BOTH'. // We update their userRole to 'BOTH'.
if (user.userRole == 'BUSINESS') { if (user.userRole == 'BUSINESS') {
await executeProtected(() => await _service.run(
dataConnect.updateUser(id: firebaseUser.uid).userRole('BOTH').execute()); () => _service.connector
.updateUser(id: firebaseUser.uid)
.userRole('BOTH')
.execute(),
requiresAuthentication: false,
);
} }
} }
} else { } else {
if (user == null) { if (user == null) {
await firebaseAuth.signOut(); await _service.auth.signOut();
throw const domain.UserNotFoundException( throw const domain.UserNotFoundException(
technicalMessage: 'Authenticated user profile not found in database.', technicalMessage: 'Authenticated user profile not found in database.',
); );
} }
// Allow STAFF or BOTH roles to log in to the Staff App // Allow STAFF or BOTH roles to log in to the Staff App
if (user.userRole != 'STAFF' && user.userRole != 'BOTH') { if (user.userRole != 'STAFF' && user.userRole != 'BOTH') {
await firebaseAuth.signOut(); await _service.auth.signOut();
throw const domain.UnauthorizedAppException( throw const domain.UnauthorizedAppException(
technicalMessage: 'User is not authorized for this app.', technicalMessage: 'User is not authorized for this app.',
); );
} }
final QueryResult<GetStaffByUserIdData, GetStaffByUserIdVariables> final QueryResult<GetStaffByUserIdData, GetStaffByUserIdVariables>
staffResponse = await executeProtected(() => dataConnect staffResponse = await _service.run(
.getStaffByUserId( () => _service.connector
userId: firebaseUser.uid, .getStaffByUserId(
) userId: firebaseUser.uid,
.execute()); )
.execute(),
requiresAuthentication: false,
);
if (staffResponse.data.staffs.isEmpty) { if (staffResponse.data.staffs.isEmpty) {
await firebaseAuth.signOut(); await _service.auth.signOut();
throw const domain.UserNotFoundException( throw const domain.UserNotFoundException(
technicalMessage: 'Your account is not registered yet. Please register first.', technicalMessage:
'Your account is not registered yet. Please register first.',
); );
} }
staffRecord = staffResponse.data.staffs.first; staffRecord = staffResponse.data.staffs.first;

View File

@@ -1,18 +1,13 @@
import 'package:firebase_auth/firebase_auth.dart' as auth;
import 'package:krow_data_connect/krow_data_connect.dart'; import 'package:krow_data_connect/krow_data_connect.dart';
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_domain/krow_domain.dart'; import 'package:krow_domain/krow_domain.dart';
import 'package:firebase_auth/firebase_auth.dart' as auth;
import '../../domain/repositories/profile_setup_repository.dart'; import '../../domain/repositories/profile_setup_repository.dart';
class ProfileSetupRepositoryImpl implements ProfileSetupRepository { class ProfileSetupRepositoryImpl implements ProfileSetupRepository {
final auth.FirebaseAuth _firebaseAuth; final DataConnectService _service;
final ExampleConnector _dataConnect;
ProfileSetupRepositoryImpl({ ProfileSetupRepositoryImpl() : _service = DataConnectService.instance;
required auth.FirebaseAuth firebaseAuth,
required ExampleConnector dataConnect,
}) : _firebaseAuth = firebaseAuth,
_dataConnect = dataConnect;
@override @override
Future<void> submitProfile({ Future<void> submitProfile({
@@ -23,17 +18,19 @@ class ProfileSetupRepositoryImpl implements ProfileSetupRepository {
required List<String> industries, required List<String> industries,
required List<String> skills, required List<String> skills,
}) async { }) async {
final auth.User? firebaseUser = _firebaseAuth.currentUser; return _service.run(() async {
if (firebaseUser == null) { final auth.User? firebaseUser = _service.auth.currentUser;
throw Exception('User not authenticated.'); if (firebaseUser == null) {
} throw const NotAuthenticatedException(
technicalMessage: 'User not authenticated.');
}
final StaffSession? session = StaffSessionStore.instance.session; final StaffSession? session = StaffSessionStore.instance.session;
final String email = session?.user.email ?? ''; final String email = session?.user.email ?? '';
final String? phone = firebaseUser.phoneNumber; final String? phone = firebaseUser.phoneNumber;
final fdc.OperationResult<CreateStaffData, CreateStaffVariables> final fdc.OperationResult<CreateStaffData, CreateStaffVariables> result =
result = await _dataConnect await _service.connector
.createStaff( .createStaff(
userId: firebaseUser.uid, userId: firebaseUser.uid,
fullName: fullName, fullName: fullName,
@@ -63,5 +60,6 @@ class ProfileSetupRepositoryImpl implements ProfileSetupRepository {
StaffSession(user: session.user, staff: staff, ownerId: session.ownerId), StaffSession(user: session.user, staff: staff, ownerId: session.ownerId),
); );
} }
});
} }
} }

View File

@@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_modular/flutter_modular.dart'; import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_core/core.dart'; import 'package:krow_core/core.dart';
import 'package:krow_data_connect/krow_data_connect.dart'; import 'package:krow_data_connect/krow_data_connect.dart';
import 'package:firebase_auth/firebase_auth.dart' as firebase;
import 'package:staff_authentication/src/data/repositories_impl/auth_repository_impl.dart'; import 'package:staff_authentication/src/data/repositories_impl/auth_repository_impl.dart';
import 'package:staff_authentication/src/domain/repositories/auth_repository_interface.dart'; import 'package:staff_authentication/src/domain/repositories/auth_repository_interface.dart';
import 'package:staff_authentication/src/domain/usecases/sign_in_with_phone_usecase.dart'; import 'package:staff_authentication/src/domain/usecases/sign_in_with_phone_usecase.dart';
@@ -28,18 +27,8 @@ class StaffAuthenticationModule extends Module {
@override @override
void binds(Injector i) { void binds(Injector i) {
// Repositories // Repositories
i.addLazySingleton<AuthRepositoryInterface>( i.addLazySingleton<AuthRepositoryInterface>(AuthRepositoryImpl.new);
() => AuthRepositoryImpl( i.addLazySingleton<ProfileSetupRepository>(ProfileSetupRepositoryImpl.new);
firebaseAuth: firebase.FirebaseAuth.instance,
dataConnect: ExampleConnector.instance,
),
);
i.addLazySingleton<ProfileSetupRepository>(
() => ProfileSetupRepositoryImpl(
firebaseAuth: firebase.FirebaseAuth.instance,
dataConnect: ExampleConnector.instance,
),
);
i.addLazySingleton<PlaceRepository>(PlaceRepositoryImpl.new); i.addLazySingleton<PlaceRepository>(PlaceRepositoryImpl.new);
// UseCases // UseCases

View File

@@ -1,4 +1,3 @@
import 'package:firebase_auth/firebase_auth.dart' as firebase;
import 'package:firebase_data_connect/firebase_data_connect.dart'; import 'package:firebase_data_connect/firebase_data_connect.dart';
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';
@@ -10,44 +9,19 @@ import '../../domain/repositories/availability_repository.dart';
/// not specific date availability. Therefore, updating availability for a specific /// not specific date availability. Therefore, updating availability for a specific
/// date will update the availability for that Day of Week globally (Recurring). /// date will update the availability for that Day of Week globally (Recurring).
class AvailabilityRepositoryImpl class AvailabilityRepositoryImpl
with dc.DataErrorHandler
implements AvailabilityRepository { implements AvailabilityRepository {
final dc.ExampleConnector _dataConnect; final dc.DataConnectService _service;
final firebase.FirebaseAuth _firebaseAuth;
String? _cachedStaffId;
AvailabilityRepositoryImpl({ AvailabilityRepositoryImpl() : _service = dc.DataConnectService.instance;
required dc.ExampleConnector dataConnect,
required firebase.FirebaseAuth firebaseAuth,
}) : _dataConnect = dataConnect,
_firebaseAuth = firebaseAuth;
Future<String> _getStaffId() async {
if (_cachedStaffId != null) return _cachedStaffId!;
final firebase.User? user = _firebaseAuth.currentUser;
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 const ServerException(technicalMessage: 'Staff profile not found');
}
_cachedStaffId = result.data.staffs.first.id;
return _cachedStaffId!;
}
@override @override
Future<List<DayAvailability>> getAvailability(DateTime start, DateTime end) async { Future<List<DayAvailability>> getAvailability(DateTime start, DateTime end) async {
return executeProtected(() async { return _service.run(() async {
final String staffId = await _getStaffId(); final String staffId = await _service.getStaffId();
// 1. Fetch Weekly recurring availability // 1. Fetch Weekly recurring availability
final QueryResult<dc.ListStaffAvailabilitiesByStaffIdData, dc.ListStaffAvailabilitiesByStaffIdVariables> result = final QueryResult<dc.ListStaffAvailabilitiesByStaffIdData, dc.ListStaffAvailabilitiesByStaffIdVariables> result =
await _dataConnect.listStaffAvailabilitiesByStaffId(staffId: staffId).limit(100).execute(); await _service.connector.listStaffAvailabilitiesByStaffId(staffId: staffId).limit(100).execute();
final List<dc.ListStaffAvailabilitiesByStaffIdStaffAvailabilities> items = result.data.staffAvailabilities; final List<dc.ListStaffAvailabilitiesByStaffIdStaffAvailabilities> items = result.data.staffAvailabilities;
@@ -124,8 +98,8 @@ class AvailabilityRepositoryImpl
@override @override
Future<DayAvailability> updateDayAvailability(DayAvailability availability) async { Future<DayAvailability> updateDayAvailability(DayAvailability availability) async {
return executeProtected(() async { return _service.run(() async {
final String staffId = await _getStaffId(); final String staffId = await _service.getStaffId();
final dc.DayOfWeek dow = _toBackendDay(availability.date.weekday); final dc.DayOfWeek dow = _toBackendDay(availability.date.weekday);
// Update each slot in the backend. // Update each slot in the backend.
@@ -143,8 +117,8 @@ class AvailabilityRepositoryImpl
@override @override
Future<List<DayAvailability>> applyQuickSet(DateTime start, DateTime end, String type) async { Future<List<DayAvailability>> applyQuickSet(DateTime start, DateTime end, String type) async {
return executeProtected(() async { return _service.run(() async {
final String staffId = await _getStaffId(); final String staffId = await _service.getStaffId();
// QuickSet updates the Recurring schedule for all days involved. // 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. // However, if the user selects a range that covers e.g. Mon-Fri, we update Mon-Fri.
@@ -204,7 +178,7 @@ class AvailabilityRepositoryImpl
Future<void> _upsertSlot(String staffId, dc.DayOfWeek day, dc.AvailabilitySlot slot, dc.AvailabilityStatus status) async { Future<void> _upsertSlot(String staffId, dc.DayOfWeek day, dc.AvailabilitySlot slot, dc.AvailabilityStatus status) async {
// Check if exists // Check if exists
final result = await _dataConnect.getStaffAvailabilityByKey( final result = await _service.connector.getStaffAvailabilityByKey(
staffId: staffId, staffId: staffId,
day: day, day: day,
slot: slot, slot: slot,
@@ -212,14 +186,14 @@ class AvailabilityRepositoryImpl
if (result.data.staffAvailability != null) { if (result.data.staffAvailability != null) {
// Update // Update
await _dataConnect.updateStaffAvailability( await _service.connector.updateStaffAvailability(
staffId: staffId, staffId: staffId,
day: day, day: day,
slot: slot, slot: slot,
).status(status).execute(); ).status(status).execute();
} else { } else {
// Create // Create
await _dataConnect.createStaffAvailability( await _service.connector.createStaffAvailability(
staffId: staffId, staffId: staffId,
day: day, day: day,
slot: slot, slot: slot,

View File

@@ -18,12 +18,7 @@ class StaffAvailabilityModule extends Module {
@override @override
void binds(Injector i) { void binds(Injector i) {
// Repository // Repository
i.add<AvailabilityRepository>( i.add<AvailabilityRepository>(AvailabilityRepositoryImpl.new);
() => AvailabilityRepositoryImpl(
dataConnect: ExampleConnector.instance,
firebaseAuth: FirebaseAuth.instance,
),
);
// UseCases // UseCases
i.add(GetWeeklyAvailabilityUseCase.new); i.add(GetWeeklyAvailabilityUseCase.new);

View File

@@ -1,69 +1,17 @@
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_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:krow_core/core.dart';
import '../../domain/repositories/clock_in_repository_interface.dart'; import '../../domain/repositories/clock_in_repository_interface.dart';
/// Implementation of [ClockInRepositoryInterface] using Firebase Data Connect. /// Implementation of [ClockInRepositoryInterface] using Firebase Data Connect.
class ClockInRepositoryImpl class ClockInRepositoryImpl implements ClockInRepositoryInterface {
with dc.DataErrorHandler ClockInRepositoryImpl() : _service = dc.DataConnectService.instance;
implements ClockInRepositoryInterface {
ClockInRepositoryImpl({ final dc.DataConnectService _service;
required dc.ExampleConnector dataConnect,
}) : _dataConnect = dataConnect;
final dc.ExampleConnector _dataConnect;
final Map<String, String> _shiftToApplicationId = <String, String>{}; final Map<String, String> _shiftToApplicationId = <String, String>{};
String? _activeApplicationId; String? _activeApplicationId;
Future<String> _getStaffId() async {
final dc.StaffSession? session = dc.StaffSessionStore.instance.session;
final String? staffId = session?.staff?.id;
if (staffId != null && staffId.isNotEmpty) {
return staffId;
}
throw Exception('Staff session not found');
}
/// Helper to convert Data Connect fdc.Timestamp to DateTime
DateTime? _toDateTime(dynamic t) {
if (t == null) return null;
DateTime? dt;
if (t is DateTime) {
dt = t;
} else if (t is String) {
dt = DateTime.tryParse(t);
} else {
try {
if (t is fdc.Timestamp) {
dt = t.toDateTime();
}
} catch (_) {}
try {
if (dt == null && t.runtimeType.toString().contains('Timestamp')) {
dt = (t as dynamic).toDate();
}
} catch (_) {}
try {
dt ??= DateTime.tryParse(t.toString());
} catch (_) {}
}
if (dt != null) {
return DateTimeUtils.toDeviceTime(dt);
}
return null;
}
/// Helper to create fdc.Timestamp from DateTime
fdc.Timestamp _fromDateTime(DateTime d) {
// Assuming fdc.Timestamp.fromJson takes an ISO string
return fdc.Timestamp.fromJson(d.toUtc().toIso8601String());
}
({fdc.Timestamp start, fdc.Timestamp end}) _utcDayRange(DateTime localDay) { ({fdc.Timestamp start, fdc.Timestamp end}) _utcDayRange(DateTime localDay) {
final DateTime dayStartUtc = DateTime.utc( final DateTime dayStartUtc = DateTime.utc(
localDay.year, localDay.year,
@@ -81,8 +29,8 @@ class ClockInRepositoryImpl
999, 999,
); );
return ( return (
start: _fromDateTime(dayStartUtc), start: _service.toTimestamp(dayStartUtc),
end: _fromDateTime(dayEndUtc), end: _service.toTimestamp(dayEndUtc),
); );
} }
@@ -93,26 +41,29 @@ class ClockInRepositoryImpl
final DateTime now = DateTime.now(); final DateTime now = DateTime.now();
final ({fdc.Timestamp start, fdc.Timestamp end}) range = _utcDayRange(now); final ({fdc.Timestamp start, fdc.Timestamp end}) range = _utcDayRange(now);
final fdc.QueryResult<dc.GetApplicationsByStaffIdData, final fdc.QueryResult<dc.GetApplicationsByStaffIdData,
dc.GetApplicationsByStaffIdVariables> result = await executeProtected( dc.GetApplicationsByStaffIdVariables> result = await _service.run(
() => _dataConnect () => _service.connector
.getApplicationsByStaffId(staffId: staffId) .getApplicationsByStaffId(staffId: staffId)
.dayStart(range.start) .dayStart(range.start)
.dayEnd(range.end) .dayEnd(range.end)
.execute(), .execute(),
); );
final List<dc.GetApplicationsByStaffIdApplications> apps = result.data.applications; final List<dc.GetApplicationsByStaffIdApplications> apps =
result.data.applications;
if (apps.isEmpty) return const <dc.GetApplicationsByStaffIdApplications>[]; if (apps.isEmpty) return const <dc.GetApplicationsByStaffIdApplications>[];
_shiftToApplicationId _shiftToApplicationId
..clear() ..clear()
..addEntries(apps.map((dc.GetApplicationsByStaffIdApplications app) => MapEntry(app.shiftId, app.id))); ..addEntries(apps.map((dc.GetApplicationsByStaffIdApplications app) =>
MapEntry<String, String>(app.shiftId, app.id)));
apps.sort((dc.GetApplicationsByStaffIdApplications a, dc.GetApplicationsByStaffIdApplications b) { apps.sort((dc.GetApplicationsByStaffIdApplications a,
dc.GetApplicationsByStaffIdApplications b) {
final DateTime? aTime = final DateTime? aTime =
_toDateTime(a.shift.startTime) ?? _toDateTime(a.shift.date); _service.toDateTime(a.shift.startTime) ?? _service.toDateTime(a.shift.date);
final DateTime? bTime = final DateTime? bTime =
_toDateTime(b.shift.startTime) ?? _toDateTime(b.shift.date); _service.toDateTime(b.shift.startTime) ?? _service.toDateTime(b.shift.date);
if (aTime == null && bTime == null) return 0; if (aTime == null && bTime == null) return 0;
if (aTime == null) return -1; if (aTime == null) return -1;
if (bTime == null) return 1; if (bTime == null) return 1;
@@ -124,118 +75,124 @@ class ClockInRepositoryImpl
return apps; return apps;
} }
@override @override
Future<List<Shift>> getTodaysShifts() async { Future<List<Shift>> getTodaysShifts() async {
final String staffId = await _getStaffId(); return _service.run(() async {
final List<dc.GetApplicationsByStaffIdApplications> apps = final String staffId = await _service.getStaffId();
await _getTodaysApplications(staffId); final List<dc.GetApplicationsByStaffIdApplications> apps =
if (apps.isEmpty) return const <Shift>[]; await _getTodaysApplications(staffId);
if (apps.isEmpty) return const <Shift>[];
final List<Shift> shifts = <Shift>[]; final List<Shift> shifts = <Shift>[];
for (final dc.GetApplicationsByStaffIdApplications app in apps) { for (final dc.GetApplicationsByStaffIdApplications app in apps) {
final dc.GetApplicationsByStaffIdApplicationsShift shift = app.shift; final dc.GetApplicationsByStaffIdApplicationsShift shift = app.shift;
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);
final String roleName = app.shiftRole.role.name; final String roleName = app.shiftRole.role.name;
final String orderName = final String orderName =
(shift.order.eventName ?? '').trim().isNotEmpty (shift.order.eventName ?? '').trim().isNotEmpty
? shift.order.eventName! ? shift.order.eventName!
: shift.order.business.businessName; : shift.order.business.businessName;
final String title = '$roleName - $orderName'; final String title = '$roleName - $orderName';
shifts.add( shifts.add(
Shift( Shift(
id: shift.id, id: shift.id,
title: title, title: title,
clientName: shift.order.business.businessName, clientName: shift.order.business.businessName,
logoUrl: shift.order.business.companyLogoUrl ?? '', logoUrl: shift.order.business.companyLogoUrl ?? '',
hourlyRate: app.shiftRole.role.costPerHour, hourlyRate: app.shiftRole.role.costPerHour,
location: shift.location ?? '', location: shift.location ?? '',
locationAddress: shift.order.teamHub.hubName, locationAddress: shift.order.teamHub.hubName,
date: startDt?.toIso8601String() ?? '', date: startDt?.toIso8601String() ?? '',
startTime: startDt?.toIso8601String() ?? '', startTime: startDt?.toIso8601String() ?? '',
endTime: endDt?.toIso8601String() ?? '', endTime: endDt?.toIso8601String() ?? '',
createdDate: createdDt?.toIso8601String() ?? '', createdDate: createdDt?.toIso8601String() ?? '',
status: shift.status?.stringValue, status: shift.status?.stringValue,
description: shift.description, description: shift.description,
latitude: shift.latitude, latitude: shift.latitude,
longitude: shift.longitude, longitude: shift.longitude,
), ),
); );
} }
return shifts; return shifts;
});
} }
@override @override
Future<AttendanceStatus> getAttendanceStatus() async { Future<AttendanceStatus> getAttendanceStatus() async {
final String staffId = await _getStaffId(); return _service.run(() async {
final List<dc.GetApplicationsByStaffIdApplications> apps = final String staffId = await _service.getStaffId();
await _getTodaysApplications(staffId); final List<dc.GetApplicationsByStaffIdApplications> apps =
if (apps.isEmpty) { await _getTodaysApplications(staffId);
return const AttendanceStatus(isCheckedIn: false); if (apps.isEmpty) {
} return const AttendanceStatus(isCheckedIn: false);
}
dc.GetApplicationsByStaffIdApplications? activeApp; dc.GetApplicationsByStaffIdApplications? activeApp;
for (final dc.GetApplicationsByStaffIdApplications app in apps) { for (final dc.GetApplicationsByStaffIdApplications app in apps) {
if (app.checkInTime != null && app.checkOutTime == null) { if (app.checkInTime != null && app.checkOutTime == null) {
if (activeApp == null) { if (activeApp == null) {
activeApp = app;
} else {
final DateTime? current = _toDateTime(activeApp.checkInTime);
final DateTime? next = _toDateTime(app.checkInTime);
if (current == null || (next != null && next.isAfter(current))) {
activeApp = app; activeApp = app;
} else {
final DateTime? current = _service.toDateTime(activeApp.checkInTime);
final DateTime? next = _service.toDateTime(app.checkInTime);
if (current == null || (next != null && next.isAfter(current))) {
activeApp = app;
}
} }
} }
} }
}
if (activeApp == null) { if (activeApp == null) {
_activeApplicationId = null; _activeApplicationId = null;
return const AttendanceStatus(isCheckedIn: false); return const AttendanceStatus(isCheckedIn: false);
} }
_activeApplicationId = activeApp.id; _activeApplicationId = activeApp.id;
return AttendanceStatus( return AttendanceStatus(
isCheckedIn: true, isCheckedIn: true,
checkInTime: _toDateTime(activeApp.checkInTime), checkInTime: _service.toDateTime(activeApp.checkInTime),
checkOutTime: _toDateTime(activeApp.checkOutTime), checkOutTime: _service.toDateTime(activeApp.checkOutTime),
activeShiftId: activeApp.shiftId, activeShiftId: activeApp.shiftId,
activeApplicationId: activeApp.id, activeApplicationId: activeApp.id,
); );
});
} }
@override @override
Future<AttendanceStatus> clockIn({required String shiftId, String? notes}) async { Future<AttendanceStatus> clockIn({required String shiftId, String? notes}) async {
final String staffId = await _getStaffId(); return _service.run(() async {
final String staffId = await _service.getStaffId();
final String? cachedAppId = _shiftToApplicationId[shiftId]; final String? cachedAppId = _shiftToApplicationId[shiftId];
dc.GetApplicationsByStaffIdApplications? app; dc.GetApplicationsByStaffIdApplications? app;
if (cachedAppId != null) { if (cachedAppId != null) {
try { try {
final List<dc.GetApplicationsByStaffIdApplications> apps = await _getTodaysApplications(staffId); final List<dc.GetApplicationsByStaffIdApplications> apps =
app = apps.firstWhere((dc.GetApplicationsByStaffIdApplications a) => a.id == cachedAppId); await _getTodaysApplications(staffId);
} catch (_) {} app = apps.firstWhere(
} (dc.GetApplicationsByStaffIdApplications a) => a.id == cachedAppId);
app ??= (await _getTodaysApplications(staffId)) } catch (_) {}
.firstWhere((dc.GetApplicationsByStaffIdApplications a) => a.shiftId == shiftId); }
app ??= (await _getTodaysApplications(staffId)).firstWhere(
(dc.GetApplicationsByStaffIdApplications a) => a.shiftId == shiftId);
final fdc.Timestamp checkInTs = _fromDateTime(DateTime.now()); final fdc.Timestamp checkInTs = _service.toTimestamp(DateTime.now());
await executeProtected(() => _dataConnect await _service.run(() => _service.connector
.updateApplicationStatus( .updateApplicationStatus(
id: app!.id, id: app!.id,
) )
.checkInTime(checkInTs) .checkInTime(checkInTs)
.execute()); .execute());
_activeApplicationId = app.id; _activeApplicationId = app.id;
return getAttendanceStatus(); return getAttendanceStatus();
});
} }
@override @override
@@ -244,32 +201,35 @@ class ClockInRepositoryImpl
int? breakTimeMinutes, int? breakTimeMinutes,
String? applicationId, String? applicationId,
}) async { }) async {
await _getStaffId(); // Validate session return _service.run(() async {
await _service.getStaffId(); // Validate session
final String? targetAppId = applicationId ?? _activeApplicationId;
if (targetAppId == null || targetAppId.isEmpty) {
throw Exception('No active application id for checkout');
}
final fdc.QueryResult<dc.GetApplicationByIdData,
dc.GetApplicationByIdVariables> appResult =
await _service.run(() => _service.connector
.getApplicationById(id: targetAppId)
.execute());
final dc.GetApplicationByIdApplication? app = appResult.data.application;
final String? targetAppId = applicationId ?? _activeApplicationId; if (app == null) {
if (targetAppId == null || targetAppId.isEmpty) { throw Exception('Application not found for checkout');
throw Exception('No active application id for checkout'); }
} if (app.checkInTime == null || app.checkOutTime != null) {
final fdc.QueryResult<dc.GetApplicationByIdData, dc.GetApplicationByIdVariables> appResult = await executeProtected(() => _dataConnect throw Exception('No active shift found to clock out');
.getApplicationById(id: targetAppId) }
.execute());
final dc.GetApplicationByIdApplication? app = appResult.data.application;
if (app == null) { await _service.run(() => _service.connector
throw Exception('Application not found for checkout'); .updateApplicationStatus(
} id: targetAppId,
if (app.checkInTime == null || app.checkOutTime != null) { )
throw Exception('No active shift found to clock out'); .checkOutTime(_service.toTimestamp(DateTime.now()))
} .execute());
await executeProtected(() => _dataConnect return getAttendanceStatus();
.updateApplicationStatus( });
id: targetAppId,
)
.checkOutTime(_fromDateTime(DateTime.now()))
.execute());
return getAttendanceStatus();
} }
} }

View File

@@ -1,7 +1,6 @@
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter_modular/flutter_modular.dart'; import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_core/core.dart'; import 'package:krow_core/core.dart';
import 'package:krow_data_connect/krow_data_connect.dart';
import 'data/repositories_impl/clock_in_repository_impl.dart'; import 'data/repositories_impl/clock_in_repository_impl.dart';
import 'domain/repositories/clock_in_repository_interface.dart'; import 'domain/repositories/clock_in_repository_interface.dart';
@@ -16,9 +15,7 @@ class StaffClockInModule extends Module {
@override @override
void binds(Injector i) { void binds(Injector i) {
// Repositories // Repositories
i.add<ClockInRepositoryInterface>( i.add<ClockInRepositoryInterface>(ClockInRepositoryImpl.new);
() => ClockInRepositoryImpl(dataConnect: ExampleConnector.instance),
);
// Use Cases // Use Cases
i.add<GetTodaysShiftUseCase>(GetTodaysShiftUseCase.new); i.add<GetTodaysShiftUseCase>(GetTodaysShiftUseCase.new);

View File

@@ -1,26 +1,13 @@
import 'package:firebase_data_connect/firebase_data_connect.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:krow_data_connect/krow_data_connect.dart'; import 'package:krow_data_connect/krow_data_connect.dart';
import 'package:krow_domain/krow_domain.dart'; import 'package:krow_domain/krow_domain.dart';
import 'package:krow_core/core.dart';
import 'package:staff_home/src/domain/repositories/home_repository.dart'; import 'package:staff_home/src/domain/repositories/home_repository.dart';
extension TimestampExt on Timestamp {
DateTime toDate() {
return DateTimeUtils.toDeviceTime(toDateTime());
}
}
class HomeRepositoryImpl class HomeRepositoryImpl
with DataErrorHandler
implements HomeRepository { implements HomeRepository {
HomeRepositoryImpl(); HomeRepositoryImpl() : _service = DataConnectService.instance;
String get _currentStaffId { final DataConnectService _service;
final session = StaffSessionStore.instance.session;
if (session?.staff?.id == null) throw Exception('User not logged in');
return session!.staff!.id;
}
@override @override
Future<List<Shift>> getTodayShifts() async { Future<List<Shift>> getTodayShifts() async {
@@ -33,59 +20,57 @@ class HomeRepositoryImpl
} }
Future<List<Shift>> _getShiftsForDate(DateTime date) async { Future<List<Shift>> _getShiftsForDate(DateTime date) async {
final staffId = _currentStaffId; return _service.run(() async {
final staffId = await _service.getStaffId();
// Create start and end timestamps for the target date // Create start and end timestamps for the target date
final DateTime start = DateTime(date.year, date.month, date.day); final DateTime start = DateTime(date.year, date.month, date.day);
final DateTime end = final DateTime end =
DateTime(date.year, date.month, date.day, 23, 59, 59, 999); DateTime(date.year, date.month, date.day, 23, 59, 59, 999);
final response = await executeProtected(() => ExampleConnector.instance final response = await _service.run(() => _service.connector
.getApplicationsByStaffId(staffId: staffId) .getApplicationsByStaffId(staffId: staffId)
.dayStart(_toTimestamp(start)) .dayStart(_service.toTimestamp(start))
.dayEnd(_toTimestamp(end)) .dayEnd(_service.toTimestamp(end))
.execute()); .execute());
// Filter for CONFIRMED applications (same logic as shifts_repository_impl) // Filter for CONFIRMED applications (same logic as shifts_repository_impl)
final apps = response.data.applications.where((app) => final apps = response.data.applications.where((app) =>
(app.status is Known && (app.status is Known &&
(app.status as Known).value == ApplicationStatus.CONFIRMED)); (app.status as Known).value == ApplicationStatus.CONFIRMED));
final List<Shift> shifts = []; final List<Shift> shifts = [];
for (final app in apps) { for (final app in apps) {
shifts.add(_mapApplicationToShift(app)); shifts.add(_mapApplicationToShift(app));
} }
return shifts; return shifts;
} });
Timestamp _toTimestamp(DateTime dateTime) {
final DateTime utc = dateTime.toUtc();
final int seconds = utc.millisecondsSinceEpoch ~/ 1000;
final int nanoseconds = (utc.microsecondsSinceEpoch % 1000000) * 1000;
return Timestamp(nanoseconds, seconds);
} }
@override @override
Future<List<Shift>> getRecommendedShifts() async { Future<List<Shift>> getRecommendedShifts() async {
// Logic: List ALL open shifts (simple recommendation engine) // Logic: List ALL open shifts (simple recommendation engine)
// Limitation: listShifts might return ALL shifts. We should ideally filter by status=PUBLISHED. // Limitation: listShifts might return ALL shifts. We should ideally filter by status=PUBLISHED.
final response = await executeProtected(() => ExampleConnector.instance.listShifts().execute()); return _service.run(() async {
final response =
await _service.run(() => _service.connector.listShifts().execute());
return response.data.shifts return response.data.shifts
.where((s) { .where((s) {
final isOpen = final isOpen = s.status is Known &&
s.status is Known && (s.status as Known).value == ShiftStatus.OPEN; (s.status as Known).value == ShiftStatus.OPEN;
if (!isOpen) return false; if (!isOpen) return false;
final start = s.startTime?.toDate(); final start = _service.toDateTime(s.startTime);
if (start == null) return false; if (start == null) return false;
return start.isAfter(DateTime.now()); return start.isAfter(DateTime.now());
}) })
.take(10) .take(10)
.map((s) => _mapConnectorShiftToDomain(s)) .map((s) => _mapConnectorShiftToDomain(s))
.toList(); .toList();
});
} }
@override @override
@@ -110,10 +95,10 @@ class HomeRepositoryImpl
costPerHour: r.role.costPerHour, costPerHour: r.role.costPerHour,
shiftLocation: s.location, shiftLocation: s.location,
teamHubName: s.order.teamHub.hubName, teamHubName: s.order.teamHub.hubName,
shiftDate: s.date?.toDate(), shiftDate: _service.toDateTime(s.date),
startTime: r.startTime?.toDate(), startTime: _service.toDateTime(r.startTime),
endTime: r.endTime?.toDate(), endTime: _service.toDateTime(r.endTime),
createdAt: app.createdAt?.toDate(), createdAt: _service.toDateTime(app.createdAt),
status: 'confirmed', status: 'confirmed',
description: s.description, description: s.description,
durationDays: s.durationDays, durationDays: s.durationDays,
@@ -132,10 +117,12 @@ class HomeRepositoryImpl
hourlyRate: s.cost ?? 0.0, hourlyRate: s.cost ?? 0.0,
location: s.location ?? 'Unknown', location: s.location ?? 'Unknown',
locationAddress: s.locationAddress ?? '', locationAddress: s.locationAddress ?? '',
date: s.date?.toDate().toIso8601String() ?? '', date: _service.toDateTime(s.date)?.toIso8601String() ?? '',
startTime: DateFormat('HH:mm').format(s.startTime?.toDate() ?? DateTime.now()), startTime: DateFormat('HH:mm')
endTime: DateFormat('HH:mm').format(s.endTime?.toDate() ?? DateTime.now()), .format(_service.toDateTime(s.startTime) ?? DateTime.now()),
createdDate: s.createdAt?.toDate().toIso8601String() ?? '', endTime: DateFormat('HH:mm')
.format(_service.toDateTime(s.endTime) ?? DateTime.now()),
createdDate: _service.toDateTime(s.createdAt)?.toIso8601String() ?? '',
tipsAvailable: false, tipsAvailable: false,
mealProvided: false, mealProvided: false,
managers: [], managers: [],

View File

@@ -1,97 +1,26 @@
import 'package:firebase_data_connect/firebase_data_connect.dart';
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_data_connect/krow_data_connect.dart';
import 'package:krow_domain/krow_domain.dart'; import 'package:krow_domain/krow_domain.dart';
import 'package:firebase_auth/firebase_auth.dart' as firebase_auth;
import 'package:krow_core/core.dart';
import '../../domain/repositories/payments_repository.dart'; import '../../domain/repositories/payments_repository.dart';
class PaymentsRepositoryImpl class PaymentsRepositoryImpl
with dc.DataErrorHandler
implements PaymentsRepository { implements PaymentsRepository {
PaymentsRepositoryImpl() : _dataConnect = dc.ExampleConnector.instance; PaymentsRepositoryImpl() : _service = DataConnectService.instance;
final dc.ExampleConnector _dataConnect; final DataConnectService _service;
final firebase_auth.FirebaseAuth _auth = firebase_auth.FirebaseAuth.instance;
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 const NotAuthenticatedException(
technicalMessage: 'User is not authenticated',
);
}
// This call is protected by parent execution context if called within executeProtected,
// otherwise we might need to wrap it if called standalone.
// For now we assume it's called from public methods which are protected.
final QueryResult<dc.GetStaffByUserIdData, dc.GetStaffByUserIdVariables> response = await _dataConnect.getStaffByUserId(userId: user.uid).execute();
if (response.data.staffs.isNotEmpty) {
_cachedStaffId = response.data.staffs.first.id;
return _cachedStaffId!;
}
// 4. Fallback
return user.uid;
}
/// Helper to convert Data Connect Timestamp to DateTime
DateTime? _toDateTime(dynamic t) {
if (t == null) return null;
DateTime? dt;
if (t is DateTime) {
dt = t;
} else if (t is String) {
dt = DateTime.tryParse(t);
} else {
try {
if (t is Timestamp) {
dt = t.toDateTime();
}
} catch (_) {}
try {
if (dt == null && t.runtimeType.toString().contains('Timestamp')) {
dt = (t as dynamic).toDate();
}
} catch (_) {}
try {
dt ??= DateTime.tryParse(t.toString());
} catch (_) {}
}
if (dt != null) {
return DateTimeUtils.toDeviceTime(dt);
}
return null;
}
@override @override
Future<PaymentSummary> getPaymentSummary() async { Future<PaymentSummary> getPaymentSummary() async {
return executeProtected(() async { return _service.run(() async {
final String currentStaffId = await _getStaffId(); final String currentStaffId = await _service.getStaffId();
// Fetch recent payments with a limit // Fetch recent payments with a limit
// Note: limit is chained on the query builder final response = await _service.connector.listRecentPaymentsByStaffId(
final QueryResult<dc.ListRecentPaymentsByStaffIdData, dc.ListRecentPaymentsByStaffIdVariables> result =
await _dataConnect.listRecentPaymentsByStaffId(
staffId: currentStaffId, staffId: currentStaffId,
).limit(100).execute(); ).limit(100).execute();
final List<dc.ListRecentPaymentsByStaffIdRecentPayments> payments = result.data.recentPayments; final List<dc.ListRecentPaymentsByStaffIdRecentPayments> payments = response.data.recentPayments;
double weekly = 0; double weekly = 0;
double monthly = 0; double monthly = 0;
@@ -103,7 +32,7 @@ class PaymentsRepositoryImpl
final DateTime startOfMonth = DateTime(now.year, now.month, 1); final DateTime startOfMonth = DateTime(now.year, now.month, 1);
for (final dc.ListRecentPaymentsByStaffIdRecentPayments p in payments) { for (final dc.ListRecentPaymentsByStaffIdRecentPayments p in payments) {
final DateTime? date = _toDateTime(p.invoice.issueDate) ?? _toDateTime(p.createdAt); final DateTime? date = _service.toDateTime(p.invoice.issueDate) ?? _service.toDateTime(p.createdAt);
final double amount = p.invoice.amount; final double amount = p.invoice.amount;
final String? status = p.status?.stringValue; final String? status = p.status?.stringValue;
@@ -129,11 +58,10 @@ class PaymentsRepositoryImpl
@override @override
Future<List<StaffPayment>> getPaymentHistory(String period) async { Future<List<StaffPayment>> getPaymentHistory(String period) async {
return executeProtected(() async { return _service.run(() async {
final String currentStaffId = await _getStaffId(); final String currentStaffId = await _service.getStaffId();
final QueryResult<dc.ListRecentPaymentsByStaffIdData, dc.ListRecentPaymentsByStaffIdVariables> response = final response = await _service.connector
await _dataConnect
.listRecentPaymentsByStaffId(staffId: currentStaffId) .listRecentPaymentsByStaffId(staffId: currentStaffId)
.execute(); .execute();
@@ -144,7 +72,7 @@ class PaymentsRepositoryImpl
assignmentId: payment.applicationId, assignmentId: payment.applicationId,
amount: payment.invoice.amount, amount: payment.invoice.amount,
status: PaymentAdapter.toPaymentStatus(payment.status?.stringValue ?? 'UNKNOWN'), status: PaymentAdapter.toPaymentStatus(payment.status?.stringValue ?? 'UNKNOWN'),
paidAt: _toDateTime(payment.invoice.issueDate), paidAt: _service.toDateTime(payment.invoice.issueDate),
); );
}).toList(); }).toList();
}); });

View File

@@ -1,4 +1,3 @@
import 'package:firebase_auth/firebase_auth.dart';
import 'package:krow_data_connect/krow_data_connect.dart'; import 'package:krow_data_connect/krow_data_connect.dart';
import 'package:krow_domain/krow_domain.dart'; import 'package:krow_domain/krow_domain.dart';
@@ -15,38 +14,23 @@ import '../../domain/repositories/profile_repository.dart';
/// Currently uses [ProfileRepositoryMock] from data_connect. /// Currently uses [ProfileRepositoryMock] from data_connect.
/// When Firebase Data Connect is ready, this will be swapped with a real implementation. /// When Firebase Data Connect is ready, this will be swapped with a real implementation.
class ProfileRepositoryImpl class ProfileRepositoryImpl
with DataErrorHandler
implements ProfileRepositoryInterface { implements ProfileRepositoryInterface {
/// Creates a [ProfileRepositoryImpl]. /// Creates a [ProfileRepositoryImpl].
/// ProfileRepositoryImpl() : _service = DataConnectService.instance;
/// Requires a [ExampleConnector] from the data_connect package and [FirebaseAuth].
const ProfileRepositoryImpl({
required this.connector,
required this.firebaseAuth,
});
/// The Data Connect connector used for data operations. final DataConnectService _service;
final ExampleConnector connector;
/// The Firebase Auth instance.
final FirebaseAuth firebaseAuth;
@override @override
Future<Staff> getStaffProfile() async { Future<Staff> getStaffProfile() async {
return executeProtected(() async { return _service.run(() async {
final user = firebaseAuth.currentUser; final staffId = await _service.getStaffId();
if (user == null) { final response = await _service.connector.getStaffById(id: staffId).execute();
throw NotAuthenticatedException(
technicalMessage: 'User not authenticated');
}
final response = await connector.getStaffByUserId(userId: user.uid).execute(); if (response.data.staff == null) {
if (response.data.staffs.isEmpty) {
throw const ServerException(technicalMessage: 'Staff not found'); throw const ServerException(technicalMessage: 'Staff not found');
} }
final GetStaffByUserIdStaffs rawStaff = response.data.staffs.first; final GetStaffByIdStaff rawStaff = response.data.staff!;
// Map the raw data connect object to the Domain Entity // Map the raw data connect object to the Domain Entity
return Staff( return Staff(
@@ -71,7 +55,8 @@ class ProfileRepositoryImpl
@override @override
Future<void> signOut() async { Future<void> signOut() async {
try { try {
await firebaseAuth.signOut(); await _service.auth.signOut();
_service.clearCache();
} catch (e) { } catch (e) {
throw Exception('Error signing out: ${e.toString()}'); throw Exception('Error signing out: ${e.toString()}');
} }

View File

@@ -1,8 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_modular/flutter_modular.dart'; import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_core/core.dart'; import 'package:krow_core/core.dart';
import 'package:krow_data_connect/krow_data_connect.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'data/repositories/profile_repository_impl.dart'; import 'data/repositories/profile_repository_impl.dart';
import 'domain/repositories/profile_repository.dart'; import 'domain/repositories/profile_repository.dart';
@@ -25,10 +23,7 @@ class StaffProfileModule extends Module {
void binds(Injector i) { void binds(Injector i) {
// Repository implementation - delegates to data_connect // Repository implementation - delegates to data_connect
i.addLazySingleton<ProfileRepositoryInterface>( i.addLazySingleton<ProfileRepositoryInterface>(
() => ProfileRepositoryImpl( ProfileRepositoryImpl.new,
connector: ExampleConnector.instance,
firebaseAuth: FirebaseAuth.instance,
),
); );
// Use cases - depend on repository interface // Use cases - depend on repository interface

View File

@@ -1,47 +1,30 @@
import 'package:firebase_auth/firebase_auth.dart'; import 'package:krow_core/core.dart';
import 'package:firebase_data_connect/firebase_data_connect.dart';
import 'package:krow_data_connect/krow_data_connect.dart'; import 'package:krow_data_connect/krow_data_connect.dart';
import 'package:krow_domain/krow_domain.dart' as domain; import 'package:krow_domain/krow_domain.dart' as domain;
import 'package:krow_core/core.dart';
import '../../domain/repositories/certificates_repository.dart'; import '../../domain/repositories/certificates_repository.dart';
/// Implementation of [CertificatesRepository] using Data Connect. /// Implementation of [CertificatesRepository] using Data Connect.
/// ///
/// This class handles the communication with the backend via [ExampleConnector]. /// This class handles the communication with the backend via [DataConnectService].
/// It maps raw generated data types to clean [domain.StaffDocument] entities. /// It maps raw generated data types to clean [domain.StaffDocument] entities.
class CertificatesRepositoryImpl class CertificatesRepositoryImpl
with DataErrorHandler
implements CertificatesRepository { implements CertificatesRepository {
/// The generated Data Connect SDK client. /// The Data Connect service instance.
final ExampleConnector _dataConnect; final DataConnectService _service;
/// The Firebase Authentication instance.
final FirebaseAuth _firebaseAuth;
/// Creates a [CertificatesRepositoryImpl]. /// Creates a [CertificatesRepositoryImpl].
/// CertificatesRepositoryImpl() : _service = DataConnectService.instance;
/// Requires [ExampleConnector] for data access and [FirebaseAuth] for user context.
CertificatesRepositoryImpl({
required ExampleConnector dataConnect,
required FirebaseAuth firebaseAuth,
}) : _dataConnect = dataConnect,
_firebaseAuth = firebaseAuth;
@override @override
Future<List<domain.StaffDocument>> getCertificates() async { Future<List<domain.StaffDocument>> getCertificates() async {
return executeProtected(() async { return _service.run(() async {
final User? currentUser = _firebaseAuth.currentUser; final String staffId = await _service.getStaffId();
if (currentUser == null) {
throw domain.NotAuthenticatedException(
technicalMessage: 'User not authenticated');
}
// Execute the query via DataConnect generated SDK // Execute the query via DataConnect generated SDK
final QueryResult<ListStaffDocumentsByStaffIdData, final result =
ListStaffDocumentsByStaffIdVariables> result = await _service.connector
await _dataConnect .listStaffDocumentsByStaffId(staffId: staffId)
.listStaffDocumentsByStaffId(staffId: currentUser.uid)
.execute(); .execute();
// Map the generated SDK types to pure Domain entities // Map the generated SDK types to pure Domain entities

View File

@@ -1,7 +1,5 @@
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_modular/flutter_modular.dart'; import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_core/core.dart'; import 'package:krow_core/core.dart';
import 'package:krow_data_connect/krow_data_connect.dart';
import 'data/repositories_impl/certificates_repository_impl.dart'; import 'data/repositories_impl/certificates_repository_impl.dart';
import 'domain/repositories/certificates_repository.dart'; import 'domain/repositories/certificates_repository.dart';
@@ -12,12 +10,7 @@ import 'presentation/pages/certificates_page.dart';
class StaffCertificatesModule extends Module { class StaffCertificatesModule extends Module {
@override @override
void binds(Injector i) { void binds(Injector i) {
i.addLazySingleton<CertificatesRepository>( i.addLazySingleton<CertificatesRepository>(CertificatesRepositoryImpl.new);
() => CertificatesRepositoryImpl(
dataConnect: i.get<ExampleConnector>(), // Assuming ExampleConnector is provided by parent module
firebaseAuth: FirebaseAuth.instance,
),
);
i.addLazySingleton(GetCertificatesUseCase.new); i.addLazySingleton(GetCertificatesUseCase.new);
i.addLazySingleton(CertificatesCubit.new); i.addLazySingleton(CertificatesCubit.new);
} }

View File

@@ -1,39 +1,27 @@
import 'package:firebase_auth/firebase_auth.dart'; import 'package:krow_core/core.dart';
import 'package:firebase_data_connect/firebase_data_connect.dart';
import 'package:krow_data_connect/krow_data_connect.dart'; import 'package:krow_data_connect/krow_data_connect.dart';
import 'package:krow_domain/krow_domain.dart' as domain; import 'package:krow_domain/krow_domain.dart' as domain;
import 'package:krow_core/core.dart';
import '../../domain/repositories/documents_repository.dart'; import '../../domain/repositories/documents_repository.dart';
/// Implementation of [DocumentsRepository] using Data Connect. /// Implementation of [DocumentsRepository] using Data Connect.
class DocumentsRepositoryImpl class DocumentsRepositoryImpl
with DataErrorHandler
implements DocumentsRepository { implements DocumentsRepository {
final ExampleConnector _dataConnect; final DataConnectService _service;
final FirebaseAuth _firebaseAuth;
DocumentsRepositoryImpl({ DocumentsRepositoryImpl() : _service = DataConnectService.instance;
required ExampleConnector dataConnect,
required FirebaseAuth firebaseAuth,
}) : _dataConnect = dataConnect,
_firebaseAuth = firebaseAuth;
@override @override
Future<List<domain.StaffDocument>> getDocuments() async { Future<List<domain.StaffDocument>> getDocuments() async {
return executeProtected(() async { return _service.run(() async {
final User? currentUser = _firebaseAuth.currentUser; final String? staffId = await _service.getStaffId();
if (currentUser == null) {
throw domain.NotAuthenticatedException(
technicalMessage: 'User not authenticated');
}
/// MOCK IMPLEMENTATION /// MOCK IMPLEMENTATION
/// To be replaced with real data connect query when available /// To be replaced with real data connect query when available
return [ return [
domain.StaffDocument( domain.StaffDocument(
id: 'doc1', id: 'doc1',
staffId: currentUser.uid, staffId: staffId!,
documentId: 'd1', documentId: 'd1',
name: 'Work Permit', name: 'Work Permit',
description: 'Valid work permit document', description: 'Valid work permit document',
@@ -43,7 +31,7 @@ class DocumentsRepositoryImpl
), ),
domain.StaffDocument( domain.StaffDocument(
id: 'doc2', id: 'doc2',
staffId: currentUser.uid, staffId: staffId!,
documentId: 'd2', documentId: 'd2',
name: 'Health and Safety Training', name: 'Health and Safety Training',
description: 'Certificate of completion for health and safety training', description: 'Certificate of completion for health and safety training',

View File

@@ -1,7 +1,5 @@
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_modular/flutter_modular.dart'; import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_core/core.dart'; import 'package:krow_core/core.dart';
import 'package:krow_data_connect/krow_data_connect.dart';
import 'data/repositories_impl/documents_repository_impl.dart'; import 'data/repositories_impl/documents_repository_impl.dart';
import 'domain/repositories/documents_repository.dart'; import 'domain/repositories/documents_repository.dart';
import 'domain/usecases/get_documents_usecase.dart'; import 'domain/usecases/get_documents_usecase.dart';
@@ -11,12 +9,7 @@ import 'presentation/pages/documents_page.dart';
class StaffDocumentsModule extends Module { class StaffDocumentsModule extends Module {
@override @override
void binds(Injector i) { void binds(Injector i) {
i.addLazySingleton<DocumentsRepository>( i.addLazySingleton<DocumentsRepository>(DocumentsRepositoryImpl.new);
() => DocumentsRepositoryImpl(
dataConnect: ExampleConnector.instance,
firebaseAuth: FirebaseAuth.instance,
),
);
i.addLazySingleton(GetDocumentsUseCase.new); i.addLazySingleton(GetDocumentsUseCase.new);
i.addLazySingleton(DocumentsCubit.new); i.addLazySingleton(DocumentsCubit.new);
} }

View File

@@ -1,7 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'package:firebase_auth/firebase_auth.dart' as auth;
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_data_connect/firebase_data_connect.dart'; import 'package:firebase_data_connect/firebase_data_connect.dart';
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';
@@ -10,45 +8,21 @@ import '../../domain/repositories/tax_forms_repository.dart';
import '../mappers/tax_form_mapper.dart'; import '../mappers/tax_form_mapper.dart';
class TaxFormsRepositoryImpl class TaxFormsRepositoryImpl
with dc.DataErrorHandler
implements TaxFormsRepository { implements TaxFormsRepository {
TaxFormsRepositoryImpl({ TaxFormsRepositoryImpl() : _service = dc.DataConnectService.instance;
required this.firebaseAuth,
required this.dataConnect,
});
final auth.FirebaseAuth firebaseAuth; final dc.DataConnectService _service;
final dc.ExampleConnector dataConnect;
/// Helper to get the logged-in staff ID.
String _getStaffId() {
final auth.User? user = firebaseAuth.currentUser;
if (user == null) {
throw const NotAuthenticatedException(
technicalMessage: 'Firebase User is null',
);
}
final String? staffId = dc.StaffSessionStore.instance.session?.staff?.id;
if (staffId == null || staffId.isEmpty) {
throw const StaffProfileNotFoundException(
technicalMessage: 'Staff ID missing in SessionStore',
);
}
return staffId;
}
@override @override
Future<List<TaxForm>> getTaxForms() async { Future<List<TaxForm>> getTaxForms() async {
return executeProtected(() async { return _service.run(() async {
final String staffId = _getStaffId(); final String staffId = await _service.getStaffId();
final QueryResult<dc.GetTaxFormsByStaffIdData, dc.GetTaxFormsByStaffIdVariables> final response = await _service.connector
result = await dataConnect
.getTaxFormsByStaffId(staffId: staffId) .getTaxFormsByStaffId(staffId: staffId)
.execute(); .execute();
final List<TaxForm> forms = final List<TaxForm> forms =
result.data.taxForms.map(TaxFormMapper.fromDataConnect).toList(); response.data.taxForms.map(TaxFormMapper.fromDataConnect).toList();
// Check if required forms exist, create if not. // Check if required forms exist, create if not.
final Set<TaxFormType> typesPresent = final Set<TaxFormType> typesPresent =
@@ -65,11 +39,9 @@ class TaxFormsRepositoryImpl
} }
if (createdNew) { if (createdNew) {
final QueryResult< final response2 =
dc.GetTaxFormsByStaffIdData, await _service.connector.getTaxFormsByStaffId(staffId: staffId).execute();
dc.GetTaxFormsByStaffIdVariables> result2 = return response2.data.taxForms
await dataConnect.getTaxFormsByStaffId(staffId: staffId).execute();
return result2.data.taxForms
.map(TaxFormMapper.fromDataConnect) .map(TaxFormMapper.fromDataConnect)
.toList(); .toList();
} }
@@ -79,7 +51,7 @@ class TaxFormsRepositoryImpl
} }
Future<void> _createInitialForm(String staffId, TaxFormType type) async { Future<void> _createInitialForm(String staffId, TaxFormType type) async {
await dataConnect await _service.connector
.createTaxForm( .createTaxForm(
staffId: staffId, staffId: staffId,
formType: formType:
@@ -95,10 +67,10 @@ class TaxFormsRepositoryImpl
@override @override
Future<void> updateI9Form(I9TaxForm form) async { Future<void> updateI9Form(I9TaxForm form) async {
return executeProtected(() async { return _service.run(() async {
final Map<String, dynamic> data = form.formData; final Map<String, dynamic> data = form.formData;
final dc.UpdateTaxFormVariablesBuilder builder = final dc.UpdateTaxFormVariablesBuilder builder =
dataConnect.updateTaxForm(id: form.id); _service.connector.updateTaxForm(id: form.id);
_mapCommonFields(builder, data); _mapCommonFields(builder, data);
_mapI9Fields(builder, data); _mapI9Fields(builder, data);
await builder.execute(); await builder.execute();
@@ -107,10 +79,10 @@ class TaxFormsRepositoryImpl
@override @override
Future<void> submitI9Form(I9TaxForm form) async { Future<void> submitI9Form(I9TaxForm form) async {
return executeProtected(() async { return _service.run(() async {
final Map<String, dynamic> data = form.formData; final Map<String, dynamic> data = form.formData;
final dc.UpdateTaxFormVariablesBuilder builder = final dc.UpdateTaxFormVariablesBuilder builder =
dataConnect.updateTaxForm(id: form.id); _service.connector.updateTaxForm(id: form.id);
_mapCommonFields(builder, data); _mapCommonFields(builder, data);
_mapI9Fields(builder, data); _mapI9Fields(builder, data);
await builder.status(dc.TaxFormStatus.SUBMITTED).execute(); await builder.status(dc.TaxFormStatus.SUBMITTED).execute();
@@ -119,10 +91,10 @@ class TaxFormsRepositoryImpl
@override @override
Future<void> updateW4Form(W4TaxForm form) async { Future<void> updateW4Form(W4TaxForm form) async {
return executeProtected(() async { return _service.run(() async {
final Map<String, dynamic> data = form.formData; final Map<String, dynamic> data = form.formData;
final dc.UpdateTaxFormVariablesBuilder builder = final dc.UpdateTaxFormVariablesBuilder builder =
dataConnect.updateTaxForm(id: form.id); _service.connector.updateTaxForm(id: form.id);
_mapCommonFields(builder, data); _mapCommonFields(builder, data);
_mapW4Fields(builder, data); _mapW4Fields(builder, data);
await builder.execute(); await builder.execute();
@@ -131,10 +103,10 @@ class TaxFormsRepositoryImpl
@override @override
Future<void> submitW4Form(W4TaxForm form) async { Future<void> submitW4Form(W4TaxForm form) async {
return executeProtected(() async { return _service.run(() async {
final Map<String, dynamic> data = form.formData; final Map<String, dynamic> data = form.formData;
final dc.UpdateTaxFormVariablesBuilder builder = final dc.UpdateTaxFormVariablesBuilder builder =
dataConnect.updateTaxForm(id: form.id); _service.connector.updateTaxForm(id: form.id);
_mapCommonFields(builder, data); _mapCommonFields(builder, data);
_mapW4Fields(builder, data); _mapW4Fields(builder, data);
await builder.status(dc.TaxFormStatus.SUBMITTED).execute(); await builder.status(dc.TaxFormStatus.SUBMITTED).execute();

View File

@@ -1,7 +1,5 @@
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_modular/flutter_modular.dart'; import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_core/core.dart'; import 'package:krow_core/core.dart';
import 'package:krow_data_connect/krow_data_connect.dart';
import 'package:krow_domain/krow_domain.dart'; import 'package:krow_domain/krow_domain.dart';
import 'data/repositories/tax_forms_repository_impl.dart'; import 'data/repositories/tax_forms_repository_impl.dart';
import 'domain/repositories/tax_forms_repository.dart'; import 'domain/repositories/tax_forms_repository.dart';
@@ -18,12 +16,7 @@ import 'presentation/pages/tax_forms_page.dart';
class StaffTaxFormsModule extends Module { class StaffTaxFormsModule extends Module {
@override @override
void binds(Injector i) { void binds(Injector i) {
i.addLazySingleton<TaxFormsRepository>( i.addLazySingleton<TaxFormsRepository>(TaxFormsRepositoryImpl.new);
() => TaxFormsRepositoryImpl(
firebaseAuth: FirebaseAuth.instance,
dataConnect: ExampleConnector.instance,
),
);
// Use Cases // Use Cases
i.addLazySingleton(GetTaxFormsUseCase.new); i.addLazySingleton(GetTaxFormsUseCase.new);

View File

@@ -1,31 +1,28 @@
import 'package:firebase_auth/firebase_auth.dart' as auth;
import 'package:firebase_data_connect/firebase_data_connect.dart'; import 'package:firebase_data_connect/firebase_data_connect.dart';
import 'package:krow_data_connect/krow_data_connect.dart'; import 'package:krow_data_connect/krow_data_connect.dart';
import 'package:krow_domain/krow_domain.dart'; import 'package:krow_domain/krow_domain.dart';
import '../../domain/repositories/bank_account_repository.dart'; import '../../domain/repositories/bank_account_repository.dart';
/// Implementation of [BankAccountRepository] that integrates with Data Connect. /// Implementation of [BankAccountRepository] that integrates with Data Connect.
class BankAccountRepositoryImpl class BankAccountRepositoryImpl implements BankAccountRepository {
with DataErrorHandler
implements BankAccountRepository {
/// Creates a [BankAccountRepositoryImpl]. /// Creates a [BankAccountRepositoryImpl].
const BankAccountRepositoryImpl({ BankAccountRepositoryImpl({
required this.dataConnect, DataConnectService? service,
required this.firebaseAuth, }) : _service = service ?? DataConnectService.instance;
});
/// The Data Connect instance. /// The Data Connect service.
final ExampleConnector dataConnect; final DataConnectService _service;
/// The Firebase Auth instance.
final auth.FirebaseAuth firebaseAuth;
@override @override
Future<List<BankAccount>> getAccounts() async { Future<List<BankAccount>> getAccounts() async {
return executeProtected(() async { return _service.run(() async {
final String staffId = _getStaffId(); final String staffId = await _service.getStaffId();
var x = staffId;
print(x);
final QueryResult<GetAccountsByOwnerIdData, GetAccountsByOwnerIdVariables> final QueryResult<GetAccountsByOwnerIdData, GetAccountsByOwnerIdVariables>
result = await dataConnect result = await _service.connector
.getAccountsByOwnerId(ownerId: staffId) .getAccountsByOwnerId(ownerId: staffId)
.execute(); .execute();
@@ -37,7 +34,9 @@ class BankAccountRepositoryImpl
accountNumber: account.accountNumber, accountNumber: account.accountNumber,
last4: account.last4, last4: account.last4,
sortCode: account.routeNumber, sortCode: account.routeNumber,
type: account.type is Known<AccountType> ? (account.type as Known<AccountType>).value.name : null, type: account.type is Known<AccountType>
? (account.type as Known<AccountType>).value.name
: null,
isPrimary: account.isPrimary, isPrimary: account.isPrimary,
); );
}).toList(); }).toList();
@@ -46,44 +45,31 @@ class BankAccountRepositoryImpl
@override @override
Future<void> addAccount(BankAccount account) async { Future<void> addAccount(BankAccount account) async {
return executeProtected(() async { return _service.run(() async {
final String staffId = _getStaffId(); final String staffId = await _service.getStaffId();
final QueryResult<GetAccountsByOwnerIdData, GetAccountsByOwnerIdVariables> final QueryResult<GetAccountsByOwnerIdData, GetAccountsByOwnerIdVariables>
existingAccounts = await dataConnect existingAccounts = await _service.connector
.getAccountsByOwnerId(ownerId: staffId) .getAccountsByOwnerId(ownerId: staffId)
.execute(); .execute();
final bool hasAccounts = existingAccounts.data.accounts.isNotEmpty; final bool hasAccounts = existingAccounts.data.accounts.isNotEmpty;
final bool isPrimary = !hasAccounts; final bool isPrimary = !hasAccounts;
await dataConnect.createAccount( await _service.connector
bank: account.bankName, .createAccount(
type: AccountType.values.byName(BankAccountAdapter.typeToString(account.type)), bank: account.bankName,
last4: _safeLast4(account.last4, account.accountNumber), type: AccountType.values
ownerId: staffId, .byName(BankAccountAdapter.typeToString(account.type)),
) last4: _safeLast4(account.last4, account.accountNumber),
.isPrimary(isPrimary) ownerId: staffId,
.accountNumber(account.accountNumber) )
.routeNumber(account.sortCode) .isPrimary(isPrimary)
.execute(); .accountNumber(account.accountNumber)
.routeNumber(account.sortCode)
.execute();
}); });
} }
/// Helper to get the logged-in staff ID.
String _getStaffId() {
final auth.User? user = firebaseAuth.currentUser;
if (user == null) {
throw const NotAuthenticatedException(
technicalMessage: 'User not authenticated');
}
final String? staffId = StaffSessionStore.instance.session?.staff?.id;
if (staffId == null || staffId.isEmpty) {
throw const ServerException(technicalMessage: 'Staff profile is missing or session not initialized.');
}
return staffId;
}
/// Ensures we have a last4 value, either from input or derived from account number. /// Ensures we have a last4 value, either from input or derived from account number.
String _safeLast4(String? last4, String accountNumber) { String _safeLast4(String? last4, String accountNumber) {
if (last4 != null && last4.isNotEmpty) { if (last4 != null && last4.isNotEmpty) {

View File

@@ -1,4 +1,4 @@
import 'package:firebase_auth/firebase_auth.dart' as auth; import 'package:flutter/widgets.dart';
import 'package:flutter_modular/flutter_modular.dart'; import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_core/core.dart'; import 'package:krow_core/core.dart';
import 'package:krow_data_connect/krow_data_connect.dart'; import 'package:krow_data_connect/krow_data_connect.dart';
@@ -17,12 +17,7 @@ class StaffBankAccountModule extends Module {
@override @override
void binds(Injector i) { void binds(Injector i) {
// Repositories // Repositories
i.addLazySingleton<BankAccountRepository>( i.addLazySingleton<BankAccountRepository>(BankAccountRepositoryImpl.new);
() => BankAccountRepositoryImpl(
firebaseAuth: auth.FirebaseAuth.instance,
dataConnect: ExampleConnector.instance,
),
);
// Use Cases // Use Cases
i.addLazySingleton<GetBankAccountsUseCase>(GetBankAccountsUseCase.new); i.addLazySingleton<GetBankAccountsUseCase>(GetBankAccountsUseCase.new);
@@ -41,7 +36,7 @@ class StaffBankAccountModule extends Module {
void routes(RouteManager r) { void routes(RouteManager r) {
r.child( r.child(
StaffPaths.childRoute(StaffPaths.bankAccount, StaffPaths.bankAccount), StaffPaths.childRoute(StaffPaths.bankAccount, StaffPaths.bankAccount),
child: (_) => const BankAccountPage(), child: (BuildContext context) => const BankAccountPage(),
); );
} }
} }

View File

@@ -1,63 +1,46 @@
import 'package:firebase_auth/firebase_auth.dart' as firebase;
import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc; import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc;
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
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';
// ignore: implementation_imports // ignore: implementation_imports
import 'package:krow_domain/src/adapters/financial/time_card_adapter.dart'; import 'package:krow_domain/src/adapters/financial/time_card_adapter.dart';
import 'package:krow_core/core.dart';
import '../../domain/repositories/time_card_repository.dart'; import '../../domain/repositories/time_card_repository.dart';
/// Implementation of [TimeCardRepository] using Firebase Data Connect. /// Implementation of [TimeCardRepository] using Firebase Data Connect.
class TimeCardRepositoryImpl class TimeCardRepositoryImpl implements TimeCardRepository {
with dc.DataErrorHandler final dc.DataConnectService _service;
implements TimeCardRepository {
final dc.ExampleConnector _dataConnect;
final firebase.FirebaseAuth _firebaseAuth;
/// Creates a [TimeCardRepositoryImpl]. /// Creates a [TimeCardRepositoryImpl].
TimeCardRepositoryImpl({ TimeCardRepositoryImpl({dc.DataConnectService? service})
required dc.ExampleConnector dataConnect, : _service = service ?? dc.DataConnectService.instance;
required firebase.FirebaseAuth firebaseAuth,
}) : _dataConnect = dataConnect,
_firebaseAuth = firebaseAuth;
Future<String> _getStaffId() async {
final firebase.User? user = _firebaseAuth.currentUser;
if (user == null) {
throw const NotAuthenticatedException(
technicalMessage: 'User not authenticated');
}
final fdc.QueryResult<dc.GetStaffByUserIdData, dc.GetStaffByUserIdVariables> result =
await _dataConnect.getStaffByUserId(userId: user.uid).execute();
if (result.data.staffs.isEmpty) {
throw const ServerException(technicalMessage: 'Staff profile not found');
}
return result.data.staffs.first.id;
}
@override @override
Future<List<TimeCard>> getTimeCards(DateTime month) async { Future<List<TimeCard>> getTimeCards(DateTime month) async {
return executeProtected(() async { return _service.run(() async {
final String staffId = await _getStaffId(); final String staffId = await _service.getStaffId();
// Fetch applications. Limit can be adjusted, assuming 100 is safe for now. // Fetch applications. Limit can be adjusted, assuming 100 is safe for now.
final fdc.QueryResult<dc.GetApplicationsByStaffIdData, dc.GetApplicationsByStaffIdVariables> result = final fdc.QueryResult<dc.GetApplicationsByStaffIdData,
await _dataConnect.getApplicationsByStaffId(staffId: staffId).limit(100).execute(); dc.GetApplicationsByStaffIdVariables> result =
await _service.connector
.getApplicationsByStaffId(staffId: staffId)
.limit(100)
.execute();
return result.data.applications return result.data.applications
.where((dc.GetApplicationsByStaffIdApplications app) { .where((dc.GetApplicationsByStaffIdApplications app) {
final DateTime? shiftDate = app.shift.date == null final DateTime? shiftDate = _service.toDateTime(app.shift.date);
? null
: DateTimeUtils.toDeviceTime(app.shift.date!.toDateTime());
if (shiftDate == null) return false; if (shiftDate == null) return false;
return shiftDate.year == month.year && shiftDate.month == month.month; return shiftDate.year == month.year &&
shiftDate.month == month.month;
}) })
.map((dc.GetApplicationsByStaffIdApplications app) { .map((dc.GetApplicationsByStaffIdApplications app) {
final DateTime shiftDate = final DateTime shiftDate = _service.toDateTime(app.shift.date)!;
DateTimeUtils.toDeviceTime(app.shift.date!.toDateTime()); final String startTime = _formatTime(app.checkInTime) ??
final String startTime = _formatTime(app.checkInTime) ?? _formatTime(app.shift.startTime) ?? ''; _formatTime(app.shift.startTime) ??
final String endTime = _formatTime(app.checkOutTime) ?? _formatTime(app.shift.endTime) ?? ''; '';
final String endTime = _formatTime(app.checkOutTime) ??
_formatTime(app.shift.endTime) ??
'';
// Prefer shiftRole values for pay/hours // Prefer shiftRole values for pay/hours
final double hours = app.shiftRole.hours ?? 0.0; final double hours = app.shiftRole.hours ?? 0.0;
@@ -84,7 +67,8 @@ class TimeCardRepositoryImpl
String? _formatTime(fdc.Timestamp? timestamp) { String? _formatTime(fdc.Timestamp? timestamp) {
if (timestamp == null) return null; if (timestamp == null) return null;
return DateFormat('HH:mm') final DateTime? dt = _service.toDateTime(timestamp);
.format(DateTimeUtils.toDeviceTime(timestamp.toDateTime())); if (dt == null) return null;
return DateFormat('HH:mm').format(dt);
} }
} }

View File

@@ -1,6 +1,6 @@
library staff_time_card; library staff_time_card;
import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_modular/flutter_modular.dart'; import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_core/core.dart'; import 'package:krow_core/core.dart';
import 'package:krow_data_connect/krow_data_connect.dart'; import 'package:krow_data_connect/krow_data_connect.dart';
@@ -24,12 +24,7 @@ class StaffTimeCardModule extends Module {
@override @override
void binds(Injector i) { void binds(Injector i) {
// Repositories // Repositories
i.add<TimeCardRepository>( i.addLazySingleton<TimeCardRepository>(TimeCardRepositoryImpl.new);
() => TimeCardRepositoryImpl(
dataConnect: ExampleConnector.instance,
firebaseAuth: FirebaseAuth.instance,
),
);
// UseCases // UseCases
i.add<GetTimeCardsUseCase>(GetTimeCardsUseCase.new); i.add<GetTimeCardsUseCase>(GetTimeCardsUseCase.new);
@@ -42,7 +37,7 @@ class StaffTimeCardModule extends Module {
void routes(RouteManager r) { void routes(RouteManager r) {
r.child( r.child(
StaffPaths.childRoute(StaffPaths.timeCard, StaffPaths.timeCard), StaffPaths.childRoute(StaffPaths.timeCard, StaffPaths.timeCard),
child: (context) => const TimeCardPage(), child: (BuildContext context) => const TimeCardPage(),
); );
} }
} }

View File

@@ -1,6 +1,5 @@
import 'package:flutter_modular/flutter_modular.dart'; import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_core/core.dart'; import 'package:krow_core/core.dart';
import 'package:krow_data_connect/krow_data_connect.dart';
import 'data/repositories_impl/attire_repository_impl.dart'; import 'data/repositories_impl/attire_repository_impl.dart';
import 'domain/repositories/attire_repository.dart'; import 'domain/repositories/attire_repository.dart';
@@ -14,9 +13,7 @@ class StaffAttireModule extends Module {
@override @override
void binds(Injector i) { void binds(Injector i) {
// Repository // Repository
i.addLazySingleton<AttireRepository>( i.addLazySingleton<AttireRepository>(AttireRepositoryImpl.new);
() => AttireRepositoryImpl(ExampleConnector.instance),
);
// Use Cases // Use Cases
i.addLazySingleton(GetAttireOptionsUseCase.new); i.addLazySingleton(GetAttireOptionsUseCase.new);

View File

@@ -6,24 +6,30 @@ import '../../domain/repositories/attire_repository.dart';
/// Implementation of [AttireRepository]. /// Implementation of [AttireRepository].
/// ///
/// Delegates data access to [ExampleConnector] from `data_connect`. /// Delegates data access to [DataConnectService].
class AttireRepositoryImpl implements AttireRepository { class AttireRepositoryImpl implements AttireRepository {
/// The Data Connect connector instance. /// The Data Connect service.
final ExampleConnector _connector; final DataConnectService _service;
/// Creates an [AttireRepositoryImpl]. /// Creates an [AttireRepositoryImpl].
AttireRepositoryImpl(this._connector); AttireRepositoryImpl({DataConnectService? service})
: _service = service ?? DataConnectService.instance;
@override @override
Future<List<AttireItem>> getAttireOptions() async { Future<List<AttireItem>> getAttireOptions() async {
final QueryResult<ListAttireOptionsData, void> result = await _connector.listAttireOptions().execute(); return _service.run(() async {
return result.data.attireOptions.map((ListAttireOptionsAttireOptions e) => AttireItem( final QueryResult<ListAttireOptionsData, void> result =
id: e.itemId, await _service.connector.listAttireOptions().execute();
label: e.label, return result.data.attireOptions
iconName: e.icon, .map((ListAttireOptionsAttireOptions e) => AttireItem(
imageUrl: e.imageUrl, id: e.itemId,
isMandatory: e.isMandatory ?? false, label: e.label,
)).toList(); iconName: e.icon,
imageUrl: e.imageUrl,
isMandatory: e.isMandatory ?? false,
))
.toList();
});
} }
@override @override

View File

@@ -1,4 +1,3 @@
import 'package:firebase_auth/firebase_auth.dart';
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 '../../domain/repositories/emergency_contact_repository_interface.dart'; import '../../domain/repositories/emergency_contact_repository_interface.dart';
@@ -7,38 +6,19 @@ import '../../domain/repositories/emergency_contact_repository_interface.dart';
/// ///
/// This repository delegates data operations to Firebase Data Connect. /// This repository delegates data operations to Firebase Data Connect.
class EmergencyContactRepositoryImpl class EmergencyContactRepositoryImpl
with dc.DataErrorHandler
implements EmergencyContactRepositoryInterface { implements EmergencyContactRepositoryInterface {
final dc.ExampleConnector _dataConnect; final dc.DataConnectService _service;
final FirebaseAuth _firebaseAuth;
/// Creates an [EmergencyContactRepositoryImpl]. /// Creates an [EmergencyContactRepositoryImpl].
EmergencyContactRepositoryImpl({ EmergencyContactRepositoryImpl({
required dc.ExampleConnector dataConnect, dc.DataConnectService? service,
required FirebaseAuth firebaseAuth, }) : _service = service ?? dc.DataConnectService.instance;
}) : _dataConnect = dataConnect,
_firebaseAuth = firebaseAuth;
Future<String> _getStaffId() async {
final user = _firebaseAuth.currentUser;
if (user == null) {
throw const NotAuthenticatedException(
technicalMessage: 'User not authenticated');
}
final result =
await _dataConnect.getStaffByUserId(userId: user.uid).execute();
if (result.data.staffs.isEmpty) {
throw const ServerException(technicalMessage: 'Staff profile not found');
}
return result.data.staffs.first.id;
}
@override @override
Future<List<EmergencyContact>> getContacts() async { Future<List<EmergencyContact>> getContacts() async {
return executeProtected(() async { return _service.run(() async {
final staffId = await _getStaffId(); final staffId = await _service.getStaffId();
final result = await _dataConnect final result = await _service.connector
.getEmergencyContactsByStaffId(staffId: staffId) .getEmergencyContactsByStaffId(staffId: staffId)
.execute(); .execute();
@@ -55,11 +35,11 @@ class EmergencyContactRepositoryImpl
@override @override
Future<void> saveContacts(List<EmergencyContact> contacts) async { Future<void> saveContacts(List<EmergencyContact> contacts) async {
return executeProtected(() async { return _service.run(() async {
final staffId = await _getStaffId(); final staffId = await _service.getStaffId();
// 1. Get existing to delete // 1. Get existing to delete
final existingResult = await _dataConnect final existingResult = await _service.connector
.getEmergencyContactsByStaffId(staffId: staffId) .getEmergencyContactsByStaffId(staffId: staffId)
.execute(); .execute();
final existingIds = final existingIds =
@@ -67,7 +47,7 @@ class EmergencyContactRepositoryImpl
// 2. Delete all existing // 2. Delete all existing
await Future.wait(existingIds.map( await Future.wait(existingIds.map(
(id) => _dataConnect.deleteEmergencyContact(id: id).execute())); (id) => _service.connector.deleteEmergencyContact(id: id).execute()));
// 3. Create new // 3. Create new
await Future.wait(contacts.map((contact) { await Future.wait(contacts.map((contact) {
@@ -87,7 +67,7 @@ class EmergencyContactRepositoryImpl
break; break;
} }
return _dataConnect return _service.connector
.createEmergencyContact( .createEmergencyContact(
name: contact.name, name: contact.name,
phone: contact.phone, phone: contact.phone,

View File

@@ -1,6 +1,5 @@
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_modular/flutter_modular.dart'; import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_data_connect/krow_data_connect.dart';
import 'data/repositories/emergency_contact_repository_impl.dart'; import 'data/repositories/emergency_contact_repository_impl.dart';
import 'domain/repositories/emergency_contact_repository_interface.dart'; import 'domain/repositories/emergency_contact_repository_interface.dart';
import 'domain/usecases/get_emergency_contacts_usecase.dart'; import 'domain/usecases/get_emergency_contacts_usecase.dart';
@@ -13,10 +12,7 @@ class StaffEmergencyContactModule extends Module {
void binds(Injector i) { void binds(Injector i) {
// Repository // Repository
i.addLazySingleton<EmergencyContactRepositoryInterface>( i.addLazySingleton<EmergencyContactRepositoryInterface>(
() => EmergencyContactRepositoryImpl( EmergencyContactRepositoryImpl.new,
dataConnect: ExampleConnector.instance,
firebaseAuth: FirebaseAuth.instance,
),
); );
// UseCases // UseCases

View File

@@ -1,42 +1,31 @@
import 'package:firebase_auth/firebase_auth.dart';
import 'package:krow_data_connect/krow_data_connect.dart' as dc; import 'package:krow_data_connect/krow_data_connect.dart' as dc;
import '../../domain/repositories/experience_repository_interface.dart';
import 'package:krow_domain/krow_domain.dart'; import 'package:krow_domain/krow_domain.dart';
import '../../domain/repositories/experience_repository_interface.dart';
/// Implementation of [ExperienceRepositoryInterface] that delegates to Data Connect. /// Implementation of [ExperienceRepositoryInterface] that delegates to Data Connect.
class ExperienceRepositoryImpl class ExperienceRepositoryImpl implements ExperienceRepositoryInterface {
with dc.DataErrorHandler final dc.DataConnectService _service;
implements ExperienceRepositoryInterface {
final dc.ExampleConnector _dataConnect;
// ignore: unused_field
final FirebaseAuth _firebaseAuth;
/// Creates a [ExperienceRepositoryImpl] using Data Connect and Auth. /// Creates a [ExperienceRepositoryImpl] using Data Connect Service.
ExperienceRepositoryImpl({ ExperienceRepositoryImpl({
required dc.ExampleConnector dataConnect, dc.DataConnectService? service,
required FirebaseAuth firebaseAuth, }) : _service = service ?? dc.DataConnectService.instance;
}) : _dataConnect = dataConnect,
_firebaseAuth = firebaseAuth;
Future<dc.GetStaffByUserIdStaffs> _getStaff() async { Future<dc.GetStaffByIdStaff> _getStaff() async {
final user = _firebaseAuth.currentUser; final staffId = await _service.getStaffId();
if (user == null) {
throw const NotAuthenticatedException(
technicalMessage: 'User not authenticated');
}
final result = final result =
await _dataConnect.getStaffByUserId(userId: user.uid).execute(); await _service.connector.getStaffById(id: staffId).execute();
if (result.data.staffs.isEmpty) { if (result.data.staff == null) {
throw const ServerException(technicalMessage: 'Staff profile not found'); throw const ServerException(technicalMessage: 'Staff profile not found');
} }
return result.data.staffs.first; return result.data.staff!;
} }
@override @override
Future<List<String>> getIndustries() async { Future<List<String>> getIndustries() async {
return executeProtected(() async { return _service.run(() async {
final staff = await _getStaff(); final staff = await _getStaff();
return staff.industries ?? []; return staff.industries ?? [];
}); });
@@ -44,7 +33,7 @@ class ExperienceRepositoryImpl
@override @override
Future<List<String>> getSkills() async { Future<List<String>> getSkills() async {
return executeProtected(() async { return _service.run(() async {
final staff = await _getStaff(); final staff = await _getStaff();
return staff.skills ?? []; return staff.skills ?? [];
}); });
@@ -55,9 +44,9 @@ class ExperienceRepositoryImpl
List<String> industries, List<String> industries,
List<String> skills, List<String> skills,
) async { ) async {
return executeProtected(() async { return _service.run(() async {
final staff = await _getStaff(); final staff = await _getStaff();
await _dataConnect await _service.connector
.updateStaff(id: staff.id) .updateStaff(id: staff.id)
.industries(industries) .industries(industries)
.skills(skills) .skills(skills)

View File

@@ -1,6 +1,5 @@
library staff_profile_experience; library staff_profile_experience;
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_modular/flutter_modular.dart'; import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_data_connect/krow_data_connect.dart'; import 'package:krow_data_connect/krow_data_connect.dart';
@@ -22,10 +21,7 @@ class StaffProfileExperienceModule extends Module {
void binds(Injector i) { void binds(Injector i) {
// Repository // Repository
i.addLazySingleton<ExperienceRepositoryInterface>( i.addLazySingleton<ExperienceRepositoryInterface>(
() => ExperienceRepositoryImpl( ExperienceRepositoryImpl.new,
dataConnect: ExampleConnector.instance,
firebaseAuth: FirebaseAuth.instance,
),
); );
// UseCases // UseCases

View File

@@ -1,4 +1,3 @@
import 'package:firebase_auth/firebase_auth.dart' as firebase_auth;
import 'package:firebase_data_connect/firebase_data_connect.dart'; import 'package:firebase_data_connect/firebase_data_connect.dart';
import 'package:krow_data_connect/krow_data_connect.dart'; import 'package:krow_data_connect/krow_data_connect.dart';
import 'package:krow_domain/krow_domain.dart'; import 'package:krow_domain/krow_domain.dart';
@@ -14,32 +13,24 @@ import '../../domain/repositories/personal_info_repository_interface.dart';
/// - Mapping between data_connect DTOs and domain entities /// - Mapping between data_connect DTOs and domain entities
/// - Containing no business logic /// - Containing no business logic
class PersonalInfoRepositoryImpl class PersonalInfoRepositoryImpl
with DataErrorHandler
implements PersonalInfoRepositoryInterface { implements PersonalInfoRepositoryInterface {
/// Creates a [PersonalInfoRepositoryImpl]. /// Creates a [PersonalInfoRepositoryImpl].
/// ///
/// Requires the Firebase Data Connect connector instance and Firebase Auth. /// Requires the Firebase Data Connect service.
PersonalInfoRepositoryImpl({ PersonalInfoRepositoryImpl({
required ExampleConnector dataConnect, DataConnectService? service,
required firebase_auth.FirebaseAuth firebaseAuth, }) : _service = service ?? DataConnectService.instance;
}) : _dataConnect = dataConnect,
_firebaseAuth = firebaseAuth; final DataConnectService _service;
final ExampleConnector _dataConnect;
final firebase_auth.FirebaseAuth _firebaseAuth;
@override @override
Future<Staff> getStaffProfile() async { Future<Staff> getStaffProfile() async {
return executeProtected(() async { return _service.run(() async {
final firebase_auth.User? user = _firebaseAuth.currentUser; final String uid = _service.auth.currentUser!.uid;
if (user == null) {
throw NotAuthenticatedException(
technicalMessage: 'User not authenticated');
}
// Query staff data from Firebase Data Connect // Query staff data from Firebase Data Connect
final QueryResult<GetStaffByUserIdData, GetStaffByUserIdVariables> result = final QueryResult<GetStaffByUserIdData, GetStaffByUserIdVariables> result =
await _dataConnect.getStaffByUserId(userId: user.uid).execute(); await _service.connector.getStaffByUserId(userId: uid).execute();
if (result.data.staffs.isEmpty) { if (result.data.staffs.isEmpty) {
throw const ServerException(technicalMessage: 'Staff profile not found'); throw const ServerException(technicalMessage: 'Staff profile not found');
@@ -53,10 +44,12 @@ class PersonalInfoRepositoryImpl
} }
@override @override
Future<Staff> updateStaffProfile({required String staffId, required Map<String, dynamic> data}) async { Future<Staff> updateStaffProfile(
return executeProtected(() async { {required String staffId, required Map<String, dynamic> data}) async {
return _service.run(() async {
// Start building the update mutation // Start building the update mutation
UpdateStaffVariablesBuilder updateBuilder = _dataConnect.updateStaff(id: staffId); UpdateStaffVariablesBuilder updateBuilder =
_service.connector.updateStaff(id: staffId);
// Apply updates from map if present // Apply updates from map if present
if (data.containsKey('name')) { if (data.containsKey('name')) {
@@ -72,8 +65,9 @@ class PersonalInfoRepositoryImpl
updateBuilder = updateBuilder.photoUrl(data['avatar'] as String?); updateBuilder = updateBuilder.photoUrl(data['avatar'] as String?);
} }
if (data.containsKey('preferredLocations')) { if (data.containsKey('preferredLocations')) {
// After schema update and SDK regeneration, preferredLocations accepts List<String> // After schema update and SDK regeneration, preferredLocations accepts List<String>
updateBuilder = updateBuilder.preferredLocations(data['preferredLocations'] as List<String>); updateBuilder = updateBuilder.preferredLocations(
data['preferredLocations'] as List<String>);
} }
// Execute the update // Execute the update
@@ -81,7 +75,8 @@ class PersonalInfoRepositoryImpl
await updateBuilder.execute(); await updateBuilder.execute();
if (result.data.staff_update == null) { if (result.data.staff_update == null) {
throw const ServerException(technicalMessage: 'Failed to update staff profile'); throw const ServerException(
technicalMessage: 'Failed to update staff profile');
} }
// Fetch the updated staff profile to return complete entity // Fetch the updated staff profile to return complete entity

View File

@@ -1,7 +1,5 @@
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_modular/flutter_modular.dart'; import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_data_connect/krow_data_connect.dart';
import 'data/repositories/personal_info_repository_impl.dart'; import 'data/repositories/personal_info_repository_impl.dart';
import 'domain/repositories/personal_info_repository_interface.dart'; import 'domain/repositories/personal_info_repository_interface.dart';
@@ -25,11 +23,7 @@ class StaffProfileInfoModule extends Module {
void binds(Injector i) { void binds(Injector i) {
// Repository // Repository
i.addLazySingleton<PersonalInfoRepositoryInterface>( i.addLazySingleton<PersonalInfoRepositoryInterface>(
() => PersonalInfoRepositoryImpl( PersonalInfoRepositoryImpl.new);
dataConnect: ExampleConnector.instance,
firebaseAuth: FirebaseAuth.instance,
),
);
// Use Cases - delegate business logic to repository // Use Cases - delegate business logic to repository
i.addLazySingleton<GetPersonalInfoUseCase>( i.addLazySingleton<GetPersonalInfoUseCase>(

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());