feat: Add localization for hourly rate and enhance geofence notifications
- Added localization for hourly rate in English and Spanish. - Updated background geofence service to use localized notification titles and bodies. - Enhanced ClockInBloc to compute time window flags for check-in/check-out availability. - Updated ClockInState and ClockInEvent to include check-in/check-out availability flags and messages. - Refactored ClockInActionSection to display availability messages based on computed flags. - Ensured compliance with design system for user-facing strings and notification messages.
This commit is contained in:
@@ -862,6 +862,7 @@
|
|||||||
"great_work": "Great work today",
|
"great_work": "Great work today",
|
||||||
"no_shifts_today": "No confirmed shifts for today",
|
"no_shifts_today": "No confirmed shifts for today",
|
||||||
"accept_shift_cta": "Accept a shift to clock in",
|
"accept_shift_cta": "Accept a shift to clock in",
|
||||||
|
"per_hr": "\\$$amount/hr",
|
||||||
"soon": "soon",
|
"soon": "soon",
|
||||||
"checked_in_at_label": "Checked in at",
|
"checked_in_at_label": "Checked in at",
|
||||||
"not_in_range": "You must be within $distance m to clock in.",
|
"not_in_range": "You must be within $distance m to clock in.",
|
||||||
|
|||||||
@@ -857,6 +857,7 @@
|
|||||||
"great_work": "Buen trabajo hoy",
|
"great_work": "Buen trabajo hoy",
|
||||||
"no_shifts_today": "No hay turnos confirmados para hoy",
|
"no_shifts_today": "No hay turnos confirmados para hoy",
|
||||||
"accept_shift_cta": "Acepte un turno para registrar su entrada",
|
"accept_shift_cta": "Acepte un turno para registrar su entrada",
|
||||||
|
"per_hr": "\\$$amount/hr",
|
||||||
"soon": "pronto",
|
"soon": "pronto",
|
||||||
"checked_in_at_label": "Entrada registrada a las",
|
"checked_in_at_label": "Entrada registrada a las",
|
||||||
"nfc_dialog": {
|
"nfc_dialog": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// ignore_for_file: avoid_print
|
// ignore_for_file: avoid_print
|
||||||
import 'package:krow_core/core.dart';
|
import 'package:krow_core/core.dart';
|
||||||
import 'package:krow_domain/src/core/models/device_location.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
/// Top-level callback dispatcher for background geofence tasks.
|
/// Top-level callback dispatcher for background geofence tasks.
|
||||||
///
|
///
|
||||||
@@ -63,13 +63,17 @@ void backgroundGeofenceDispatcher() {
|
|||||||
'showing notification',
|
'showing notification',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final String title = inputData?['leftGeofenceTitle'] as String? ??
|
||||||
|
"You've Left the Workplace";
|
||||||
|
final String body = inputData?['leftGeofenceBody'] as String? ??
|
||||||
|
'You appear to be more than 500m from your shift location.';
|
||||||
|
|
||||||
final NotificationService notificationService =
|
final NotificationService notificationService =
|
||||||
NotificationService();
|
NotificationService();
|
||||||
await notificationService.showNotification(
|
await notificationService.showNotification(
|
||||||
id: BackgroundGeofenceService.leftGeofenceNotificationId,
|
id: BackgroundGeofenceService.leftGeofenceNotificationId,
|
||||||
title: "You've Left the Workplace",
|
title: title,
|
||||||
body:
|
body: body,
|
||||||
'You appear to be more than 500m from your shift location.',
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
print(
|
print(
|
||||||
@@ -138,11 +142,14 @@ class BackgroundGeofenceService {
|
|||||||
/// Starts periodic 15-minute background geofence checks.
|
/// Starts periodic 15-minute background geofence checks.
|
||||||
///
|
///
|
||||||
/// Called after a successful clock-in. Persists the target coordinates
|
/// Called after a successful clock-in. Persists the target coordinates
|
||||||
/// so the background isolate can access them.
|
/// and passes localized notification strings via [inputData] so the
|
||||||
|
/// background isolate can display them without DI.
|
||||||
Future<void> startBackgroundTracking({
|
Future<void> startBackgroundTracking({
|
||||||
required double targetLat,
|
required double targetLat,
|
||||||
required double targetLng,
|
required double targetLng,
|
||||||
required String shiftId,
|
required String shiftId,
|
||||||
|
required String leftGeofenceTitle,
|
||||||
|
required String leftGeofenceBody,
|
||||||
}) async {
|
}) async {
|
||||||
await Future.wait(<Future<bool>>[
|
await Future.wait(<Future<bool>>[
|
||||||
_storageService.setDouble(_keyTargetLat, targetLat),
|
_storageService.setDouble(_keyTargetLat, targetLat),
|
||||||
@@ -159,6 +166,8 @@ class BackgroundGeofenceService {
|
|||||||
'targetLat': targetLat,
|
'targetLat': targetLat,
|
||||||
'targetLng': targetLng,
|
'targetLng': targetLng,
|
||||||
'shiftId': shiftId,
|
'shiftId': shiftId,
|
||||||
|
'leftGeofenceTitle': leftGeofenceTitle,
|
||||||
|
'leftGeofenceBody': leftGeofenceBody,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import '../../../domain/usecases/get_todays_shift_usecase.dart';
|
|||||||
import '../../../domain/validators/clock_in_validation_context.dart';
|
import '../../../domain/validators/clock_in_validation_context.dart';
|
||||||
import '../../../domain/validators/clock_in_validation_result.dart';
|
import '../../../domain/validators/clock_in_validation_result.dart';
|
||||||
import '../../../domain/validators/validators/composite_clock_in_validator.dart';
|
import '../../../domain/validators/validators/composite_clock_in_validator.dart';
|
||||||
|
import '../../../domain/validators/validators/time_window_validator.dart';
|
||||||
import '../geofence/geofence_bloc.dart';
|
import '../geofence/geofence_bloc.dart';
|
||||||
import '../geofence/geofence_event.dart';
|
import '../geofence/geofence_event.dart';
|
||||||
import '../geofence/geofence_state.dart';
|
import '../geofence/geofence_state.dart';
|
||||||
@@ -86,11 +87,19 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState>
|
|||||||
selectedShift ??= shifts.last;
|
selectedShift ??= shifts.last;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final _TimeWindowFlags timeFlags = _computeTimeWindowFlags(
|
||||||
|
selectedShift,
|
||||||
|
);
|
||||||
|
|
||||||
emit(state.copyWith(
|
emit(state.copyWith(
|
||||||
status: ClockInStatus.success,
|
status: ClockInStatus.success,
|
||||||
todayShifts: shifts,
|
todayShifts: shifts,
|
||||||
selectedShift: selectedShift,
|
selectedShift: selectedShift,
|
||||||
attendance: status,
|
attendance: status,
|
||||||
|
isCheckInAllowed: timeFlags.isCheckInAllowed,
|
||||||
|
isCheckOutAllowed: timeFlags.isCheckOutAllowed,
|
||||||
|
checkInAvailabilityTime: timeFlags.checkInAvailabilityTime,
|
||||||
|
checkOutAvailabilityTime: timeFlags.checkOutAvailabilityTime,
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
onError: (String errorKey) => state.copyWith(
|
onError: (String errorKey) => state.copyWith(
|
||||||
@@ -100,12 +109,19 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates the currently selected shift.
|
/// Updates the currently selected shift and recomputes time window flags.
|
||||||
void _onShiftSelected(
|
void _onShiftSelected(
|
||||||
ShiftSelected event,
|
ShiftSelected event,
|
||||||
Emitter<ClockInState> emit,
|
Emitter<ClockInState> emit,
|
||||||
) {
|
) {
|
||||||
emit(state.copyWith(selectedShift: event.shift));
|
final _TimeWindowFlags timeFlags = _computeTimeWindowFlags(event.shift);
|
||||||
|
emit(state.copyWith(
|
||||||
|
selectedShift: event.shift,
|
||||||
|
isCheckInAllowed: timeFlags.isCheckInAllowed,
|
||||||
|
isCheckOutAllowed: timeFlags.isCheckOutAllowed,
|
||||||
|
checkInAvailabilityTime: timeFlags.checkInAvailabilityTime,
|
||||||
|
checkOutAvailabilityTime: timeFlags.checkOutAvailabilityTime,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates the selected date for shift viewing.
|
/// Updates the selected date for shift viewing.
|
||||||
@@ -236,6 +252,56 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState>
|
|||||||
return DateTime.tryParse(value);
|
return DateTime.tryParse(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Computes time-window check-in/check-out flags for the given [shift].
|
||||||
|
///
|
||||||
|
/// Uses [TimeWindowValidator] so this business logic stays out of widgets.
|
||||||
|
static _TimeWindowFlags _computeTimeWindowFlags(Shift? shift) {
|
||||||
|
if (shift == null) {
|
||||||
|
return const _TimeWindowFlags();
|
||||||
|
}
|
||||||
|
|
||||||
|
const TimeWindowValidator validator = TimeWindowValidator();
|
||||||
|
final DateTime? shiftStart = _tryParseDateTime(shift.startTime);
|
||||||
|
final DateTime? shiftEnd = _tryParseDateTime(shift.endTime);
|
||||||
|
|
||||||
|
// Check-in window.
|
||||||
|
bool isCheckInAllowed = true;
|
||||||
|
String? checkInAvailabilityTime;
|
||||||
|
if (shiftStart != null) {
|
||||||
|
final ClockInValidationContext checkInCtx = ClockInValidationContext(
|
||||||
|
isCheckingIn: true,
|
||||||
|
shiftStartTime: shiftStart,
|
||||||
|
);
|
||||||
|
isCheckInAllowed = validator.validate(checkInCtx).isValid;
|
||||||
|
if (!isCheckInAllowed) {
|
||||||
|
checkInAvailabilityTime =
|
||||||
|
TimeWindowValidator.getAvailabilityTime(shiftStart);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check-out window.
|
||||||
|
bool isCheckOutAllowed = true;
|
||||||
|
String? checkOutAvailabilityTime;
|
||||||
|
if (shiftEnd != null) {
|
||||||
|
final ClockInValidationContext checkOutCtx = ClockInValidationContext(
|
||||||
|
isCheckingIn: false,
|
||||||
|
shiftEndTime: shiftEnd,
|
||||||
|
);
|
||||||
|
isCheckOutAllowed = validator.validate(checkOutCtx).isValid;
|
||||||
|
if (!isCheckOutAllowed) {
|
||||||
|
checkOutAvailabilityTime =
|
||||||
|
TimeWindowValidator.getAvailabilityTime(shiftEnd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return _TimeWindowFlags(
|
||||||
|
isCheckInAllowed: isCheckInAllowed,
|
||||||
|
isCheckOutAllowed: isCheckOutAllowed,
|
||||||
|
checkInAvailabilityTime: checkInAvailabilityTime,
|
||||||
|
checkOutAvailabilityTime: checkOutAvailabilityTime,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Dispatches [BackgroundTrackingStarted] to [_geofenceBloc] if the
|
/// Dispatches [BackgroundTrackingStarted] to [_geofenceBloc] if the
|
||||||
/// geofence has target coordinates.
|
/// geofence has target coordinates.
|
||||||
void _dispatchBackgroundTrackingStarted({
|
void _dispatchBackgroundTrackingStarted({
|
||||||
@@ -254,8 +320,33 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState>
|
|||||||
targetLng: geofenceState.targetLng!,
|
targetLng: geofenceState.targetLng!,
|
||||||
greetingTitle: event.clockInGreetingTitle,
|
greetingTitle: event.clockInGreetingTitle,
|
||||||
greetingBody: event.clockInGreetingBody,
|
greetingBody: event.clockInGreetingBody,
|
||||||
|
leftGeofenceTitle: event.leftGeofenceTitle,
|
||||||
|
leftGeofenceBody: event.leftGeofenceBody,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Internal value holder for time-window computation results.
|
||||||
|
class _TimeWindowFlags {
|
||||||
|
/// Creates a [_TimeWindowFlags] with default allowed values.
|
||||||
|
const _TimeWindowFlags({
|
||||||
|
this.isCheckInAllowed = true,
|
||||||
|
this.isCheckOutAllowed = true,
|
||||||
|
this.checkInAvailabilityTime,
|
||||||
|
this.checkOutAvailabilityTime,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Whether the time window currently allows check-in.
|
||||||
|
final bool isCheckInAllowed;
|
||||||
|
|
||||||
|
/// Whether the time window currently allows check-out.
|
||||||
|
final bool isCheckOutAllowed;
|
||||||
|
|
||||||
|
/// Formatted time when check-in becomes available, or `null`.
|
||||||
|
final String? checkInAvailabilityTime;
|
||||||
|
|
||||||
|
/// Formatted time when check-out becomes available, or `null`.
|
||||||
|
final String? checkOutAvailabilityTime;
|
||||||
|
}
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ class CheckInRequested extends ClockInEvent {
|
|||||||
this.notes,
|
this.notes,
|
||||||
this.clockInGreetingTitle = '',
|
this.clockInGreetingTitle = '',
|
||||||
this.clockInGreetingBody = '',
|
this.clockInGreetingBody = '',
|
||||||
|
this.leftGeofenceTitle = '',
|
||||||
|
this.leftGeofenceBody = '',
|
||||||
});
|
});
|
||||||
|
|
||||||
/// The ID of the shift to clock into.
|
/// The ID of the shift to clock into.
|
||||||
@@ -59,12 +61,20 @@ class CheckInRequested extends ClockInEvent {
|
|||||||
/// Localized body for the clock-in greeting notification.
|
/// Localized body for the clock-in greeting notification.
|
||||||
final String clockInGreetingBody;
|
final String clockInGreetingBody;
|
||||||
|
|
||||||
|
/// Localized title for the left-geofence background notification.
|
||||||
|
final String leftGeofenceTitle;
|
||||||
|
|
||||||
|
/// Localized body for the left-geofence background notification.
|
||||||
|
final String leftGeofenceBody;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => <Object?>[
|
List<Object?> get props => <Object?>[
|
||||||
shiftId,
|
shiftId,
|
||||||
notes,
|
notes,
|
||||||
clockInGreetingTitle,
|
clockInGreetingTitle,
|
||||||
clockInGreetingBody,
|
clockInGreetingBody,
|
||||||
|
leftGeofenceTitle,
|
||||||
|
leftGeofenceBody,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ class ClockInState extends Equatable {
|
|||||||
required this.selectedDate,
|
required this.selectedDate,
|
||||||
this.checkInMode = 'swipe',
|
this.checkInMode = 'swipe',
|
||||||
this.errorMessage,
|
this.errorMessage,
|
||||||
|
this.isCheckInAllowed = true,
|
||||||
|
this.isCheckOutAllowed = true,
|
||||||
|
this.checkInAvailabilityTime,
|
||||||
|
this.checkOutAvailabilityTime,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Current page status.
|
/// Current page status.
|
||||||
@@ -41,6 +45,18 @@ class ClockInState extends Equatable {
|
|||||||
/// Error message key for displaying failures.
|
/// Error message key for displaying failures.
|
||||||
final String? errorMessage;
|
final String? errorMessage;
|
||||||
|
|
||||||
|
/// Whether the time window allows the user to check in.
|
||||||
|
final bool isCheckInAllowed;
|
||||||
|
|
||||||
|
/// Whether the time window allows the user to check out.
|
||||||
|
final bool isCheckOutAllowed;
|
||||||
|
|
||||||
|
/// Formatted earliest time when check-in becomes available, or `null`.
|
||||||
|
final String? checkInAvailabilityTime;
|
||||||
|
|
||||||
|
/// Formatted earliest time when check-out becomes available, or `null`.
|
||||||
|
final String? checkOutAvailabilityTime;
|
||||||
|
|
||||||
/// Creates a copy of this state with the given fields replaced.
|
/// Creates a copy of this state with the given fields replaced.
|
||||||
ClockInState copyWith({
|
ClockInState copyWith({
|
||||||
ClockInStatus? status,
|
ClockInStatus? status,
|
||||||
@@ -50,6 +66,10 @@ class ClockInState extends Equatable {
|
|||||||
DateTime? selectedDate,
|
DateTime? selectedDate,
|
||||||
String? checkInMode,
|
String? checkInMode,
|
||||||
String? errorMessage,
|
String? errorMessage,
|
||||||
|
bool? isCheckInAllowed,
|
||||||
|
bool? isCheckOutAllowed,
|
||||||
|
String? checkInAvailabilityTime,
|
||||||
|
String? checkOutAvailabilityTime,
|
||||||
}) {
|
}) {
|
||||||
return ClockInState(
|
return ClockInState(
|
||||||
status: status ?? this.status,
|
status: status ?? this.status,
|
||||||
@@ -59,6 +79,12 @@ class ClockInState extends Equatable {
|
|||||||
selectedDate: selectedDate ?? this.selectedDate,
|
selectedDate: selectedDate ?? this.selectedDate,
|
||||||
checkInMode: checkInMode ?? this.checkInMode,
|
checkInMode: checkInMode ?? this.checkInMode,
|
||||||
errorMessage: errorMessage,
|
errorMessage: errorMessage,
|
||||||
|
isCheckInAllowed: isCheckInAllowed ?? this.isCheckInAllowed,
|
||||||
|
isCheckOutAllowed: isCheckOutAllowed ?? this.isCheckOutAllowed,
|
||||||
|
checkInAvailabilityTime:
|
||||||
|
checkInAvailabilityTime ?? this.checkInAvailabilityTime,
|
||||||
|
checkOutAvailabilityTime:
|
||||||
|
checkOutAvailabilityTime ?? this.checkOutAvailabilityTime,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,5 +97,9 @@ class ClockInState extends Equatable {
|
|||||||
selectedDate,
|
selectedDate,
|
||||||
checkInMode,
|
checkInMode,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
|
isCheckInAllowed,
|
||||||
|
isCheckOutAllowed,
|
||||||
|
checkInAvailabilityTime,
|
||||||
|
checkOutAvailabilityTime,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -213,6 +213,8 @@ class GeofenceBloc extends Bloc<GeofenceEvent, GeofenceState>
|
|||||||
targetLat: event.targetLat,
|
targetLat: event.targetLat,
|
||||||
targetLng: event.targetLng,
|
targetLng: event.targetLng,
|
||||||
shiftId: event.shiftId,
|
shiftId: event.shiftId,
|
||||||
|
leftGeofenceTitle: event.leftGeofenceTitle,
|
||||||
|
leftGeofenceBody: event.leftGeofenceBody,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Show greeting notification using localized strings from the UI.
|
// Show greeting notification using localized strings from the UI.
|
||||||
|
|||||||
@@ -71,6 +71,8 @@ class BackgroundTrackingStarted extends GeofenceEvent {
|
|||||||
required this.targetLng,
|
required this.targetLng,
|
||||||
required this.greetingTitle,
|
required this.greetingTitle,
|
||||||
required this.greetingBody,
|
required this.greetingBody,
|
||||||
|
required this.leftGeofenceTitle,
|
||||||
|
required this.leftGeofenceBody,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// The shift ID being tracked.
|
/// The shift ID being tracked.
|
||||||
@@ -88,9 +90,24 @@ class BackgroundTrackingStarted extends GeofenceEvent {
|
|||||||
/// Localized greeting notification body passed from the UI layer.
|
/// Localized greeting notification body passed from the UI layer.
|
||||||
final String greetingBody;
|
final String greetingBody;
|
||||||
|
|
||||||
|
/// Localized title for the left-geofence notification, persisted to storage
|
||||||
|
/// for the background isolate.
|
||||||
|
final String leftGeofenceTitle;
|
||||||
|
|
||||||
|
/// Localized body for the left-geofence notification, persisted to storage
|
||||||
|
/// for the background isolate.
|
||||||
|
final String leftGeofenceBody;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props =>
|
List<Object?> get props => <Object?>[
|
||||||
<Object?>[shiftId, targetLat, targetLng, greetingTitle, greetingBody];
|
shiftId,
|
||||||
|
targetLat,
|
||||||
|
targetLng,
|
||||||
|
greetingTitle,
|
||||||
|
greetingBody,
|
||||||
|
leftGeofenceTitle,
|
||||||
|
leftGeofenceBody,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stops background tracking after clock-out.
|
/// Stops background tracking after clock-out.
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ import 'package:krow_domain/krow_domain.dart';
|
|||||||
import 'package:staff_clock_in/src/presentation/widgets/early_check_in_banner.dart';
|
import 'package:staff_clock_in/src/presentation/widgets/early_check_in_banner.dart';
|
||||||
import 'package:staff_clock_in/src/presentation/widgets/early_check_out_banner.dart';
|
import 'package:staff_clock_in/src/presentation/widgets/early_check_out_banner.dart';
|
||||||
|
|
||||||
import '../../domain/validators/clock_in_validation_context.dart';
|
|
||||||
import '../../domain/validators/validators/time_window_validator.dart';
|
|
||||||
import '../bloc/clock_in/clock_in_bloc.dart';
|
import '../bloc/clock_in/clock_in_bloc.dart';
|
||||||
import '../bloc/clock_in/clock_in_event.dart';
|
import '../bloc/clock_in/clock_in_event.dart';
|
||||||
import '../bloc/geofence/geofence_bloc.dart';
|
import '../bloc/geofence/geofence_bloc.dart';
|
||||||
@@ -37,6 +35,10 @@ class ClockInActionSection extends StatelessWidget {
|
|||||||
required this.checkInMode,
|
required this.checkInMode,
|
||||||
required this.isActionInProgress,
|
required this.isActionInProgress,
|
||||||
this.hasClockinError = false,
|
this.hasClockinError = false,
|
||||||
|
this.isCheckInAllowed = true,
|
||||||
|
this.isCheckOutAllowed = true,
|
||||||
|
this.checkInAvailabilityTime,
|
||||||
|
this.checkOutAvailabilityTime,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -65,6 +67,18 @@ class ClockInActionSection extends StatelessWidget {
|
|||||||
/// Whether the last action attempt resulted in an error.
|
/// Whether the last action attempt resulted in an error.
|
||||||
final bool hasClockinError;
|
final bool hasClockinError;
|
||||||
|
|
||||||
|
/// Whether the time window allows check-in, computed by the BLoC.
|
||||||
|
final bool isCheckInAllowed;
|
||||||
|
|
||||||
|
/// Whether the time window allows check-out, computed by the BLoC.
|
||||||
|
final bool isCheckOutAllowed;
|
||||||
|
|
||||||
|
/// Formatted earliest time when check-in becomes available, or `null`.
|
||||||
|
final String? checkInAvailabilityTime;
|
||||||
|
|
||||||
|
/// Formatted earliest time when check-out becomes available, or `null`.
|
||||||
|
final String? checkOutAvailabilityTime;
|
||||||
|
|
||||||
/// Resolves the [CheckInInteraction] for the current mode.
|
/// Resolves the [CheckInInteraction] for the current mode.
|
||||||
///
|
///
|
||||||
/// Falls back to [SwipeCheckInInteraction] if the mode is unrecognized.
|
/// Falls back to [SwipeCheckInInteraction] if the mode is unrecognized.
|
||||||
@@ -86,31 +100,30 @@ class ClockInActionSection extends StatelessWidget {
|
|||||||
|
|
||||||
/// Builds the action widget for an active (not completed) shift.
|
/// Builds the action widget for an active (not completed) shift.
|
||||||
Widget _buildActiveShiftAction(BuildContext context) {
|
Widget _buildActiveShiftAction(BuildContext context) {
|
||||||
|
final String soonLabel = Translations.of(context).staff.clock_in.soon;
|
||||||
|
|
||||||
// Show geofence status and time-based availability banners when relevant.
|
// Show geofence status and time-based availability banners when relevant.
|
||||||
if (!isCheckedIn && !_isCheckInAllowed(selectedShift!)) {
|
if (!isCheckedIn && !isCheckInAllowed) {
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
const GeofenceStatusBanner(),
|
const GeofenceStatusBanner(),
|
||||||
const SizedBox(height: UiConstants.space3),
|
const SizedBox(height: UiConstants.space3),
|
||||||
EarlyCheckInBanner(
|
EarlyCheckInBanner(
|
||||||
availabilityTime: _getAvailabilityTimeText(selectedShift!, context),
|
availabilityTime: checkInAvailabilityTime ?? soonLabel,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isCheckedIn && !_isCheckOutAllowed(selectedShift!)) {
|
if (isCheckedIn && !isCheckOutAllowed) {
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
const GeofenceStatusBanner(),
|
const GeofenceStatusBanner(),
|
||||||
const SizedBox(height: UiConstants.space3),
|
const SizedBox(height: UiConstants.space3),
|
||||||
EarlyCheckOutBanner(
|
EarlyCheckOutBanner(
|
||||||
availabilityTime: _getCheckOutAvailabilityTimeText(
|
availabilityTime: checkOutAvailabilityTime ?? soonLabel,
|
||||||
selectedShift!,
|
|
||||||
context,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -164,64 +177,12 @@ class ClockInActionSection extends StatelessWidget {
|
|||||||
notes: geofenceState.overrideNotes,
|
notes: geofenceState.overrideNotes,
|
||||||
clockInGreetingTitle: geofenceI18n.clock_in_greeting_title,
|
clockInGreetingTitle: geofenceI18n.clock_in_greeting_title,
|
||||||
clockInGreetingBody: geofenceI18n.clock_in_greeting_body,
|
clockInGreetingBody: geofenceI18n.clock_in_greeting_body,
|
||||||
|
leftGeofenceTitle: geofenceI18n.background_left_title,
|
||||||
|
leftGeofenceBody: geofenceI18n.background_left_body,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether the user is allowed to check in for the given [shift].
|
|
||||||
///
|
|
||||||
/// Delegates to [TimeWindowValidator]; returns `true` if the start time
|
|
||||||
/// cannot be parsed (don't block the user).
|
|
||||||
bool _isCheckInAllowed(Shift shift) {
|
|
||||||
final DateTime? shiftStart = DateTime.tryParse(shift.startTime);
|
|
||||||
if (shiftStart == null) return true;
|
|
||||||
|
|
||||||
final ClockInValidationContext validationContext = ClockInValidationContext(
|
|
||||||
isCheckingIn: true,
|
|
||||||
shiftStartTime: shiftStart,
|
|
||||||
);
|
|
||||||
return const TimeWindowValidator().validate(validationContext).isValid;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether the user is allowed to check out for the given [shift].
|
|
||||||
///
|
|
||||||
/// Delegates to [TimeWindowValidator]; returns `true` if the end time
|
|
||||||
/// cannot be parsed (don't block the user).
|
|
||||||
bool _isCheckOutAllowed(Shift shift) {
|
|
||||||
final DateTime? shiftEnd = DateTime.tryParse(shift.endTime);
|
|
||||||
if (shiftEnd == null) return true;
|
|
||||||
|
|
||||||
final ClockInValidationContext validationContext = ClockInValidationContext(
|
|
||||||
isCheckingIn: false,
|
|
||||||
shiftEndTime: shiftEnd,
|
|
||||||
);
|
|
||||||
return const TimeWindowValidator().validate(validationContext).isValid;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the formatted earliest check-out time for the given [shift].
|
|
||||||
///
|
|
||||||
/// Falls back to the localized "soon" label when the end time cannot
|
|
||||||
/// be parsed.
|
|
||||||
String _getCheckOutAvailabilityTimeText(Shift shift, BuildContext context) {
|
|
||||||
final DateTime? shiftEnd = DateTime.tryParse(shift.endTime.trim());
|
|
||||||
if (shiftEnd != null) {
|
|
||||||
return TimeWindowValidator.getAvailabilityTime(shiftEnd);
|
|
||||||
}
|
|
||||||
return Translations.of(context).staff.clock_in.soon;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the formatted earliest check-in time for the given [shift].
|
|
||||||
///
|
|
||||||
/// Falls back to the localized "soon" label when the start time cannot
|
|
||||||
/// be parsed.
|
|
||||||
String _getAvailabilityTimeText(Shift shift, BuildContext context) {
|
|
||||||
final DateTime? shiftStart = DateTime.tryParse(shift.startTime.trim());
|
|
||||||
if (shiftStart != null) {
|
|
||||||
return TimeWindowValidator.getAvailabilityTime(shiftStart);
|
|
||||||
}
|
|
||||||
return Translations.of(context).staff.clock_in.soon;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Triggers the check-out flow via the lunch-break confirmation dialog.
|
/// Triggers the check-out flow via the lunch-break confirmation dialog.
|
||||||
void _handleCheckOut(BuildContext context) {
|
void _handleCheckOut(BuildContext context) {
|
||||||
final TranslationsStaffClockInGeofenceEn geofenceI18n = Translations.of(
|
final TranslationsStaffClockInGeofenceEn geofenceI18n = Translations.of(
|
||||||
|
|||||||
@@ -112,6 +112,10 @@ class _ClockInBodyState extends State<ClockInBody> {
|
|||||||
isActionInProgress:
|
isActionInProgress:
|
||||||
state.status == ClockInStatus.actionInProgress,
|
state.status == ClockInStatus.actionInProgress,
|
||||||
hasClockinError: state.status == ClockInStatus.failure,
|
hasClockinError: state.status == ClockInStatus.failure,
|
||||||
|
isCheckInAllowed: state.isCheckInAllowed,
|
||||||
|
isCheckOutAllowed: state.isCheckOutAllowed,
|
||||||
|
checkInAvailabilityTime: state.checkInAvailabilityTime,
|
||||||
|
checkOutAvailabilityTime: state.checkOutAvailabilityTime,
|
||||||
),
|
),
|
||||||
|
|
||||||
// checked-in banner (only when checked in to the selected shift)
|
// checked-in banner (only when checked in to the selected shift)
|
||||||
|
|||||||
@@ -106,6 +106,10 @@ class _ShiftTimeAndRate extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final TranslationsStaffClockInEn i18n = Translations.of(
|
||||||
|
context,
|
||||||
|
).staff.clock_in;
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
@@ -114,7 +118,7 @@ class _ShiftTimeAndRate extends StatelessWidget {
|
|||||||
style: UiTypography.body3m.textSecondary,
|
style: UiTypography.body3m.textSecondary,
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'\$${shift.hourlyRate}/hr',
|
i18n.per_hr(amount: shift.hourlyRate),
|
||||||
style: UiTypography.body3m.copyWith(color: UiColors.primary),
|
style: UiTypography.body3m.copyWith(color: UiColors.primary),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user