feat(clock_in): add early check-out banner and localization support

This commit is contained in:
Achintha Isuru
2026-03-14 20:36:35 -04:00
parent f6de07fc25
commit e02de1fb68
4 changed files with 106 additions and 8 deletions

View File

@@ -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",

View File

@@ -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",

View File

@@ -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,

View File

@@ -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,
),
],
),
);
}
}