feat: Migrate staff profile features from Data Connect to V2 REST API

- Removed data_connect package from mobile pubspec.yaml.
- Added documentation for V2 profile migration status and QA findings.
- Implemented new session management with ClientSessionStore and StaffSessionStore.
- Created V2SessionService for handling user sessions via the V2 API.
- Developed use cases for cancelling late worker assignments and submitting worker reviews.
- Added arguments and use cases for payment chart retrieval and profile completion checks.
- Implemented repository interfaces and their implementations for staff main and profile features.
- Ensured proper error handling and validation in use cases.
This commit is contained in:
Achintha Isuru
2026-03-16 22:45:06 -04:00
parent 4834266986
commit b31a615092
478 changed files with 10512 additions and 19854 deletions

View File

@@ -1,235 +1,99 @@
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_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../../domain/repositories/clock_in_repository_interface.dart';
import 'package:staff_clock_in/src/domain/repositories/clock_in_repository_interface.dart';
/// Implementation of [ClockInRepositoryInterface] using Firebase Data Connect.
/// Implementation of [ClockInRepositoryInterface] using the V2 REST API.
///
/// All backend calls go through [BaseApiService] with [V2ApiEndpoints].
/// The old Data Connect implementation has been removed.
class ClockInRepositoryImpl implements ClockInRepositoryInterface {
ClockInRepositoryImpl() : _service = dc.DataConnectService.instance;
/// Creates a [ClockInRepositoryImpl] backed by the V2 API.
ClockInRepositoryImpl({required BaseApiService apiService})
: _apiService = apiService;
final dc.DataConnectService _service;
final Map<String, String> _shiftToApplicationId = <String, String>{};
String? _activeApplicationId;
({fdc.Timestamp start, fdc.Timestamp end}) _utcDayRange(DateTime localDay) {
final DateTime dayStartUtc = DateTime.utc(
localDay.year,
localDay.month,
localDay.day,
);
final DateTime dayEndUtc = DateTime.utc(
localDay.year,
localDay.month,
localDay.day,
23,
59,
59,
999,
999,
);
return (
start: _service.toTimestamp(dayStartUtc),
end: _service.toTimestamp(dayEndUtc),
);
}
/// Helper to find today's applications ordered with the closest at the end.
Future<List<dc.GetApplicationsByStaffIdApplications>> _getTodaysApplications(
String staffId,
) async {
final DateTime now = DateTime.now();
final ({fdc.Timestamp start, fdc.Timestamp end}) range = _utcDayRange(now);
final fdc.QueryResult<dc.GetApplicationsByStaffIdData,
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;
if (apps.isEmpty) return const <dc.GetApplicationsByStaffIdApplications>[];
_shiftToApplicationId
..clear()
..addEntries(apps.map((dc.GetApplicationsByStaffIdApplications app) =>
MapEntry<String, String>(app.shiftId, app.id)));
apps.sort((dc.GetApplicationsByStaffIdApplications a,
dc.GetApplicationsByStaffIdApplications b) {
final DateTime? aTime =
_service.toDateTime(a.shift.startTime) ?? _service.toDateTime(a.shift.date);
final DateTime? bTime =
_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;
final Duration aDiff = aTime.difference(now).abs();
final Duration bDiff = bTime.difference(now).abs();
return bDiff.compareTo(aDiff); // closest at the end
});
return apps;
}
final BaseApiService _apiService;
@override
Future<List<Shift>> getTodaysShifts() async {
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 = _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,
),
);
}
return shifts;
});
final ApiResponse response = await _apiService.get(
V2ApiEndpoints.staffClockInShiftsToday,
);
final List<dynamic> items = response.data['items'] as List<dynamic>;
// TODO: Ask BE to add latitude, longitude, hourlyRate, and clientName
// to the listTodayShifts query to avoid mapping gaps and extra API calls.
return items
.map(
(dynamic json) =>
_mapTodayShiftJsonToShift(json as Map<String, dynamic>),
)
.toList();
}
@override
Future<AttendanceStatus> getAttendanceStatus() async {
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 = _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);
}
_activeApplicationId = activeApp.id;
return AttendanceStatus(
isCheckedIn: true,
checkInTime: _service.toDateTime(activeApp.checkInTime),
checkOutTime: _service.toDateTime(activeApp.checkOutTime),
activeShiftId: activeApp.shiftId,
activeApplicationId: activeApp.id,
);
});
final ApiResponse response = await _apiService.get(
V2ApiEndpoints.staffClockInStatus,
);
return AttendanceStatus.fromJson(response.data as Map<String, dynamic>);
}
@override
Future<AttendanceStatus> clockIn({required String shiftId, String? notes}) async {
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 fdc.Timestamp checkInTs = _service.toTimestamp(DateTime.now());
await _service.connector
.updateApplicationStatus(
id: app.id,
)
.checkInTime(checkInTs)
.execute();
_activeApplicationId = app.id;
return getAttendanceStatus();
});
Future<AttendanceStatus> clockIn({
required String shiftId,
String? notes,
}) async {
await _apiService.post(
V2ApiEndpoints.staffClockIn,
data: <String, dynamic>{
'shiftId': shiftId,
'sourceType': 'GEO',
if (notes != null && notes.isNotEmpty) 'notes': notes,
},
);
// Re-fetch the attendance status to get the canonical state after clock-in.
return getAttendanceStatus();
}
@override
Future<AttendanceStatus> clockOut({
String? notes,
int? breakTimeMinutes,
String? applicationId,
String? shiftId,
}) async {
return _service.run(() async {
await _service.getStaffId(); // Validate session
await _apiService.post(
V2ApiEndpoints.staffClockOut,
data: <String, dynamic>{
if (shiftId != null) 'shiftId': shiftId,
'sourceType': 'GEO',
if (notes != null && notes.isNotEmpty) 'notes': notes,
if (breakTimeMinutes != null) 'breakMinutes': breakTimeMinutes,
},
);
// Re-fetch the attendance status to get the canonical state after clock-out.
return getAttendanceStatus();
}
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.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');
}
await _service.connector
.updateApplicationStatus(
id: targetAppId,
)
.checkOutTime(_service.toTimestamp(DateTime.now()))
.execute();
return getAttendanceStatus();
});
/// Maps a V2 `listTodayShifts` JSON item to the domain [Shift] entity.
///
/// The today-shifts endpoint returns a lightweight shape that lacks some
/// [Shift] fields. Missing fields are defaulted:
/// - `orderId` defaults to empty string
/// - `latitude` / `longitude` default to null (disables geofence)
/// - `requiredWorkers` / `assignedWorkers` default to 0
// TODO: Ask BE to add latitude/longitude to the listTodayShifts query
// to avoid losing geofence validation.
static Shift _mapTodayShiftJsonToShift(Map<String, dynamic> json) {
return Shift(
id: json['shiftId'] as String,
orderId: '',
title: json['roleName'] as String? ?? '',
status: ShiftStatus.fromJson(json['attendanceStatus'] as String?),
startsAt: DateTime.parse(json['startTime'] as String),
endsAt: DateTime.parse(json['endTime'] as String),
locationName: json['location'] as String?,
requiredWorkers: 0,
assignedWorkers: 0,
);
}
}

