solving bugs in check in

This commit is contained in:
José Salazar
2026-02-02 08:25:13 +09:00
parent b7b7709386
commit 279544930c
13 changed files with 20978 additions and 20841 deletions

View File

@@ -1,16 +1,16 @@
# Basic Usage # Basic Usage
```dart ```dart
ExampleConnector.instance.getShiftRoleById(getShiftRoleByIdVariables).execute(); ExampleConnector.instance.CreateStaff(createStaffVariables).execute();
ExampleConnector.instance.listShiftRolesByShiftId(listShiftRolesByShiftIdVariables).execute(); ExampleConnector.instance.UpdateStaff(updateStaffVariables).execute();
ExampleConnector.instance.listShiftRolesByRoleId(listShiftRolesByRoleIdVariables).execute(); ExampleConnector.instance.DeleteStaff(deleteStaffVariables).execute();
ExampleConnector.instance.listShiftRolesByShiftIdAndTimeRange(listShiftRolesByShiftIdAndTimeRangeVariables).execute(); ExampleConnector.instance.listStaffAvailabilities(listStaffAvailabilitiesVariables).execute();
ExampleConnector.instance.listShiftRolesByVendorId(listShiftRolesByVendorIdVariables).execute(); ExampleConnector.instance.listStaffAvailabilitiesByStaffId(listStaffAvailabilitiesByStaffIdVariables).execute();
ExampleConnector.instance.listShiftRolesByBusinessAndDateRange(listShiftRolesByBusinessAndDateRangeVariables).execute(); ExampleConnector.instance.getStaffAvailabilityByKey(getStaffAvailabilityByKeyVariables).execute();
ExampleConnector.instance.listShiftRolesByBusinessAndOrder(listShiftRolesByBusinessAndOrderVariables).execute(); ExampleConnector.instance.listStaffAvailabilitiesByDay(listStaffAvailabilitiesByDayVariables).execute();
ExampleConnector.instance.listShiftRolesByBusinessDateRangeCompletedOrders(listShiftRolesByBusinessDateRangeCompletedOrdersVariables).execute(); ExampleConnector.instance.createStaffAvailabilityStats(createStaffAvailabilityStatsVariables).execute();
ExampleConnector.instance.listShiftRolesByBusinessAndDatesSummary(listShiftRolesByBusinessAndDatesSummaryVariables).execute(); ExampleConnector.instance.updateStaffAvailabilityStats(updateStaffAvailabilityStatsVariables).execute();
ExampleConnector.instance.getCompletedShiftsByBusinessId(getCompletedShiftsByBusinessIdVariables).execute(); ExampleConnector.instance.deleteStaffAvailabilityStats(deleteStaffAvailabilityStatsVariables).execute();
``` ```
@@ -23,8 +23,8 @@ Optional fields can be discovered based on classes that have `Optional` object t
This is an example of a mutation with an optional field: This is an example of a mutation with an optional field:
```dart ```dart
await ExampleConnector.instance.updateRoleCategory({ ... }) await ExampleConnector.instance.searchInvoiceTemplatesByOwnerAndName({ ... })
.roleName(...) .offset(...)
.execute(); .execute();
``` ```

View File

