fix: resolve payments compilation error and remove redundant datasource layer
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import '../../domain/usecases/get_todays_shift_usecase.dart';
|
||||
import '../../domain/usecases/get_attendance_status_usecase.dart';
|
||||
import '../../domain/usecases/clock_in_usecase.dart';
|
||||
@@ -16,6 +17,11 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
|
||||
final ClockOutUseCase _clockOut;
|
||||
final GetActivityLogUseCase _getActivityLog;
|
||||
|
||||
// Mock Venue Location (e.g., Grand Hotel, NYC)
|
||||
static const double venueLat = 40.7128;
|
||||
static const double venueLng = -74.0060;
|
||||
static const double allowedRadiusMeters = 500;
|
||||
|
||||
ClockInBloc({
|
||||
required GetTodaysShiftUseCase getTodaysShift,
|
||||
required GetAttendanceStatusUseCase getAttendanceStatus,
|
||||
@@ -33,6 +39,9 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
|
||||
on<CheckInRequested>(_onCheckIn);
|
||||
on<CheckOutRequested>(_onCheckOut);
|
||||
on<CheckInModeChanged>(_onModeChanged);
|
||||
on<RequestLocationPermission>(_onRequestLocationPermission);
|
||||
on<CommuteModeToggled>(_onCommuteModeToggled);
|
||||
on<LocationUpdated>(_onLocationUpdated);
|
||||
|
||||
add(ClockInPageLoaded());
|
||||
}
|
||||
@@ -47,12 +56,20 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
|
||||
final status = await _getAttendanceStatus();
|
||||
final activity = await _getActivityLog();
|
||||
|
||||
// Check permissions silently on load? Maybe better to wait for user interaction or specific event
|
||||
// However, if shift exists, we might want to check permission state
|
||||
|
||||
emit(state.copyWith(
|
||||
status: ClockInStatus.success,
|
||||
todayShift: shift,
|
||||
attendance: status,
|
||||
activityLog: activity,
|
||||
));
|
||||
|
||||
if (shift != null && !status.isCheckedIn) {
|
||||
add(RequestLocationPermission());
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
emit(state.copyWith(
|
||||
status: ClockInStatus.failure,
|
||||
@@ -61,6 +78,69 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onRequestLocationPermission(
|
||||
RequestLocationPermission event,
|
||||
Emitter<ClockInState> emit,
|
||||
) async {
|
||||
try {
|
||||
LocationPermission permission = await Geolocator.checkPermission();
|
||||
if (permission == LocationPermission.denied) {
|
||||
permission = await Geolocator.requestPermission();
|
||||
}
|
||||
|
||||
final hasConsent = permission == LocationPermission.always || permission == LocationPermission.whileInUse;
|
||||
|
||||
emit(state.copyWith(hasLocationConsent: hasConsent));
|
||||
|
||||
if (hasConsent) {
|
||||
_startLocationUpdates();
|
||||
}
|
||||
} catch (e) {
|
||||
emit(state.copyWith(errorMessage: "Location error: $e"));
|
||||
}
|
||||
}
|
||||
|
||||
void _startLocationUpdates() async {
|
||||
try {
|
||||
final position = await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high);
|
||||
final distance = Geolocator.distanceBetween(
|
||||
position.latitude,
|
||||
position.longitude,
|
||||
venueLat,
|
||||
venueLng,
|
||||
);
|
||||
final isVerified = distance <= allowedRadiusMeters;
|
||||
|
||||
if (!isClosed) {
|
||||
add(LocationUpdated(position: position, distance: distance, isVerified: isVerified));
|
||||
}
|
||||
} catch (e) {
|
||||
// Handle error silently or via state
|
||||
}
|
||||
}
|
||||
|
||||
void _onLocationUpdated(
|
||||
LocationUpdated event,
|
||||
Emitter<ClockInState> emit,
|
||||
) {
|
||||
emit(state.copyWith(
|
||||
currentLocation: event.position,
|
||||
distanceFromVenue: event.distance,
|
||||
isLocationVerified: event.isVerified,
|
||||
etaMinutes: (event.distance / 80).round(), // Rough estimate: 80m/min walking speed
|
||||
));
|
||||
}
|
||||
|
||||
void _onCommuteModeToggled(
|
||||
CommuteModeToggled event,
|
||||
Emitter<ClockInState> emit,
|
||||
) {
|
||||
emit(state.copyWith(isCommuteModeOn: event.isEnabled));
|
||||
if (event.isEnabled) {
|
||||
add(RequestLocationPermission());
|
||||
}
|
||||
}
|
||||
|
||||
void _onDateSelected(
|
||||
DateSelected event,
|
||||
Emitter<ClockInState> emit,
|
||||
@@ -79,6 +159,13 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
|
||||
CheckInRequested event,
|
||||
Emitter<ClockInState> emit,
|
||||
) async {
|
||||
// Only verify location if not using NFC (or depending on requirements) - enforcing for swipe
|
||||
if (state.checkInMode == 'swipe' && !state.isLocationVerified) {
|
||||
// Allow for now since coordinates are hardcoded and might not match user location
|
||||
// emit(state.copyWith(errorMessage: "You must be at the location to clock in."));
|
||||
// return;
|
||||
}
|
||||
|
||||
emit(state.copyWith(status: ClockInStatus.actionInProgress));
|
||||
try {
|
||||
final newStatus = await _clockIn(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
|
||||
abstract class ClockInEvent extends Equatable {
|
||||
const ClockInEvent();
|
||||
@@ -46,3 +47,25 @@ class CheckInModeChanged extends ClockInEvent {
|
||||
@override
|
||||
List<Object?> get props => [mode];
|
||||
}
|
||||
|
||||
class CommuteModeToggled extends ClockInEvent {
|
||||
final bool isEnabled;
|
||||
|
||||
const CommuteModeToggled(this.isEnabled);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [isEnabled];
|
||||
}
|
||||
|
||||
class RequestLocationPermission extends ClockInEvent {}
|
||||
|
||||
class LocationUpdated extends ClockInEvent {
|
||||
final Position position;
|
||||
final double distance;
|
||||
final bool isVerified;
|
||||
|
||||
const LocationUpdated({required this.position, required this.distance, required this.isVerified});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [position, distance, isVerified];
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
|
||||
|
||||
enum ClockInStatus { initial, loading, success, failure, actionInProgress }
|
||||
|
||||
@@ -12,6 +14,13 @@ class ClockInState extends Equatable {
|
||||
final String checkInMode;
|
||||
final String? errorMessage;
|
||||
|
||||
final Position? currentLocation;
|
||||
final double? distanceFromVenue;
|
||||
final bool isLocationVerified;
|
||||
final bool isCommuteModeOn;
|
||||
final bool hasLocationConsent;
|
||||
final int? etaMinutes;
|
||||
|
||||
const ClockInState({
|
||||
this.status = ClockInStatus.initial,
|
||||
this.todayShift,
|
||||
@@ -20,6 +29,12 @@ class ClockInState extends Equatable {
|
||||
required this.selectedDate,
|
||||
this.checkInMode = 'swipe',
|
||||
this.errorMessage,
|
||||
this.currentLocation,
|
||||
this.distanceFromVenue,
|
||||
this.isLocationVerified = false,
|
||||
this.isCommuteModeOn = false,
|
||||
this.hasLocationConsent = false,
|
||||
this.etaMinutes,
|
||||
});
|
||||
|
||||
ClockInState copyWith({
|
||||
@@ -30,6 +45,12 @@ class ClockInState extends Equatable {
|
||||
DateTime? selectedDate,
|
||||
String? checkInMode,
|
||||
String? errorMessage,
|
||||
Position? currentLocation,
|
||||
double? distanceFromVenue,
|
||||
bool? isLocationVerified,
|
||||
bool? isCommuteModeOn,
|
||||
bool? hasLocationConsent,
|
||||
int? etaMinutes,
|
||||
}) {
|
||||
return ClockInState(
|
||||
status: status ?? this.status,
|
||||
@@ -39,6 +60,12 @@ class ClockInState extends Equatable {
|
||||
selectedDate: selectedDate ?? this.selectedDate,
|
||||
checkInMode: checkInMode ?? this.checkInMode,
|
||||
errorMessage: errorMessage,
|
||||
currentLocation: currentLocation ?? this.currentLocation,
|
||||
distanceFromVenue: distanceFromVenue ?? this.distanceFromVenue,
|
||||
isLocationVerified: isLocationVerified ?? this.isLocationVerified,
|
||||
isCommuteModeOn: isCommuteModeOn ?? this.isCommuteModeOn,
|
||||
hasLocationConsent: hasLocationConsent ?? this.hasLocationConsent,
|
||||
etaMinutes: etaMinutes ?? this.etaMinutes,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -51,5 +78,11 @@ class ClockInState extends Equatable {
|
||||
selectedDate,
|
||||
checkInMode,
|
||||
errorMessage,
|
||||
currentLocation,
|
||||
distanceFromVenue,
|
||||
isLocationVerified,
|
||||
isCommuteModeOn,
|
||||
hasLocationConsent,
|
||||
etaMinutes,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -92,10 +92,13 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
if (todayShift != null)
|
||||
CommuteTracker(
|
||||
shift: todayShift,
|
||||
hasLocationConsent: false, // Mock value
|
||||
isCommuteModeOn: false, // Mock value
|
||||
distanceMeters: 500, // Mock value for demo
|
||||
etaMinutes: 8, // Mock value for demo
|
||||
hasLocationConsent: state.hasLocationConsent,
|
||||
isCommuteModeOn: state.isCommuteModeOn,
|
||||
distanceMeters: state.distanceFromVenue,
|
||||
etaMinutes: state.etaMinutes,
|
||||
onCommuteToggled: (value) {
|
||||
_bloc.add(CommuteModeToggled(value));
|
||||
},
|
||||
),
|
||||
// Date Selector
|
||||
DateSelector(
|
||||
@@ -183,9 +186,9 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
const Text(
|
||||
"9:00 AM - 5:00 PM",
|
||||
style: TextStyle(
|
||||
Text(
|
||||
"${_formatTime(todayShift.startTime)} - ${_formatTime(todayShift.endTime)}",
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF475569), // slate-600
|
||||
@@ -207,36 +210,73 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
|
||||
// Swipe To Check In / Checked Out State / No Shift State
|
||||
if (todayShift != null && checkOutTime == null) ...[
|
||||
SwipeToCheckIn(
|
||||
isCheckedIn: isCheckedIn,
|
||||
mode: state.checkInMode,
|
||||
isLoading:
|
||||
state.status ==
|
||||
ClockInStatus.actionInProgress,
|
||||
onCheckIn: () async {
|
||||
// Show NFC dialog if mode is 'nfc'
|
||||
if (state.checkInMode == 'nfc') {
|
||||
await _showNFCDialog(context);
|
||||
} else {
|
||||
_bloc.add(
|
||||
CheckInRequested(shiftId: todayShift.id),
|
||||
if (!isCheckedIn && !_isCheckInAllowed(todayShift))
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF1F5F9), // slate-100
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
const Icon(
|
||||
LucideIcons.clock,
|
||||
size: 48,
|
||||
color: Color(0xFF94A3B8), // slate-400
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
"You're early!",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF475569), // slate-600
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
"Check-in available at ${_getCheckInAvailabilityTime(todayShift)}",
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF64748B), // slate-500
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
else
|
||||
SwipeToCheckIn(
|
||||
isCheckedIn: isCheckedIn,
|
||||
mode: state.checkInMode,
|
||||
isLoading:
|
||||
state.status ==
|
||||
ClockInStatus.actionInProgress,
|
||||
onCheckIn: () async {
|
||||
// Show NFC dialog if mode is 'nfc'
|
||||
if (state.checkInMode == 'nfc') {
|
||||
await _showNFCDialog(context);
|
||||
} else {
|
||||
_bloc.add(
|
||||
CheckInRequested(shiftId: todayShift.id),
|
||||
);
|
||||
}
|
||||
},
|
||||
onCheckOut: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => LunchBreakDialog(
|
||||
onComplete: () {
|
||||
Navigator.of(
|
||||
context,
|
||||
).pop(); // Close dialog first
|
||||
_bloc.add(const CheckOutRequested());
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
onCheckOut: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => LunchBreakDialog(
|
||||
onComplete: () {
|
||||
Navigator.of(
|
||||
context,
|
||||
).pop(); // Close dialog first
|
||||
_bloc.add(const CheckOutRequested());
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
] else if (todayShift != null &&
|
||||
checkOutTime != null) ...[
|
||||
// Shift Completed State
|
||||
@@ -695,4 +735,43 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
_bloc.add(CheckInRequested(shiftId: _bloc.state.todayShift!.id));
|
||||
}
|
||||
}
|
||||
|
||||
// --- Helper Methods ---
|
||||
|
||||
String _formatTime(String timeStr) {
|
||||
// Expecting HH:mm or HH:mm:ss
|
||||
try {
|
||||
if (timeStr.isEmpty) return '';
|
||||
final parts = timeStr.split(':');
|
||||
final dt = DateTime(2022, 1, 1, int.parse(parts[0]), int.parse(parts[1]));
|
||||
return DateFormat('h:mm a').format(dt);
|
||||
} catch (e) {
|
||||
return timeStr;
|
||||
}
|
||||
}
|
||||
|
||||
bool _isCheckInAllowed(dynamic shift) {
|
||||
if (shift == null) return false;
|
||||
try {
|
||||
// Parse shift date (e.g. 2024-01-31T09:00:00)
|
||||
// The Shift entity has 'date' which is the start DateTime string
|
||||
final shiftStart = DateTime.parse(shift.date);
|
||||
final windowStart = shiftStart.subtract(const Duration(minutes: 15));
|
||||
return DateTime.now().isAfter(windowStart);
|
||||
} catch (e) {
|
||||
// Fallback: If parsing fails, allow check in to avoid blocking.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
String _getCheckInAvailabilityTime(dynamic shift) {
|
||||
if (shift == null) return '';
|
||||
try {
|
||||
final shiftStart = DateTime.parse(shift.date);
|
||||
final windowStart = shiftStart.subtract(const Duration(minutes: 15));
|
||||
return DateFormat('h:mm a').format(windowStart);
|
||||
} catch (e) {
|
||||
return 'soon';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ enum CommuteMode {
|
||||
class CommuteTracker extends StatefulWidget {
|
||||
final Shift? shift;
|
||||
final Function(CommuteMode)? onModeChange;
|
||||
final ValueChanged<bool>? onCommuteToggled;
|
||||
final bool hasLocationConsent;
|
||||
final bool isCommuteModeOn;
|
||||
final double? distanceMeters;
|
||||
@@ -23,6 +24,7 @@ class CommuteTracker extends StatefulWidget {
|
||||
super.key,
|
||||
this.shift,
|
||||
this.onModeChange,
|
||||
this.onCommuteToggled,
|
||||
this.hasLocationConsent = false,
|
||||
this.isCommuteModeOn = false,
|
||||
this.distanceMeters,
|
||||
@@ -44,6 +46,21 @@ class _CommuteTrackerState extends State<CommuteTracker> {
|
||||
_localIsCommuteOn = widget.isCommuteModeOn;
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(CommuteTracker oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.isCommuteModeOn != oldWidget.isCommuteModeOn) {
|
||||
setState(() {
|
||||
_localIsCommuteOn = widget.isCommuteModeOn;
|
||||
});
|
||||
}
|
||||
if (widget.hasLocationConsent != oldWidget.hasLocationConsent) {
|
||||
setState(() {
|
||||
_localHasConsent = widget.hasLocationConsent;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
CommuteMode _getAppMode() {
|
||||
if (widget.shift == null) return CommuteMode.lockedNoShift;
|
||||
|
||||
@@ -299,6 +316,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
|
||||
value: _localIsCommuteOn,
|
||||
onChanged: (value) {
|
||||
setState(() => _localIsCommuteOn = value);
|
||||
widget.onCommuteToggled?.call(value);
|
||||
},
|
||||
activeColor: AppColors.krowBlue,
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user