View File

@@ -1,23 +1,23 @@
import 'package:krow_core/core.dart';
/// Represents the arguments required for the [ClockOutUseCase].
/// Arguments required for the [ClockOutUseCase].
class ClockOutArguments extends UseCaseArgument {
/// Creates a [ClockOutArguments] instance.
const ClockOutArguments({
this.notes,
this.breakTimeMinutes,
this.applicationId,
this.shiftId,
});
/// Optional notes provided by the user during clock-out.
final String? notes;
/// Optional break time in minutes.
final int? breakTimeMinutes;
/// Optional application id for checkout.
final String? applicationId;
/// The shift id used by the V2 API to resolve the assignment.
final String? shiftId;
@override
List<Object?> get props => <Object?>[notes, breakTimeMinutes, applicationId];
List<Object?> get props => <Object?>[notes, breakTimeMinutes, shiftId];
}

View File

@@ -16,10 +16,12 @@ abstract class ClockInRepositoryInterface {
Future<AttendanceStatus> clockIn({required String shiftId, String? notes});
/// Checks the user out for the currently active shift.
/// Optionally accepts [breakTimeMinutes] if tracked.
///
/// The V2 API resolves the assignment from [shiftId]. Optionally accepts
/// [breakTimeMinutes] if tracked.
Future<AttendanceStatus> clockOut({
String? notes,
int? breakTimeMinutes,
String? applicationId,
String? shiftId,
});
}