@@ -144,6 +144,8 @@ class GetApplicationsByStaffIdApplicationsShift {
final EnumValue<ShiftStatus>? status; final EnumValue<ShiftStatus>? status;
final int? durationDays; final int? durationDays;
final String? description; final String? description;
final double? latitude;
final double? longitude;
final GetApplicationsByStaffIdApplicationsShiftOrder order; final GetApplicationsByStaffIdApplicationsShiftOrder order;
GetApplicationsByStaffIdApplicationsShift.fromJson(dynamic json): GetApplicationsByStaffIdApplicationsShift.fromJson(dynamic json):
@@ -156,6 +158,8 @@ class GetApplicationsByStaffIdApplicationsShift {
status = json['status'] == null ? null : shiftStatusDeserializer(json['status']), status = json['status'] == null ? null : shiftStatusDeserializer(json['status']),
durationDays = json['durationDays'] == null ? null : nativeFromJson<int>(json['durationDays']), durationDays = json['durationDays'] == null ? null : nativeFromJson<int>(json['durationDays']),
description = json['description'] == null ? null : nativeFromJson<String>(json['description']), description = json['description'] == null ? null : nativeFromJson<String>(json['description']),
latitude = json['latitude'] == null ? null : nativeFromJson<double>(json['latitude']),
longitude = json['longitude'] == null ? null : nativeFromJson<double>(json['longitude']),
order = GetApplicationsByStaffIdApplicationsShiftOrder.fromJson(json['order']); order = GetApplicationsByStaffIdApplicationsShiftOrder.fromJson(json['order']);
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
@@ -176,11 +180,13 @@ class GetApplicationsByStaffIdApplicationsShift {
status == otherTyped.status && status == otherTyped.status &&
durationDays == otherTyped.durationDays && durationDays == otherTyped.durationDays &&
description == otherTyped.description && description == otherTyped.description &&
latitude == otherTyped.latitude &&
longitude == otherTyped.longitude &&
order == otherTyped.order; order == otherTyped.order;
} }
@override @override
int get hashCode => Object.hashAll([id.hashCode, title.hashCode, date.hashCode, startTime.hashCode, endTime.hashCode, location.hashCode, status.hashCode, durationDays.hashCode, description.hashCode, order.hashCode]); int get hashCode => Object.hashAll([id.hashCode, title.hashCode, date.hashCode, startTime.hashCode, endTime.hashCode, location.hashCode, status.hashCode, durationDays.hashCode, description.hashCode, latitude.hashCode, longitude.hashCode, order.hashCode]);
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
@@ -210,6 +216,12 @@ class GetApplicationsByStaffIdApplicationsShift {
if (description != null) { if (description != null) {
json['description'] = nativeToJson<String?>(description); json['description'] = nativeToJson<String?>(description);
} }
if (latitude != null) {
json['latitude'] = nativeToJson<double?>(latitude);
}
if (longitude != null) {
json['longitude'] = nativeToJson<double?>(longitude);
}
json['order'] = order.toJson(); json['order'] = order.toJson();
return json; return json;
} }
@@ -224,6 +236,8 @@ class GetApplicationsByStaffIdApplicationsShift {
this.status, this.status,
this.durationDays, this.durationDays,
this.description, this.description,
this.latitude,
this.longitude,
required this.order, required this.order,
}); });
} }

View File

