feat: integrate Google Maps Places autocomplete for hub address validation and remove activity log functionality

This commit is contained in:
Achintha Isuru
2026-02-01 11:57:57 -05:00
parent 0dfe7a4e6d
commit 1bc308d90f
9 changed files with 53 additions and 220 deletions

View File

@@ -129,6 +129,8 @@ class ClockInRepositoryImpl implements ClockInRepositoryInterface {
createdDate: _toDateTime(fullShift.createdAt)?.toIso8601String() ?? '',
status: fullShift.status?.stringValue,
description: fullShift.description,
latitude: fullShift.latitude,
longitude: fullShift.longitude,
);
}
@@ -186,10 +188,4 @@ class ClockInRepositoryImpl implements ClockInRepositoryInterface {
return getAttendanceStatus();
}
@override
Future<List<Map<String, dynamic>>> getActivityLog() async {
// Placeholder as this wasn't main focus and returns raw maps
return <Map<String, dynamic>>[];
}
}

View File

@@ -18,7 +18,4 @@ abstract class ClockInRepositoryInterface {
/// Checks the user out for the currently active shift.
/// Optionally accepts [breakTimeMinutes] if tracked.
Future<AttendanceStatus> clockOut({String? notes, int? breakTimeMinutes});
/// Retrieves a list of recent clock-in/out activities.
Future<List<Map<String, dynamic>>> getActivityLog();
}

View File

@@ -1,14 +0,0 @@
import 'package:krow_core/core.dart';
import '../repositories/clock_in_repository_interface.dart';
/// Use case for retrieving the activity log.
class GetActivityLogUseCase implements NoInputUseCase<List<Map<String, dynamic>>> {
final ClockInRepositoryInterface _repository;
GetActivityLogUseCase(this._repository);
@override
Future<List<Map<String, dynamic>>> call() {
return _repository.getActivityLog();
}
}

View File

