solving bugs in check in
This commit is contained in:
@@ -1,16 +1,16 @@
|
||||
# Basic Usage
|
||||
|
||||
```dart
|
||||
ExampleConnector.instance.getShiftRoleById(getShiftRoleByIdVariables).execute();
|
||||
ExampleConnector.instance.listShiftRolesByShiftId(listShiftRolesByShiftIdVariables).execute();
|
||||
ExampleConnector.instance.listShiftRolesByRoleId(listShiftRolesByRoleIdVariables).execute();
|
||||
ExampleConnector.instance.listShiftRolesByShiftIdAndTimeRange(listShiftRolesByShiftIdAndTimeRangeVariables).execute();
|
||||
ExampleConnector.instance.listShiftRolesByVendorId(listShiftRolesByVendorIdVariables).execute();
|
||||
ExampleConnector.instance.listShiftRolesByBusinessAndDateRange(listShiftRolesByBusinessAndDateRangeVariables).execute();
|
||||
ExampleConnector.instance.listShiftRolesByBusinessAndOrder(listShiftRolesByBusinessAndOrderVariables).execute();
|
||||
ExampleConnector.instance.listShiftRolesByBusinessDateRangeCompletedOrders(listShiftRolesByBusinessDateRangeCompletedOrdersVariables).execute();
|
||||
ExampleConnector.instance.listShiftRolesByBusinessAndDatesSummary(listShiftRolesByBusinessAndDatesSummaryVariables).execute();
|
||||
ExampleConnector.instance.getCompletedShiftsByBusinessId(getCompletedShiftsByBusinessIdVariables).execute();
|
||||
ExampleConnector.instance.CreateStaff(createStaffVariables).execute();
|
||||
ExampleConnector.instance.UpdateStaff(updateStaffVariables).execute();
|
||||
ExampleConnector.instance.DeleteStaff(deleteStaffVariables).execute();
|
||||
ExampleConnector.instance.listStaffAvailabilities(listStaffAvailabilitiesVariables).execute();
|
||||
ExampleConnector.instance.listStaffAvailabilitiesByStaffId(listStaffAvailabilitiesByStaffIdVariables).execute();
|
||||
ExampleConnector.instance.getStaffAvailabilityByKey(getStaffAvailabilityByKeyVariables).execute();
|
||||
ExampleConnector.instance.listStaffAvailabilitiesByDay(listStaffAvailabilitiesByDayVariables).execute();
|
||||
ExampleConnector.instance.createStaffAvailabilityStats(createStaffAvailabilityStatsVariables).execute();
|
||||
ExampleConnector.instance.updateStaffAvailabilityStats(updateStaffAvailabilityStatsVariables).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:
|
||||
|
||||
```dart
|
||||
await ExampleConnector.instance.updateRoleCategory({ ... })
|
||||
.roleName(...)
|
||||
await ExampleConnector.instance.searchInvoiceTemplatesByOwnerAndName({ ... })
|
||||
.offset(...)
|
||||
.execute();
|
||||
```
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -144,6 +144,8 @@ class GetApplicationsByStaffIdApplicationsShift {
|
||||
final EnumValue<ShiftStatus>? status;
|
||||
final int? durationDays;
|
||||
final String? description;
|
||||
final double? latitude;
|
||||
final double? longitude;
|
||||
final GetApplicationsByStaffIdApplicationsShiftOrder order;
|
||||
GetApplicationsByStaffIdApplicationsShift.fromJson(dynamic json):
|
||||
|
||||
@@ -156,6 +158,8 @@ class GetApplicationsByStaffIdApplicationsShift {
|
||||
status = json['status'] == null ? null : shiftStatusDeserializer(json['status']),
|
||||
durationDays = json['durationDays'] == null ? null : nativeFromJson<int>(json['durationDays']),
|
||||
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']);
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
@@ -176,11 +180,13 @@ class GetApplicationsByStaffIdApplicationsShift {
|
||||
status == otherTyped.status &&
|
||||
durationDays == otherTyped.durationDays &&
|
||||
description == otherTyped.description &&
|
||||
latitude == otherTyped.latitude &&
|
||||
longitude == otherTyped.longitude &&
|
||||
order == otherTyped.order;
|
||||
|
||||
}
|
||||
@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() {
|
||||
@@ -210,6 +216,12 @@ class GetApplicationsByStaffIdApplicationsShift {
|
||||
if (description != null) {
|
||||
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();
|
||||
return json;
|
||||
}
|
||||
@@ -224,6 +236,8 @@ class GetApplicationsByStaffIdApplicationsShift {
|
||||
this.status,
|
||||
this.durationDays,
|
||||
this.description,
|
||||
this.latitude,
|
||||
this.longitude,
|
||||
required this.order,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,31 +1,25 @@
|
||||
import 'package:firebase_auth/firebase_auth.dart' as firebase;
|
||||
import 'package:firebase_data_connect/firebase_data_connect.dart';
|
||||
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 'package:krow_data_connect/src/session/staff_session_store.dart';
|
||||
import '../../domain/repositories/clock_in_repository_interface.dart';
|
||||
|
||||
/// Implementation of [ClockInRepositoryInterface] using Firebase Data Connect.
|
||||
class ClockInRepositoryImpl implements ClockInRepositoryInterface {
|
||||
final dc.ExampleConnector _dataConnect;
|
||||
final firebase.FirebaseAuth _firebaseAuth;
|
||||
|
||||
ClockInRepositoryImpl({
|
||||
required dc.ExampleConnector dataConnect,
|
||||
required firebase.FirebaseAuth firebaseAuth,
|
||||
}) : _dataConnect = dataConnect,
|
||||
_firebaseAuth = firebaseAuth;
|
||||
}) : _dataConnect = dataConnect;
|
||||
|
||||
Future<String> _getStaffId() async {
|
||||
final firebase.User? user = _firebaseAuth.currentUser;
|
||||
if (user == null) throw Exception('User not authenticated');
|
||||
|
||||
final QueryResult<dc.GetStaffByUserIdData, dc.GetStaffByUserIdVariables> result =
|
||||
await _dataConnect.getStaffByUserId(userId: user.uid).execute();
|
||||
if (result.data.staffs.isEmpty) {
|
||||
throw Exception('Staff profile not found');
|
||||
final StaffSession? session = StaffSessionStore.instance.session;
|
||||
final String? staffId = session?.staff?.id;
|
||||
if (staffId != null && staffId.isNotEmpty) {
|
||||
return staffId;
|
||||
}
|
||||
return result.data.staffs.first.id;
|
||||
throw Exception('Staff session not found');
|
||||
}
|
||||
|
||||
/// Helper to convert Data Connect Timestamp to DateTime
|
||||
@@ -68,81 +62,131 @@ class ClockInRepositoryImpl implements ClockInRepositoryInterface {
|
||||
return Timestamp.fromJson(d.toUtc().toIso8601String());
|
||||
}
|
||||
|
||||
/// Helper to find today's active application
|
||||
Future<dc.GetApplicationsByStaffIdApplications?> _getTodaysApplication(String staffId) async {
|
||||
({Timestamp start, 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: _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();
|
||||
|
||||
// Fetch recent applications (assuming meaningful limit)
|
||||
final QueryResult<dc.GetApplicationsByStaffIdData, dc.GetApplicationsByStaffIdVariables> result =
|
||||
await _dataConnect.getApplicationsByStaffId(
|
||||
staffId: staffId,
|
||||
).limit(20).execute();
|
||||
final range = _utcDayRange(now);
|
||||
final QueryResult<dc.GetApplicationsByStaffIdData, dc.GetApplicationsByStaffIdVariables>
|
||||
result = await _dataConnect
|
||||
.getApplicationsByStaffId(staffId: staffId)
|
||||
.dayStart(range.start)
|
||||
.dayEnd(range.end)
|
||||
.execute();
|
||||
|
||||
final apps = result.data.applications;
|
||||
if (apps.isEmpty) return const [];
|
||||
|
||||
apps.sort((a, b) {
|
||||
final DateTime? aTime =
|
||||
_toDateTime(a.shift.startTime) ?? _toDateTime(a.shift.date);
|
||||
final DateTime? bTime =
|
||||
_toDateTime(b.shift.startTime) ?? _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;
|
||||
}
|
||||
|
||||
dc.GetApplicationsByStaffIdApplications? _getActiveApplication(
|
||||
List<dc.GetApplicationsByStaffIdApplications> apps,
|
||||
) {
|
||||
try {
|
||||
return result.data.applications.firstWhere((dc.GetApplicationsByStaffIdApplications app) {
|
||||
final DateTime? shiftTime = _toDateTime(app.shift.startTime);
|
||||
|
||||
if (shiftTime == null) return false;
|
||||
|
||||
final bool isSameDay = shiftTime.year == now.year &&
|
||||
shiftTime.month == now.month &&
|
||||
shiftTime.day == now.day;
|
||||
|
||||
if (!isSameDay) return false;
|
||||
|
||||
// Check Status
|
||||
final dynamic status = app.status.stringValue;
|
||||
return status != 'PENDING' && status != 'REJECTED' && status != 'NO_SHOW' && status != 'CANCELED';
|
||||
return apps.firstWhere((app) {
|
||||
final status = app.status.stringValue;
|
||||
return status == 'CHECKED_IN' || status == 'LATE';
|
||||
});
|
||||
} catch (e) {
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Shift?> getTodaysShift() async {
|
||||
Future<List<Shift>> getTodaysShifts() async {
|
||||
final String staffId = await _getStaffId();
|
||||
final dc.GetApplicationsByStaffIdApplications? app = await _getTodaysApplication(staffId);
|
||||
final List<dc.GetApplicationsByStaffIdApplications> apps =
|
||||
await _getTodaysApplications(staffId);
|
||||
if (apps.isEmpty) return const [];
|
||||
|
||||
if (app == null) return null;
|
||||
final List<Shift> shifts = [];
|
||||
for (final 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 dc.GetApplicationsByStaffIdApplicationsShift shift = app.shift;
|
||||
|
||||
final QueryResult<dc.GetShiftByIdData, dc.GetShiftByIdVariables> shiftResult =
|
||||
await _dataConnect.getShiftById(id: shift.id).execute();
|
||||
|
||||
if (shiftResult.data.shift == null) return null;
|
||||
|
||||
final dc.GetShiftByIdShift fullShift = shiftResult.data.shift!;
|
||||
|
||||
return Shift(
|
||||
id: fullShift.id,
|
||||
title: fullShift.title,
|
||||
clientName: fullShift.order.business.businessName,
|
||||
logoUrl: '', // Not available in GetShiftById
|
||||
hourlyRate: 0.0,
|
||||
location: fullShift.location ?? '',
|
||||
locationAddress: fullShift.locationAddress ?? '',
|
||||
date: _toDateTime(fullShift.startTime)?.toIso8601String() ?? '',
|
||||
startTime: _toDateTime(fullShift.startTime)?.toIso8601String() ?? '',
|
||||
endTime: _toDateTime(fullShift.endTime)?.toIso8601String() ?? '',
|
||||
createdDate: _toDateTime(fullShift.createdAt)?.toIso8601String() ?? '',
|
||||
status: fullShift.status?.stringValue,
|
||||
description: fullShift.description,
|
||||
latitude: fullShift.latitude,
|
||||
longitude: fullShift.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;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<AttendanceStatus> getAttendanceStatus() async {
|
||||
final String staffId = await _getStaffId();
|
||||
final dc.GetApplicationsByStaffIdApplications? app = await _getTodaysApplication(staffId);
|
||||
|
||||
if (app == null) {
|
||||
final List<dc.GetApplicationsByStaffIdApplications> apps =
|
||||
await _getTodaysApplications(staffId);
|
||||
if (apps.isEmpty) {
|
||||
return const AttendanceStatus(isCheckedIn: false);
|
||||
}
|
||||
|
||||
final dc.GetApplicationsByStaffIdApplications? activeApp =
|
||||
_getActiveApplication(apps);
|
||||
final dc.GetApplicationsByStaffIdApplications app =
|
||||
activeApp ?? apps.last;
|
||||
|
||||
return ClockInAdapter.toAttendanceStatus(
|
||||
status: app.status.stringValue,
|
||||
checkInTime: _toDateTime(app.checkInTime),
|
||||
@@ -175,7 +219,10 @@ class ClockInRepositoryImpl implements ClockInRepositoryInterface {
|
||||
Future<AttendanceStatus> clockOut({String? notes, int? breakTimeMinutes}) async {
|
||||
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');
|
||||
|
||||
await _dataConnect.updateApplicationStatus(
|
||||
|
||||
@@ -3,9 +3,9 @@ import 'package:krow_domain/krow_domain.dart';
|
||||
/// Repository interface for Clock In/Out functionality
|
||||
abstract class ClockInRepositoryInterface {
|
||||
|
||||
/// Retrieves the shift assigned to the user for the current day.
|
||||
/// Returns null if no shift is assigned for today.
|
||||
Future<Shift?> getTodaysShift();
|
||||
/// Retrieves the shifts assigned to the user for the current day.
|
||||
/// Returns empty list if no shift is assigned for today.
|
||||
Future<List<Shift>> getTodaysShifts();
|
||||
|
||||
/// Gets the current attendance status (e.g., checked in or not, times).
|
||||
/// This helps in restoring the UI state if the app was killed.
|
||||
|
||||
@@ -2,14 +2,14 @@ import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../repositories/clock_in_repository_interface.dart';
|
||||
|
||||
/// Use case for retrieving the user's scheduled shift for today.
|
||||
class GetTodaysShiftUseCase implements NoInputUseCase<Shift?> {
|
||||
/// Use case for retrieving the user's scheduled shifts for today.
|
||||
class GetTodaysShiftUseCase implements NoInputUseCase<List<Shift>> {
|
||||
final ClockInRepositoryInterface _repository;
|
||||
|
||||
GetTodaysShiftUseCase(this._repository);
|
||||
|
||||
@override
|
||||
Future<Shift?> call() {
|
||||
return _repository.getTodaysShift();
|
||||
Future<List<Shift>> call() {
|
||||
return _repository.getTodaysShifts();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.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_attendance_status_usecase.dart';
|
||||
import '../../domain/usecases/clock_in_usecase.dart';
|
||||
@@ -29,6 +30,7 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
|
||||
_clockOut = clockOut,
|
||||
super(ClockInState(selectedDate: DateTime.now())) {
|
||||
on<ClockInPageLoaded>(_onLoaded);
|
||||
on<ShiftSelected>(_onShiftSelected);
|
||||
on<DateSelected>(_onDateSelected);
|
||||
on<CheckInRequested>(_onCheckIn);
|
||||
on<CheckOutRequested>(_onCheckOut);
|
||||
@@ -46,19 +48,30 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
|
||||
) async {
|
||||
emit(state.copyWith(status: ClockInStatus.loading));
|
||||
try {
|
||||
final shift = await _getTodaysShift();
|
||||
final shifts = await _getTodaysShift();
|
||||
final status = await _getAttendanceStatus();
|
||||
|
||||
// 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
|
||||
|
||||
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(
|
||||
status: ClockInStatus.success,
|
||||
todayShift: shift,
|
||||
todayShifts: shifts,
|
||||
selectedShift: selectedShift,
|
||||
attendance: status,
|
||||
));
|
||||
|
||||
if (shift != null && !status.isCheckedIn) {
|
||||
if (selectedShift != null && !status.isCheckedIn) {
|
||||
add(RequestLocationPermission());
|
||||
}
|
||||
|
||||
@@ -99,12 +112,14 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
|
||||
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) {
|
||||
if (state.selectedShift != null &&
|
||||
state.selectedShift!.latitude != null &&
|
||||
state.selectedShift!.longitude != null) {
|
||||
distance = Geolocator.distanceBetween(
|
||||
position.latitude,
|
||||
position.longitude,
|
||||
state.todayShift!.latitude!,
|
||||
state.todayShift!.longitude!,
|
||||
state.selectedShift!.latitude!,
|
||||
state.selectedShift!.longitude!,
|
||||
);
|
||||
isVerified = distance <= allowedRadiusMeters;
|
||||
} 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(
|
||||
DateSelected event,
|
||||
Emitter<ClockInState> emit,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
abstract class ClockInEvent extends Equatable {
|
||||
const ClockInEvent();
|
||||
@@ -10,6 +11,14 @@ abstract class ClockInEvent extends Equatable {
|
||||
|
||||
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 {
|
||||
final DateTime date;
|
||||
|
||||
|
||||
@@ -7,7 +7,8 @@ enum ClockInStatus { initial, loading, success, failure, actionInProgress }
|
||||
|
||||
class ClockInState extends Equatable {
|
||||
final ClockInStatus status;
|
||||
final Shift? todayShift;
|
||||
final List<Shift> todayShifts;
|
||||
final Shift? selectedShift;
|
||||
final AttendanceStatus attendance;
|
||||
final DateTime selectedDate;
|
||||
final String checkInMode;
|
||||
@@ -22,7 +23,8 @@ class ClockInState extends Equatable {
|
||||
|
||||
const ClockInState({
|
||||
this.status = ClockInStatus.initial,
|
||||
this.todayShift,
|
||||
this.todayShifts = const [],
|
||||
this.selectedShift,
|
||||
this.attendance = const AttendanceStatus(),
|
||||
required this.selectedDate,
|
||||
this.checkInMode = 'swipe',
|
||||
@@ -37,7 +39,8 @@ class ClockInState extends Equatable {
|
||||
|
||||
ClockInState copyWith({
|
||||
ClockInStatus? status,
|
||||
Shift? todayShift,
|
||||
List<Shift>? todayShifts,
|
||||
Shift? selectedShift,
|
||||
AttendanceStatus? attendance,
|
||||
DateTime? selectedDate,
|
||||
String? checkInMode,
|
||||
@@ -51,7 +54,8 @@ class ClockInState extends Equatable {
|
||||
}) {
|
||||
return ClockInState(
|
||||
status: status ?? this.status,
|
||||
todayShift: todayShift ?? this.todayShift,
|
||||
todayShifts: todayShifts ?? this.todayShifts,
|
||||
selectedShift: selectedShift ?? this.selectedShift,
|
||||
attendance: attendance ?? this.attendance,
|
||||
selectedDate: selectedDate ?? this.selectedDate,
|
||||
checkInMode: checkInMode ?? this.checkInMode,
|
||||
@@ -68,7 +72,8 @@ class ClockInState extends Equatable {
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
status,
|
||||
todayShift,
|
||||
todayShifts,
|
||||
selectedShift,
|
||||
attendance,
|
||||
selectedDate,
|
||||
checkInMode,
|
||||
|
||||
@@ -46,16 +46,23 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
},
|
||||
builder: (context, state) {
|
||||
if (state.status == ClockInStatus.loading &&
|
||||
state.todayShift == null) {
|
||||
state.todayShifts.isEmpty) {
|
||||
return const Scaffold(
|
||||
body: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
final todayShift = state.todayShift;
|
||||
final checkInTime = state.attendance.checkInTime;
|
||||
final checkOutTime = state.attendance.checkOutTime;
|
||||
final isCheckedIn = state.attendance.isCheckedIn;
|
||||
final todayShifts = state.todayShifts;
|
||||
final selectedShift = state.selectedShift;
|
||||
final activeShiftId = state.attendance.activeShiftId;
|
||||
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
|
||||
final checkInStr = checkInTime != null
|
||||
@@ -89,9 +96,9 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Commute Tracker (shows before date selector when applicable)
|
||||
if (todayShift != null)
|
||||
if (selectedShift != null)
|
||||
CommuteTracker(
|
||||
shift: todayShift,
|
||||
shift: selectedShift,
|
||||
hasLocationConsent: state.hasLocationConsent,
|
||||
isCommuteModeOn: state.isCommuteModeOn,
|
||||
distanceMeters: state.distanceFromVenue,
|
||||
@@ -125,85 +132,113 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Selected Shift Info Card
|
||||
if (todayShift != null)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: const Color(0xFFE2E8F0),
|
||||
), // slate-200
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
"TODAY'S SHIFT",
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.krowBlue,
|
||||
letterSpacing: 0.5,
|
||||
if (todayShifts.isNotEmpty)
|
||||
Column(
|
||||
children: todayShifts
|
||||
.map(
|
||||
(shift) => GestureDetector(
|
||||
onTap: () =>
|
||||
_bloc.add(ShiftSelected(shift)),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
margin:
|
||||
const EdgeInsets.only(bottom: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(
|
||||
12,
|
||||
),
|
||||
border: Border.all(
|
||||
color: shift.id ==
|
||||
selectedShift?.id
|
||||
? AppColors.krowBlue
|
||||
: const Color(0xFFE2E8F0),
|
||||
width:
|
||||
shift.id == selectedShift?.id
|
||||
? 2
|
||||
: 1,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
todayShift.title,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(
|
||||
0xFF1E293B,
|
||||
), // slate-800
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
shift.id ==
|
||||
selectedShift?.id
|
||||
? "SELECTED SHIFT"
|
||||
: "TODAY'S SHIFT",
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight:
|
||||
FontWeight.w600,
|
||||
color: shift.id ==
|
||||
selectedShift?.id
|
||||
? AppColors.krowBlue
|
||||
: AppColors
|
||||
.krowCharcoal,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
shift.title,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight:
|
||||
FontWeight.w600,
|
||||
color: Color(0xFF1E293B),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"${shift.clientName} • ${shift.location}",
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF64748B),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
"${_formatTime(shift.startTime)} - ${_formatTime(shift.endTime)}",
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF475569),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"\$${shift.hourlyRate}/hr",
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.krowBlue,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
"${todayShift.clientName} • ${todayShift.location}",
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(
|
||||
0xFF64748B,
|
||||
), // slate-500
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
"${_formatTime(todayShift.startTime)} - ${_formatTime(todayShift.endTime)}",
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF475569), // slate-600
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"\$${todayShift.hourlyRate}/hr",
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.krowBlue,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
|
||||
// Swipe To Check In / Checked Out State / No Shift State
|
||||
if (todayShift != null && checkOutTime == null) ...[
|
||||
if (!isCheckedIn && !_isCheckInAllowed(todayShift))
|
||||
if (selectedShift != null && checkOutTime == null) ...[
|
||||
if (!isCheckedIn &&
|
||||
!_isCheckInAllowed(selectedShift))
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(24),
|
||||
@@ -229,7 +264,7 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
"Check-in available at ${_getCheckInAvailabilityTime(todayShift)}",
|
||||
"Check-in available at ${_getCheckInAvailabilityTime(selectedShift)}",
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF64748B), // slate-500
|
||||
@@ -252,7 +287,9 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
await _showNFCDialog(context);
|
||||
} else {
|
||||
_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) ...[
|
||||
// Shift Completed State
|
||||
Container(
|
||||
@@ -572,8 +609,8 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
|
||||
// After dialog closes, trigger the event if scan was successful (simulated)
|
||||
// In real app, we would check the dialog result
|
||||
if (scanned && _bloc.state.todayShift != null) {
|
||||
_bloc.add(CheckInRequested(shiftId: _bloc.state.todayShift!.id));
|
||||
if (scanned && _bloc.state.selectedShift != null) {
|
||||
_bloc.add(CheckInRequested(shiftId: _bloc.state.selectedShift!.id));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -617,7 +654,7 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
String _getCheckInAvailabilityTime(Shift shift) {
|
||||
if (shift == null) return '';
|
||||
try {
|
||||
final shiftStart = DateTime.parse(shift.endTime);
|
||||
final shiftStart = DateTime.parse(shift.startTime.trim());
|
||||
final windowStart = shiftStart.subtract(const Duration(minutes: 15));
|
||||
return DateFormat('h:mm a').format(windowStart);
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||
|
||||
@@ -18,7 +17,6 @@ class StaffClockInModule extends Module {
|
||||
i.add<ClockInRepositoryInterface>(
|
||||
() => ClockInRepositoryImpl(
|
||||
dataConnect: ExampleConnector.instance,
|
||||
firebaseAuth: FirebaseAuth.instance,
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -304,6 +304,8 @@ query getApplicationsByStaffId(
|
||||
status
|
||||
durationDays
|
||||
description
|
||||
latitude
|
||||
longitude
|
||||
|
||||
order {
|
||||
id
|
||||
|
||||
Reference in New Issue
Block a user