View File

@@ -14,7 +14,7 @@ class ClockOutUseCase implements UseCase<ClockOutArguments, AttendanceStatus> {
return _repository.clockOut(
notes: arguments.notes,
breakTimeMinutes: arguments.breakTimeMinutes,
applicationId: arguments.applicationId,
shiftId: arguments.shiftId,
);
}
}

View File

@@ -177,8 +177,8 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState>
// Build validation context from combined BLoC states.
final ClockInValidationContext validationContext = ClockInValidationContext(
isCheckingIn: true,
shiftStartTime: _tryParseDateTime(shift?.startTime),
shiftEndTime: _tryParseDateTime(shift?.endTime),
shiftStartTime: shift?.startsAt,
shiftEndTime: shift?.endsAt,
hasCoordinates: hasCoordinates,
isLocationVerified: geofenceState.isLocationVerified,
isLocationTimedOut: geofenceState.isLocationTimedOut,
@@ -237,7 +237,7 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState>
ClockOutArguments(
notes: event.notes,
breakTimeMinutes: event.breakTimeMinutes ?? 0,
applicationId: state.attendance.activeApplicationId,
shiftId: state.attendance.activeShiftId,
),
);
emit(state.copyWith(
@@ -299,12 +299,6 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState>
return super.close();
}
/// Safely parses a time string into a [DateTime], returning `null` on failure.
static DateTime? _tryParseDateTime(String? value) {
if (value == null || value.isEmpty) return null;
return DateTime.tryParse(value);
}
/// Computes time-window check-in/check-out flags for the given [shift].
///
/// Uses [TimeWindowValidator] so this business logic stays out of widgets.
@@ -314,37 +308,33 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState>
}
const TimeWindowValidator validator = TimeWindowValidator();
final DateTime? shiftStart = _tryParseDateTime(shift.startTime);
final DateTime? shiftEnd = _tryParseDateTime(shift.endTime);
final DateTime shiftStart = shift.startsAt;
final DateTime shiftEnd = shift.endsAt;
// Check-in window.
bool isCheckInAllowed = true;
String? checkInAvailabilityTime;
if (shiftStart != null) {
final ClockInValidationContext checkInCtx = ClockInValidationContext(
isCheckingIn: true,
shiftStartTime: shiftStart,
);
isCheckInAllowed = validator.validate(checkInCtx).isValid;
if (!isCheckInAllowed) {
checkInAvailabilityTime =
TimeWindowValidator.getAvailabilityTime(shiftStart);
}
final ClockInValidationContext checkInCtx = ClockInValidationContext(
isCheckingIn: true,
shiftStartTime: shiftStart,
);
isCheckInAllowed = validator.validate(checkInCtx).isValid;
if (!isCheckInAllowed) {
checkInAvailabilityTime =
TimeWindowValidator.getAvailabilityTime(shiftStart);
}
// Check-out window.
bool isCheckOutAllowed = true;
String? checkOutAvailabilityTime;
if (shiftEnd != null) {
final ClockInValidationContext checkOutCtx = ClockInValidationContext(
isCheckingIn: false,
shiftEndTime: shiftEnd,
);
isCheckOutAllowed = validator.validate(checkOutCtx).isValid;
if (!isCheckOutAllowed) {
checkOutAvailabilityTime =
TimeWindowValidator.getAvailabilityTime(shiftEnd);
}
final ClockInValidationContext checkOutCtx = ClockInValidationContext(
isCheckingIn: false,
shiftEndTime: shiftEnd,
);
isCheckOutAllowed = validator.validate(checkOutCtx).isValid;
if (!isCheckOutAllowed) {
checkOutAvailabilityTime =
TimeWindowValidator.getAvailabilityTime(shiftEnd);
}
return _TimeWindowFlags(

View File

@@ -14,7 +14,9 @@ class ClockInState extends Equatable {
this.status = ClockInStatus.initial,
this.todayShifts = const <Shift>[],
this.selectedShift,
this.attendance = const AttendanceStatus(),
this.attendance = const AttendanceStatus(
attendanceStatus: AttendanceStatusType.notClockedIn,
),
required this.selectedDate,
this.checkInMode = 'swipe',
this.errorMessage,

View File

@@ -31,7 +31,7 @@ class ClockInActionSection extends StatelessWidget {
const ClockInActionSection({
required this.selectedShift,
required this.isCheckedIn,
required this.checkOutTime,
required this.hasCompletedShift,
required this.checkInMode,
required this.isActionInProgress,
this.hasClockinError = false,
@@ -55,8 +55,8 @@ class ClockInActionSection extends StatelessWidget {
/// Whether the user is currently checked in for the active shift.
final bool isCheckedIn;
/// The check-out time, or null if the user has not checked out.
final DateTime? checkOutTime;
/// Whether the shift has been completed (clocked out).
final bool hasCompletedShift;
/// The current check-in mode (e.g. "swipe" or "nfc").
final String checkInMode;
@@ -87,15 +87,15 @@ class ClockInActionSection extends StatelessWidget {
@override
Widget build(BuildContext context) {
if (selectedShift != null && checkOutTime == null) {
return _buildActiveShiftAction(context);
if (selectedShift == null) {
return const NoShiftsBanner();
}
if (selectedShift != null && checkOutTime != null) {
if (hasCompletedShift) {
return const ShiftCompletedBanner();
}
return const NoShiftsBanner();
return _buildActiveShiftAction(context);
}
/// Builds the action widget for an active (not completed) shift.

View File

@@ -66,12 +66,16 @@ class _ClockInBodyState extends State<ClockInBody> {
final String? activeShiftId = state.attendance.activeShiftId;
final bool isActiveSelected =
selectedShift != null && selectedShift.id == activeShiftId;
final DateTime? checkInTime =
isActiveSelected ? state.attendance.checkInTime : null;
final DateTime? checkOutTime =
isActiveSelected ? state.attendance.checkOutTime : null;
final bool isCheckedIn =
state.attendance.isCheckedIn && isActiveSelected;
final DateTime? clockInAt =
isActiveSelected ? state.attendance.clockInAt : null;
final bool isClockedIn =
state.attendance.isClockedIn && isActiveSelected;
// The V2 AttendanceStatus no longer carries checkOutTime.
// A closed session means the worker already clocked out for
// this shift, which the UI shows via ShiftCompletedBanner.
final bool hasCompletedShift = isActiveSelected &&
state.attendance.attendanceStatus ==
AttendanceStatusType.closed;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -106,8 +110,8 @@ class _ClockInBodyState extends State<ClockInBody> {
// action section (check-in/out buttons)
ClockInActionSection(
selectedShift: selectedShift,
isCheckedIn: isCheckedIn,
checkOutTime: checkOutTime,
isCheckedIn: isClockedIn,
hasCompletedShift: hasCompletedShift,
checkInMode: state.checkInMode,
isActionInProgress:
state.status == ClockInStatus.actionInProgress,
@@ -119,9 +123,9 @@ class _ClockInBodyState extends State<ClockInBody> {
),
// checked-in banner (only when checked in to the selected shift)
if (isCheckedIn && checkInTime != null) ...<Widget>[
if (isClockedIn && clockInAt != null) ...<Widget>[
const SizedBox(height: UiConstants.space3),
CheckedInBanner(checkInTime: checkInTime),
CheckedInBanner(checkInTime: clockInAt),
],
const SizedBox(height: UiConstants.space4),
],

View File

@@ -67,21 +67,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
// For demo purposes, check if we're within 24 hours of shift
final DateTime now = DateTime.now();
DateTime shiftStart;
try {
// Try parsing startTime as full datetime first
shiftStart = DateTime.parse(widget.shift!.startTime);
} catch (_) {
try {
// Try parsing date as full datetime
shiftStart = DateTime.parse(widget.shift!.date);
} catch (_) {
// Fall back to combining date and time
shiftStart = DateTime.parse(
'${widget.shift!.date} ${widget.shift!.startTime}',
);
}
}
final DateTime shiftStart = widget.shift!.startsAt;
final int hoursUntilShift = shiftStart.difference(now).inHours;
final bool inCommuteWindow = hoursUntilShift <= 24 && hoursUntilShift >= 0;
@@ -112,21 +98,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
int _getMinutesUntilShift() {
if (widget.shift == null) return 0;
final DateTime now = DateTime.now();
DateTime shiftStart;
try {
// Try parsing startTime as full datetime first
shiftStart = DateTime.parse(widget.shift!.startTime);
} catch (_) {
try {
// Try parsing date as full datetime
shiftStart = DateTime.parse(widget.shift!.date);
} catch (_) {
// Fall back to combining date and time
shiftStart = DateTime.parse(
'${widget.shift!.date} ${widget.shift!.startTime}',
);
}
}
final DateTime shiftStart = widget.shift!.startsAt;
return shiftStart.difference(now).inMinutes;
}

View File

@@ -1,13 +1,12 @@
import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:krow_domain/krow_domain.dart';
import 'package:krow_core/core.dart' show formatTime;
/// A selectable card that displays a single shift's summary information.
///
/// Shows the shift title, client/location, time range, and hourly rate.
/// Shows the shift title, location, and time range.
/// Highlights with a primary border when [isSelected] is true.
class ShiftCard extends StatelessWidget {
/// Creates a shift card for the given [shift].
@@ -50,7 +49,7 @@ class ShiftCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Expanded(child: _ShiftDetails(shift: shift, isSelected: isSelected, i18n: i18n)),
_ShiftTimeAndRate(shift: shift),
_ShiftTimeRange(shift: shift),
],
),
),
@@ -58,7 +57,7 @@ class ShiftCard extends StatelessWidget {
}
}
/// Displays the shift title, client name, and location on the left side.
/// Displays the shift title and location on the left side.
class _ShiftDetails extends StatelessWidget {
const _ShiftDetails({
required this.shift,
@@ -88,8 +87,10 @@ class _ShiftDetails extends StatelessWidget {
),
const SizedBox(height: 2),
Text(shift.title, style: UiTypography.body2b),
// TODO: Ask BE to add clientName to the listTodayShifts response.
// Currently showing locationName as subtitle fallback.
Text(
'${shift.clientName} ${shift.location}',
shift.locationName ?? '',
style: UiTypography.body3r.textSecondary,
),
],
@@ -97,30 +98,26 @@ class _ShiftDetails extends StatelessWidget {
}
}
/// Displays the shift time range and hourly rate on the right side.
class _ShiftTimeAndRate extends StatelessWidget {
const _ShiftTimeAndRate({required this.shift});
/// Displays the shift time range on the right side.
class _ShiftTimeRange extends StatelessWidget {
const _ShiftTimeRange({required this.shift});
/// The shift whose time and rate to display.
/// The shift whose time to display.
final Shift shift;
@override
Widget build(BuildContext context) {
final TranslationsStaffClockInEn i18n = Translations.of(
context,
).staff.clock_in;
final String startFormatted = DateFormat('h:mm a').format(shift.startsAt);
final String endFormatted = DateFormat('h:mm a').format(shift.endsAt);
return Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
Text(
'${formatTime(shift.startTime)} - ${formatTime(shift.endTime)}',
'$startFormatted - $endFormatted',
style: UiTypography.body3m.textSecondary,
),
Text(
i18n.per_hr(amount: shift.hourlyRate),
style: UiTypography.body3m.copyWith(color: UiColors.primary),
),
// TODO: Ask BE to add hourlyRate to the listTodayShifts response.
],
);
}

View File

@@ -1,6 +1,7 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import 'data/repositories_impl/clock_in_repository_impl.dart';
import 'data/services/background_geofence_service.dart';
@@ -30,8 +31,10 @@ class StaffClockInModule extends Module {
@override
void binds(Injector i) {
// Repositories
i.add<ClockInRepositoryInterface>(ClockInRepositoryImpl.new);
// Repositories (V2 API via BaseApiService from CoreModule)
i.add<ClockInRepositoryInterface>(
() => ClockInRepositoryImpl(apiService: i.get<BaseApiService>()),
);
// Geofence Services (resolve core singletons from DI)
i.add<GeofenceServiceInterface>(