@@ -4,7 +4,6 @@ import '../../domain/usecases/get_todays_shift_usecase.dart';
import '../../domain/usecases/get_attendance_status_usecase.dart';
import '../../domain/usecases/clock_in_usecase.dart';
import '../../domain/usecases/clock_out_usecase.dart';
import '../../domain/usecases/get_activity_log_usecase.dart';
import '../../domain/arguments/clock_in_arguments.dart';
import '../../domain/arguments/clock_out_arguments.dart';
import 'clock_in_event.dart';
@@ -15,11 +14,8 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
final GetAttendanceStatusUseCase _getAttendanceStatus;
final ClockInUseCase _clockIn;
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({
@@ -27,12 +23,10 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
required GetAttendanceStatusUseCase getAttendanceStatus,
required ClockInUseCase clockIn,
required ClockOutUseCase clockOut,
required GetActivityLogUseCase getActivityLog,
}) : _getTodaysShift = getTodaysShift,
_getAttendanceStatus = getAttendanceStatus,
_clockIn = clockIn,
_clockOut = clockOut,
_getActivityLog = getActivityLog,
super(ClockInState(selectedDate: DateTime.now())) {
on<ClockInPageLoaded>(_onLoaded);
on<DateSelected>(_onDateSelected);
@@ -54,7 +48,6 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
try {
final shift = await _getTodaysShift();
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
@@ -63,7 +56,6 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
status: ClockInStatus.success,
todayShift: shift,
attendance: status,
activityLog: activity,
));
if (shift != null && !status.isCheckedIn) {
@@ -103,13 +95,23 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
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;
double distance = 0;
bool isVerified = false; // Require location match by default if shift has location
if (state.todayShift != null && state.todayShift!.latitude != null && state.todayShift!.longitude != null) {
distance = Geolocator.distanceBetween(
position.latitude,
position.longitude,
state.todayShift!.latitude!,
state.todayShift!.longitude!,
);
isVerified = distance <= allowedRadiusMeters;
} else {
// If no shift location, assume verified or don't restrict?
// For strict clock-in, maybe false? but let's default to verified to avoid blocking if data missing
isVerified = true;
}
if (!isClosed) {
add(LocationUpdated(position: position, distance: distance, isVerified: isVerified));

View File

@@ -9,7 +9,6 @@ class ClockInState extends Equatable {
final ClockInStatus status;
final Shift? todayShift;
final AttendanceStatus attendance;
final List<Map<String, dynamic>> activityLog;
final DateTime selectedDate;
final String checkInMode;
final String? errorMessage;
@@ -25,7 +24,6 @@ class ClockInState extends Equatable {
this.status = ClockInStatus.initial,
this.todayShift,
this.attendance = const AttendanceStatus(),
this.activityLog = const [],
required this.selectedDate,
this.checkInMode = 'swipe',
this.errorMessage,
@@ -41,7 +39,6 @@ class ClockInState extends Equatable {
ClockInStatus? status,
Shift? todayShift,
AttendanceStatus? attendance,
List<Map<String, dynamic>>? activityLog,
DateTime? selectedDate,
String? checkInMode,
String? errorMessage,
@@ -56,7 +53,6 @@ class ClockInState extends Equatable {
status: status ?? this.status,
todayShift: todayShift ?? this.todayShift,
attendance: attendance ?? this.attendance,
activityLog: activityLog ?? this.activityLog,
selectedDate: selectedDate ?? this.selectedDate,
checkInMode: checkInMode ?? this.checkInMode,
errorMessage: errorMessage,
@@ -74,7 +70,6 @@ class ClockInState extends Equatable {
status,
todayShift,
attendance,
activityLog,
selectedDate,
checkInMode,
errorMessage,

View File

@@ -3,13 +3,13 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:intl/intl.dart';
import 'package:krow_domain/krow_domain.dart';
import 'package:lucide_icons/lucide_icons.dart';
import '../bloc/clock_in_bloc.dart';
import '../bloc/clock_in_event.dart';
import '../bloc/clock_in_state.dart';
import '../theme/app_colors.dart';
import '../widgets/attendance_card.dart';
import '../widgets/commute_tracker.dart';
import '../widgets/date_selector.dart';
import '../widgets/lunch_break_modal.dart';
@@ -135,13 +135,6 @@ class _ClockInPageState extends State<ClockInPage> {
border: Border.all(
color: const Color(0xFFE2E8F0),
), // slate-200
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 2,
offset: const Offset(0, 1),
),
],
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -417,78 +410,7 @@ class _ClockInPageState extends State<ClockInPage> {
const SizedBox(height: 16),
// Recent Activity List
if (state.activityLog.isNotEmpty)
...state.activityLog.map(
(activity) => Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFF8F9FA),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: const Color(0xFFF1F5F9),
), // slate-100
),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: AppColors.krowBlue.withOpacity(
0.1,
),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
LucideIcons.mapPin,
color: AppColors.krowBlue,
size: 20,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
DateFormat('MMM d').format(
activity['date'] as DateTime,
),
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Color(
0xFF0F172A,
), // slate-900
),
),
Text(
"${activity['start']} - ${activity['end']}",
style: const TextStyle(
fontSize: 12,
color: Color(
0xFF64748B,
), // slate-500
),
),
],
),
),
Text(
activity['hours'] as String,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.krowBlue,
),
),
],
),
),
),
// Recent Activity List (Temporarily removed)
const SizedBox(height: 16),
],
),
@@ -552,87 +474,6 @@ class _ClockInPageState extends State<ClockInPage> {
);
}
Widget _buildHeader() {
return Padding(
padding: const EdgeInsets.fromLTRB(20, 24, 20, 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: AppColors.krowBlue.withOpacity(0.2),
width: 2,
),
),
child: CircleAvatar(
backgroundColor: AppColors.krowBlue.withOpacity(0.1),
child: const Text(
'K',
style: TextStyle(
color: AppColors.krowBlue,
fontWeight: FontWeight.bold,
),
),
),
),
const SizedBox(width: 12),
const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Good Morning',
style: TextStyle(color: AppColors.krowMuted, fontSize: 12),
),
Text(
'Krower',
style: TextStyle(
color: AppColors.krowCharcoal,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
Text(
'Warehouse Assistant',
style: TextStyle(color: AppColors.krowMuted, fontSize: 12),
),
],
),
],
),
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(
20,
), // Rounded full for this page per design
border: Border.all(color: Colors.grey.shade100),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 2,
offset: const Offset(0, 1),
),
],
),
child: const Icon(
LucideIcons.bell,
color: AppColors.krowMuted,
size: 20,
),
),
],
),
);
}
Future<void> _showNFCDialog(BuildContext context) async {
bool scanned = false;
@@ -739,23 +580,32 @@ class _ClockInPageState extends State<ClockInPage> {
// --- Helper Methods ---
String _formatTime(String timeStr) {
// Expecting HH:mm or HH:mm:ss
if (timeStr.isEmpty) return '';
try {
if (timeStr.isEmpty) return '';
final parts = timeStr.split(':');
final dt = DateTime(2022, 1, 1, int.parse(parts[0]), int.parse(parts[1]));
// Try parsing as ISO string first (which contains date)
final dt = DateTime.parse(timeStr);
return DateFormat('h:mm a').format(dt);
} catch (e) {
return timeStr;
} catch (_) {
// Fallback for strict "HH:mm" or "HH:mm:ss" strings
try {
final parts = timeStr.split(':');
if (parts.length >= 2) {
final dt = DateTime(2022, 1, 1, int.parse(parts[0]), int.parse(parts[1]));
return DateFormat('h:mm a').format(dt);
}
return timeStr;
} catch (e) {
return timeStr;
}
}
}
bool _isCheckInAllowed(dynamic shift) {
bool _isCheckInAllowed(Shift 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 shiftStart = DateTime.parse(shift.startTime);
final windowStart = shiftStart.subtract(const Duration(minutes: 15));
return DateTime.now().isAfter(windowStart);
} catch (e) {
@@ -764,10 +614,10 @@ class _ClockInPageState extends State<ClockInPage> {
}
}
String _getCheckInAvailabilityTime(dynamic shift) {
String _getCheckInAvailabilityTime(Shift shift) {
if (shift == null) return '';
try {
final shiftStart = DateTime.parse(shift.date);
final shiftStart = DateTime.parse(shift.endTime);
final windowStart = shiftStart.subtract(const Duration(minutes: 15));
return DateFormat('h:mm a').format(windowStart);
} catch (e) {

View File

@@ -66,9 +66,14 @@ class _CommuteTrackerState extends State<CommuteTracker> {
// For demo purposes, check if we're within 24 hours of shift
final now = DateTime.now();
final shiftStart = DateTime.parse(
'${widget.shift!.date} ${widget.shift!.startTime}',
);
DateTime shiftStart;
try {
shiftStart = DateTime.parse(widget.shift!.startTime);
} catch (_) {
shiftStart = DateTime.parse(
'${widget.shift!.date} ${widget.shift!.startTime}',
);
}
final hoursUntilShift = shiftStart.difference(now).inHours;
final inCommuteWindow = hoursUntilShift <= 24 && hoursUntilShift >= 0;

View File

@@ -6,7 +6,6 @@ import 'data/repositories_impl/clock_in_repository_impl.dart';
import 'domain/repositories/clock_in_repository_interface.dart';
import 'domain/usecases/clock_in_usecase.dart';
import 'domain/usecases/clock_out_usecase.dart';
import 'domain/usecases/get_activity_log_usecase.dart';
import 'domain/usecases/get_attendance_status_usecase.dart';
import 'domain/usecases/get_todays_shift_usecase.dart';
import 'presentation/bloc/clock_in_bloc.dart';
@@ -28,7 +27,6 @@ class StaffClockInModule extends Module {
i.add<GetAttendanceStatusUseCase>(GetAttendanceStatusUseCase.new);
i.add<ClockInUseCase>(ClockInUseCase.new);
i.add<ClockOutUseCase>(ClockOutUseCase.new);
i.add<GetActivityLogUseCase>(GetActivityLogUseCase.new);
// BLoC
i.add<ClockInBloc>(ClockInBloc.new);

View File

@@ -2,7 +2,11 @@ import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_domain/krow_domain.dart';
extension ShiftsNavigator on IModularNavigator {
void navigateToShiftsHome() {
navigate('/worker-main/shifts/');
}
void pushShiftDetails(Shift shift) {
pushNamed('/worker-main/shift-details/${shift.id}', arguments: shift);
navigate('/worker-main/shift-details/${shift.id}', arguments: shift);
}
}