feat(clock-in): Refactor ClockInRepositoryImpl to utilize DataConnectService and simplify dependency injection

This commit is contained in:
Achintha Isuru
2026-02-16 16:00:27 -05:00
parent 1f7134799b
commit 66859e4241
2 changed files with 135 additions and 178 deletions

View File

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

View File

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