@@ -1,31 +1,25 @@
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';
import 'package:krow_core/core.dart'; import 'package:krow_core/core.dart';
import 'package:krow_data_connect/src/session/staff_session_store.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 implements ClockInRepositoryInterface { class ClockInRepositoryImpl implements ClockInRepositoryInterface {
final dc.ExampleConnector _dataConnect; final dc.ExampleConnector _dataConnect;
final firebase.FirebaseAuth _firebaseAuth;
ClockInRepositoryImpl({ ClockInRepositoryImpl({
required dc.ExampleConnector dataConnect, required dc.ExampleConnector dataConnect,
required firebase.FirebaseAuth firebaseAuth, }) : _dataConnect = dataConnect;
}) : _dataConnect = dataConnect,
_firebaseAuth = firebaseAuth;
Future<String> _getStaffId() async { Future<String> _getStaffId() async {
final firebase.User? user = _firebaseAuth.currentUser; final StaffSession? session = StaffSessionStore.instance.session;
if (user == null) throw Exception('User not authenticated'); final String? staffId = session?.staff?.id;
if (staffId != null && staffId.isNotEmpty) {
final QueryResult<dc.GetStaffByUserIdData, dc.GetStaffByUserIdVariables> result = return staffId;
await _dataConnect.getStaffByUserId(userId: user.uid).execute();
if (result.data.staffs.isEmpty) {
throw Exception('Staff profile not found');
} }
return result.data.staffs.first.id; throw Exception('Staff session not found');
} }
/// Helper to convert Data Connect Timestamp to DateTime /// Helper to convert Data Connect Timestamp to DateTime
@@ -68,81 +62,131 @@ class ClockInRepositoryImpl implements ClockInRepositoryInterface {
return Timestamp.fromJson(d.toUtc().toIso8601String()); return Timestamp.fromJson(d.toUtc().toIso8601String());
} }
/// Helper to find today's active application ({Timestamp start, Timestamp end}) _utcDayRange(DateTime localDay) {
Future<dc.GetApplicationsByStaffIdApplications?> _getTodaysApplication(String staffId) async { 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: _fromDateTime(dayStartUtc),
end: _fromDateTime(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 DateTime now = DateTime.now();
final range = _utcDayRange(now);
final QueryResult<dc.GetApplicationsByStaffIdData, dc.GetApplicationsByStaffIdVariables>
result = await _dataConnect
.getApplicationsByStaffId(staffId: staffId)
.dayStart(range.start)
.dayEnd(range.end)
.execute();
// Fetch recent applications (assuming meaningful limit) final apps = result.data.applications;
final QueryResult<dc.GetApplicationsByStaffIdData, dc.GetApplicationsByStaffIdVariables> result = if (apps.isEmpty) return const [];
await _dataConnect.getApplicationsByStaffId(
staffId: staffId,
).limit(20).execute();
try { apps.sort((a, b) {
return result.data.applications.firstWhere((dc.GetApplicationsByStaffIdApplications app) { final DateTime? aTime =
final DateTime? shiftTime = _toDateTime(app.shift.startTime); _toDateTime(a.shift.startTime) ?? _toDateTime(a.shift.date);
final DateTime? bTime =
if (shiftTime == null) return false; _toDateTime(b.shift.startTime) ?? _toDateTime(b.shift.date);
if (aTime == null && bTime == null) return 0;
final bool isSameDay = shiftTime.year == now.year && if (aTime == null) return -1;
shiftTime.month == now.month && if (bTime == null) return 1;
shiftTime.day == now.day; final Duration aDiff = aTime.difference(now).abs();
final Duration bDiff = bTime.difference(now).abs();
if (!isSameDay) return false; return bDiff.compareTo(aDiff); // closest at the end
// Check Status
final dynamic status = app.status.stringValue;
return status != 'PENDING' && status != 'REJECTED' && status != 'NO_SHOW' && status != 'CANCELED';
}); });
} catch (e) {
return apps;
}
dc.GetApplicationsByStaffIdApplications? _getActiveApplication(
List<dc.GetApplicationsByStaffIdApplications> apps,
) {
try {
return apps.firstWhere((app) {
final status = app.status.stringValue;
return status == 'CHECKED_IN' || status == 'LATE';
});
} catch (_) {
return null; return null;
} }
} }
@override @override
Future<Shift?> getTodaysShift() async { Future<List<Shift>> getTodaysShifts() async {
final String staffId = await _getStaffId(); final String staffId = await _getStaffId();
final dc.GetApplicationsByStaffIdApplications? app = await _getTodaysApplication(staffId); final List<dc.GetApplicationsByStaffIdApplications> apps =
await _getTodaysApplications(staffId);
if (app == null) return null; if (apps.isEmpty) return const [];
final List<Shift> shifts = [];
for (final app in apps) {
final dc.GetApplicationsByStaffIdApplicationsShift shift = app.shift; 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 QueryResult<dc.GetShiftByIdData, dc.GetShiftByIdVariables> shiftResult = final String roleName = app.shiftRole.role.name;
await _dataConnect.getShiftById(id: shift.id).execute(); final String orderName =
(shift.order.eventName ?? '').trim().isNotEmpty
if (shiftResult.data.shift == null) return null; ? shift.order.eventName!
: shift.order.business.businessName;
final dc.GetShiftByIdShift fullShift = shiftResult.data.shift!; final String title = '$roleName - $orderName';
shifts.add(
return Shift( Shift(
id: fullShift.id, id: shift.id,
title: fullShift.title, title: title,
clientName: fullShift.order.business.businessName, clientName: shift.order.business.businessName,
logoUrl: '', // Not available in GetShiftById logoUrl: shift.order.business.companyLogoUrl ?? '',
hourlyRate: 0.0, hourlyRate: app.shiftRole.role.costPerHour,
location: fullShift.location ?? '', location: shift.location ?? '',
locationAddress: fullShift.locationAddress ?? '', locationAddress: shift.order.teamHub.hubName,
date: _toDateTime(fullShift.startTime)?.toIso8601String() ?? '', date: startDt?.toIso8601String() ?? '',
startTime: _toDateTime(fullShift.startTime)?.toIso8601String() ?? '', startTime: startDt?.toIso8601String() ?? '',
endTime: _toDateTime(fullShift.endTime)?.toIso8601String() ?? '', endTime: endDt?.toIso8601String() ?? '',
createdDate: _toDateTime(fullShift.createdAt)?.toIso8601String() ?? '', createdDate: createdDt?.toIso8601String() ?? '',
status: fullShift.status?.stringValue, status: shift.status?.stringValue,
description: fullShift.description, description: shift.description,
latitude: fullShift.latitude, latitude: shift.latitude,
longitude: fullShift.longitude, longitude: shift.longitude,
),
); );
} }
return shifts;
}
@override @override
Future<AttendanceStatus> getAttendanceStatus() async { Future<AttendanceStatus> getAttendanceStatus() async {
final String staffId = await _getStaffId(); final String staffId = await _getStaffId();
final dc.GetApplicationsByStaffIdApplications? app = await _getTodaysApplication(staffId); final List<dc.GetApplicationsByStaffIdApplications> apps =
await _getTodaysApplications(staffId);
if (app == null) { if (apps.isEmpty) {
return const AttendanceStatus(isCheckedIn: false); return const AttendanceStatus(isCheckedIn: false);
} }
final dc.GetApplicationsByStaffIdApplications? activeApp =
_getActiveApplication(apps);
final dc.GetApplicationsByStaffIdApplications app =
activeApp ?? apps.last;
return ClockInAdapter.toAttendanceStatus( return ClockInAdapter.toAttendanceStatus(
status: app.status.stringValue, status: app.status.stringValue,
checkInTime: _toDateTime(app.checkInTime), checkInTime: _toDateTime(app.checkInTime),
@@ -175,7 +219,10 @@ class ClockInRepositoryImpl implements ClockInRepositoryInterface {
Future<AttendanceStatus> clockOut({String? notes, int? breakTimeMinutes}) async { Future<AttendanceStatus> clockOut({String? notes, int? breakTimeMinutes}) async {
final String staffId = await _getStaffId(); final String staffId = await _getStaffId();
final dc.GetApplicationsByStaffIdApplications? app = await _getTodaysApplication(staffId); final List<dc.GetApplicationsByStaffIdApplications> apps =
await _getTodaysApplications(staffId);
final dc.GetApplicationsByStaffIdApplications? app =
_getActiveApplication(apps);
if (app == null) throw Exception('No active shift found to clock out'); if (app == null) throw Exception('No active shift found to clock out');
await _dataConnect.updateApplicationStatus( await _dataConnect.updateApplicationStatus(

View File

@@ -3,9 +3,9 @@ import 'package:krow_domain/krow_domain.dart';
/// Repository interface for Clock In/Out functionality /// Repository interface for Clock In/Out functionality
abstract class ClockInRepositoryInterface { abstract class ClockInRepositoryInterface {
/// Retrieves the shift assigned to the user for the current day. /// Retrieves the shifts assigned to the user for the current day.
/// Returns null if no shift is assigned for today. /// Returns empty list if no shift is assigned for today.
Future<Shift?> getTodaysShift(); Future<List<Shift>> getTodaysShifts();
/// Gets the current attendance status (e.g., checked in or not, times). /// Gets the current attendance status (e.g., checked in or not, times).
/// This helps in restoring the UI state if the app was killed. /// This helps in restoring the UI state if the app was killed.

View File

@@ -2,14 +2,14 @@ import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart'; import 'package:krow_domain/krow_domain.dart';
import '../repositories/clock_in_repository_interface.dart'; import '../repositories/clock_in_repository_interface.dart';
/// Use case for retrieving the user's scheduled shift for today. /// Use case for retrieving the user's scheduled shifts for today.
class GetTodaysShiftUseCase implements NoInputUseCase<Shift?> { class GetTodaysShiftUseCase implements NoInputUseCase<List<Shift>> {
final ClockInRepositoryInterface _repository; final ClockInRepositoryInterface _repository;
GetTodaysShiftUseCase(this._repository); GetTodaysShiftUseCase(this._repository);
@override @override
Future<Shift?> call() { Future<List<Shift>> call() {
return _repository.getTodaysShift(); return _repository.getTodaysShifts();
} }
} }

View File

@@ -1,5 +1,6 @@
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:geolocator/geolocator.dart'; import 'package:geolocator/geolocator.dart';
import 'package:krow_domain/krow_domain.dart';
import '../../domain/usecases/get_todays_shift_usecase.dart'; import '../../domain/usecases/get_todays_shift_usecase.dart';
import '../../domain/usecases/get_attendance_status_usecase.dart'; import '../../domain/usecases/get_attendance_status_usecase.dart';
import '../../domain/usecases/clock_in_usecase.dart'; import '../../domain/usecases/clock_in_usecase.dart';
@@ -29,6 +30,7 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
_clockOut = clockOut, _clockOut = clockOut,
super(ClockInState(selectedDate: DateTime.now())) { super(ClockInState(selectedDate: DateTime.now())) {
on<ClockInPageLoaded>(_onLoaded); on<ClockInPageLoaded>(_onLoaded);
on<ShiftSelected>(_onShiftSelected);
on<DateSelected>(_onDateSelected); on<DateSelected>(_onDateSelected);
on<CheckInRequested>(_onCheckIn); on<CheckInRequested>(_onCheckIn);
on<CheckOutRequested>(_onCheckOut); on<CheckOutRequested>(_onCheckOut);
@@ -46,19 +48,30 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
) async { ) async {
emit(state.copyWith(status: ClockInStatus.loading)); emit(state.copyWith(status: ClockInStatus.loading));
try { try {
final shift = await _getTodaysShift(); final shifts = await _getTodaysShift();
final status = await _getAttendanceStatus(); final status = await _getAttendanceStatus();
// Check permissions silently on load? Maybe better to wait for user interaction or specific event // 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 // However, if shift exists, we might want to check permission state
Shift? selectedShift;
if (shifts.isNotEmpty) {
if (status.activeShiftId != null) {
try {
selectedShift =
shifts.firstWhere((s) => s.id == status.activeShiftId);
} catch (_) {}
}
selectedShift ??= shifts.last;
}
emit(state.copyWith( emit(state.copyWith(
status: ClockInStatus.success, status: ClockInStatus.success,
todayShift: shift, todayShifts: shifts,
selectedShift: selectedShift,
attendance: status, attendance: status,
)); ));
if (shift != null && !status.isCheckedIn) { if (selectedShift != null && !status.isCheckedIn) {
add(RequestLocationPermission()); add(RequestLocationPermission());
} }
@@ -99,12 +112,14 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
double distance = 0; double distance = 0;
bool isVerified = false; // Require location match by default if shift has location bool isVerified = false; // Require location match by default if shift has location
if (state.todayShift != null && state.todayShift!.latitude != null && state.todayShift!.longitude != null) { if (state.selectedShift != null &&
state.selectedShift!.latitude != null &&
state.selectedShift!.longitude != null) {
distance = Geolocator.distanceBetween( distance = Geolocator.distanceBetween(
position.latitude, position.latitude,
position.longitude, position.longitude,
state.todayShift!.latitude!, state.selectedShift!.latitude!,
state.todayShift!.longitude!, state.selectedShift!.longitude!,
); );
isVerified = distance <= allowedRadiusMeters; isVerified = distance <= allowedRadiusMeters;
} else { } else {
@@ -143,6 +158,16 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
} }
} }
void _onShiftSelected(
ShiftSelected event,
Emitter<ClockInState> emit,
) {
emit(state.copyWith(selectedShift: event.shift));
if (!state.attendance.isCheckedIn) {
_startLocationUpdates();
}
}
void _onDateSelected( void _onDateSelected(
DateSelected event, DateSelected event,
Emitter<ClockInState> emit, Emitter<ClockInState> emit,

View File

@@ -1,5 +1,6 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:geolocator/geolocator.dart'; import 'package:geolocator/geolocator.dart';
import 'package:krow_domain/krow_domain.dart';
abstract class ClockInEvent extends Equatable { abstract class ClockInEvent extends Equatable {
const ClockInEvent(); const ClockInEvent();
@@ -10,6 +11,14 @@ abstract class ClockInEvent extends Equatable {
class ClockInPageLoaded extends ClockInEvent {} class ClockInPageLoaded extends ClockInEvent {}
class ShiftSelected extends ClockInEvent {
final Shift shift;
const ShiftSelected(this.shift);
@override
List<Object?> get props => [shift];
}
class DateSelected extends ClockInEvent { class DateSelected extends ClockInEvent {
final DateTime date; final DateTime date;

View File

@@ -7,7 +7,8 @@ enum ClockInStatus { initial, loading, success, failure, actionInProgress }
class ClockInState extends Equatable { class ClockInState extends Equatable {
final ClockInStatus status; final ClockInStatus status;
final Shift? todayShift; final List<Shift> todayShifts;
final Shift? selectedShift;
final AttendanceStatus attendance; final AttendanceStatus attendance;
final DateTime selectedDate; final DateTime selectedDate;
final String checkInMode; final String checkInMode;
@@ -22,7 +23,8 @@ class ClockInState extends Equatable {
const ClockInState({ const ClockInState({
this.status = ClockInStatus.initial, this.status = ClockInStatus.initial,
this.todayShift, this.todayShifts = const [],
this.selectedShift,
this.attendance = const AttendanceStatus(), this.attendance = const AttendanceStatus(),
required this.selectedDate, required this.selectedDate,
this.checkInMode = 'swipe', this.checkInMode = 'swipe',
@@ -37,7 +39,8 @@ class ClockInState extends Equatable {
ClockInState copyWith({ ClockInState copyWith({
ClockInStatus? status, ClockInStatus? status,
Shift? todayShift, List<Shift>? todayShifts,
Shift? selectedShift,
AttendanceStatus? attendance, AttendanceStatus? attendance,
DateTime? selectedDate, DateTime? selectedDate,
String? checkInMode, String? checkInMode,
@@ -51,7 +54,8 @@ class ClockInState extends Equatable {
}) { }) {
return ClockInState( return ClockInState(
status: status ?? this.status, status: status ?? this.status,
todayShift: todayShift ?? this.todayShift, todayShifts: todayShifts ?? this.todayShifts,
selectedShift: selectedShift ?? this.selectedShift,
attendance: attendance ?? this.attendance, attendance: attendance ?? this.attendance,
selectedDate: selectedDate ?? this.selectedDate, selectedDate: selectedDate ?? this.selectedDate,
checkInMode: checkInMode ?? this.checkInMode, checkInMode: checkInMode ?? this.checkInMode,
@@ -68,7 +72,8 @@ class ClockInState extends Equatable {
@override @override
List<Object?> get props => [ List<Object?> get props => [
status, status,
todayShift, todayShifts,
selectedShift,
attendance, attendance,
selectedDate, selectedDate,
checkInMode, checkInMode,

View File

@@ -46,16 +46,23 @@ class _ClockInPageState extends State<ClockInPage> {
}, },
builder: (context, state) { builder: (context, state) {
if (state.status == ClockInStatus.loading && if (state.status == ClockInStatus.loading &&
state.todayShift == null) { state.todayShifts.isEmpty) {
return const Scaffold( return const Scaffold(
body: Center(child: CircularProgressIndicator()), body: Center(child: CircularProgressIndicator()),
); );
} }
final todayShift = state.todayShift; final todayShifts = state.todayShifts;
final checkInTime = state.attendance.checkInTime; final selectedShift = state.selectedShift;
final checkOutTime = state.attendance.checkOutTime; final activeShiftId = state.attendance.activeShiftId;
final isCheckedIn = state.attendance.isCheckedIn; final bool isActiveSelected =
selectedShift != null && selectedShift.id == activeShiftId;
final checkInTime =
isActiveSelected ? state.attendance.checkInTime : null;
final checkOutTime =
isActiveSelected ? state.attendance.checkOutTime : null;
final isCheckedIn =
state.attendance.isCheckedIn && isActiveSelected;
// Format times for display // Format times for display
final checkInStr = checkInTime != null final checkInStr = checkInTime != null
@@ -89,9 +96,9 @@ class _ClockInPageState extends State<ClockInPage> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Commute Tracker (shows before date selector when applicable) // Commute Tracker (shows before date selector when applicable)
if (todayShift != null) if (selectedShift != null)
CommuteTracker( CommuteTracker(
shift: todayShift, shift: selectedShift,
hasLocationConsent: state.hasLocationConsent, hasLocationConsent: state.hasLocationConsent,
isCommuteModeOn: state.isCommuteModeOn, isCommuteModeOn: state.isCommuteModeOn,
distanceMeters: state.distanceFromVenue, distanceMeters: state.distanceFromVenue,
@@ -125,70 +132,93 @@ class _ClockInPageState extends State<ClockInPage> {
const SizedBox(height: 16), const SizedBox(height: 16),
// Selected Shift Info Card // Selected Shift Info Card
if (todayShift != null) if (todayShifts.isNotEmpty)
Container( Column(
children: todayShifts
.map(
(shift) => GestureDetector(
onTap: () =>
_bloc.add(ShiftSelected(shift)),
child: Container(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
margin: const EdgeInsets.only(bottom: 16), margin:
const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(
12,
),
border: Border.all( border: Border.all(
color: const Color(0xFFE2E8F0), color: shift.id ==
), // slate-200 selectedShift?.id
? AppColors.krowBlue
: const Color(0xFFE2E8F0),
width:
shift.id == selectedShift?.id
? 2
: 1,
),
), ),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment:
CrossAxisAlignment.start,
children: [ children: [
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: crossAxisAlignment:
CrossAxisAlignment.start, CrossAxisAlignment.start,
children: [ children: [
const Text( Text(
"TODAY'S SHIFT", shift.id ==
selectedShift?.id
? "SELECTED SHIFT"
: "TODAY'S SHIFT",
style: TextStyle( style: TextStyle(
fontSize: 10, fontSize: 10,
fontWeight: FontWeight.w600, fontWeight:
color: AppColors.krowBlue, FontWeight.w600,
color: shift.id ==
selectedShift?.id
? AppColors.krowBlue
: AppColors
.krowCharcoal,
letterSpacing: 0.5, letterSpacing: 0.5,
), ),
), ),
const SizedBox(height: 2), const SizedBox(height: 2),
Text( Text(
todayShift.title, shift.title,
style: const TextStyle( style: const TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w600, fontWeight:
color: Color( FontWeight.w600,
0xFF1E293B, color: Color(0xFF1E293B),
), // slate-800
), ),
), ),
Text( Text(
"${todayShift.clientName}${todayShift.location}", "${shift.clientName}${shift.location}",
style: const TextStyle( style: const TextStyle(
fontSize: 12, fontSize: 12,
color: Color( color: Color(0xFF64748B),
0xFF64748B,
), // slate-500
), ),
), ),
], ],
), ),
), ),
Column( Column(
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment:
CrossAxisAlignment.end,
children: [ children: [
Text( Text(
"${_formatTime(todayShift.startTime)} - ${_formatTime(todayShift.endTime)}", "${_formatTime(shift.startTime)} - ${_formatTime(shift.endTime)}",
style: const TextStyle( style: const TextStyle(
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: Color(0xFF475569), // slate-600 color: Color(0xFF475569),
), ),
), ),
Text( Text(
"\$${todayShift.hourlyRate}/hr", "\$${shift.hourlyRate}/hr",
style: const TextStyle( style: const TextStyle(
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@@ -200,10 +230,15 @@ class _ClockInPageState extends State<ClockInPage> {
], ],
), ),
), ),
),
)
.toList(),
),
// Swipe To Check In / Checked Out State / No Shift State // Swipe To Check In / Checked Out State / No Shift State
if (todayShift != null && checkOutTime == null) ...[ if (selectedShift != null && checkOutTime == null) ...[
if (!isCheckedIn && !_isCheckInAllowed(todayShift)) if (!isCheckedIn &&
!_isCheckInAllowed(selectedShift))
Container( Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.all(24), padding: const EdgeInsets.all(24),
@@ -229,7 +264,7 @@ class _ClockInPageState extends State<ClockInPage> {
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
"Check-in available at ${_getCheckInAvailabilityTime(todayShift)}", "Check-in available at ${_getCheckInAvailabilityTime(selectedShift)}",
style: const TextStyle( style: const TextStyle(
fontSize: 14, fontSize: 14,
color: Color(0xFF64748B), // slate-500 color: Color(0xFF64748B), // slate-500
@@ -252,7 +287,9 @@ class _ClockInPageState extends State<ClockInPage> {
await _showNFCDialog(context); await _showNFCDialog(context);
} else { } else {
_bloc.add( _bloc.add(
CheckInRequested(shiftId: todayShift.id), CheckInRequested(
shiftId: selectedShift.id,
),
); );
} }
}, },
@@ -270,7 +307,7 @@ class _ClockInPageState extends State<ClockInPage> {
); );
}, },
), ),
] else if (todayShift != null && ] else if (selectedShift != null &&
checkOutTime != null) ...[ checkOutTime != null) ...[
// Shift Completed State // Shift Completed State
Container( Container(
@@ -572,8 +609,8 @@ class _ClockInPageState extends State<ClockInPage> {
// After dialog closes, trigger the event if scan was successful (simulated) // After dialog closes, trigger the event if scan was successful (simulated)
// In real app, we would check the dialog result // In real app, we would check the dialog result
if (scanned && _bloc.state.todayShift != null) { if (scanned && _bloc.state.selectedShift != null) {
_bloc.add(CheckInRequested(shiftId: _bloc.state.todayShift!.id)); _bloc.add(CheckInRequested(shiftId: _bloc.state.selectedShift!.id));
} }
} }
@@ -617,7 +654,7 @@ class _ClockInPageState extends State<ClockInPage> {
String _getCheckInAvailabilityTime(Shift shift) { String _getCheckInAvailabilityTime(Shift shift) {
if (shift == null) return ''; if (shift == null) return '';
try { try {
final shiftStart = DateTime.parse(shift.endTime); final shiftStart = DateTime.parse(shift.startTime.trim());
final windowStart = shiftStart.subtract(const Duration(minutes: 15)); final windowStart = shiftStart.subtract(const Duration(minutes: 15));
return DateFormat('h:mm a').format(windowStart); return DateFormat('h:mm a').format(windowStart);
} catch (e) { } catch (e) {

View File

@@ -1,4 +1,3 @@
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';
@@ -18,7 +17,6 @@ class StaffClockInModule extends Module {
i.add<ClockInRepositoryInterface>( i.add<ClockInRepositoryInterface>(
() => ClockInRepositoryImpl( () => ClockInRepositoryImpl(
dataConnect: ExampleConnector.instance, dataConnect: ExampleConnector.instance,
firebaseAuth: FirebaseAuth.instance,
), ),
); );

View File

@@ -304,6 +304,8 @@ query getApplicationsByStaffId(
status status
durationDays durationDays
description description
latitude
longitude
order { order {
id id