feat(clock_in): add early check-out banner and localization support
This commit is contained in:
@@ -856,6 +856,8 @@
|
||||
"today_shift_badge": "TODAY'S SHIFT",
|
||||
"early_title": "You're early!",
|
||||
"check_in_at": "Check-in available at $time",
|
||||
"early_checkout_title": "Too early to check out",
|
||||
"check_out_at": "Check-out available at $time",
|
||||
"shift_completed": "Shift Completed!",
|
||||
"great_work": "Great work today",
|
||||
"no_shifts_today": "No confirmed shifts for today",
|
||||
|
||||
@@ -851,6 +851,8 @@
|
||||
"today_shift_badge": "TURNO DE HOY",
|
||||
"early_title": "\u00a1Ha llegado temprano!",
|
||||
"check_in_at": "Entrada disponible a las $time",
|
||||
"early_checkout_title": "Muy temprano para salir",
|
||||
"check_out_at": "Salida disponible a las $time",
|
||||
"shift_completed": "\u00a1Turno completado!",
|
||||
"great_work": "Buen trabajo hoy",
|
||||
"no_shifts_today": "No hay turnos confirmados para hoy",
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
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_out_banner.dart';
|
||||
|
||||
import '../../domain/validators/clock_in_validation_context.dart';
|
||||
import '../../domain/validators/validators/time_window_validator.dart';
|
||||
@@ -85,6 +86,7 @@ class ClockInActionSection extends StatelessWidget {
|
||||
|
||||
/// Builds the action widget for an active (not completed) shift.
|
||||
Widget _buildActiveShiftAction(BuildContext context) {
|
||||
// Show geofence status and time-based availability banners when relevant.
|
||||
if (!isCheckedIn && !_isCheckInAllowed(selectedShift!)) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -92,7 +94,20 @@ class ClockInActionSection extends StatelessWidget {
|
||||
const GeofenceStatusBanner(),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
EarlyCheckInBanner(
|
||||
availabilityTime: _getAvailabilityTimeText(
|
||||
availabilityTime: _getAvailabilityTimeText(selectedShift!, context),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
if (isCheckedIn && !_isCheckOutAllowed(selectedShift!)) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
const GeofenceStatusBanner(),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
EarlyCheckOutBanner(
|
||||
availabilityTime: _getCheckOutAvailabilityTimeText(
|
||||
selectedShift!,
|
||||
context,
|
||||
),
|
||||
@@ -139,8 +154,9 @@ class ClockInActionSection extends StatelessWidget {
|
||||
final GeofenceState geofenceState = ReadContext(
|
||||
context,
|
||||
).read<GeofenceBloc>().state;
|
||||
final TranslationsStaffClockInGeofenceEn geofenceI18n =
|
||||
Translations.of(context).staff.clock_in.geofence;
|
||||
final TranslationsStaffClockInGeofenceEn geofenceI18n = Translations.of(
|
||||
context,
|
||||
).staff.clock_in.geofence;
|
||||
|
||||
ReadContext(context).read<ClockInBloc>().add(
|
||||
CheckInRequested(
|
||||
@@ -167,6 +183,33 @@ class ClockInActionSection extends StatelessWidget {
|
||||
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
|
||||
@@ -181,8 +224,9 @@ class ClockInActionSection extends StatelessWidget {
|
||||
|
||||
/// Triggers the check-out flow via the lunch-break confirmation dialog.
|
||||
void _handleCheckOut(BuildContext context) {
|
||||
final TranslationsStaffClockInGeofenceEn geofenceI18n =
|
||||
Translations.of(context).staff.clock_in.geofence;
|
||||
final TranslationsStaffClockInGeofenceEn geofenceI18n = Translations.of(
|
||||
context,
|
||||
).staff.clock_in.geofence;
|
||||
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Banner shown when the user tries to check out too early.
|
||||
///
|
||||
/// Displays a clock icon and a message indicating when check-out
|
||||
/// will become available.
|
||||
class EarlyCheckOutBanner extends StatelessWidget {
|
||||
/// Creates an early check-out banner.
|
||||
const EarlyCheckOutBanner({
|
||||
required this.availabilityTime,
|
||||
super.key,
|
||||
});
|
||||
|
||||
/// Formatted time string when check-out becomes available (e.g. "4:45 PM").
|
||||
final String availabilityTime;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final TranslationsStaffClockInEn i18n = Translations.of(
|
||||
context,
|
||||
).staff.clock_in;
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(UiConstants.space6),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.bgSecondary,
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
const Icon(UiIcons.clock, size: 48, color: UiColors.iconThird),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
Text(
|
||||
i18n.early_checkout_title,
|
||||
style: UiTypography.body1m.textSecondary,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space1),
|
||||
Text(
|
||||
i18n.check_out_at(time: availabilityTime),
|
||||
style: UiTypography.body2r.textSecondary,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user