refactor: centralize data connect error handling and resolve build issues across applications

This commit addresses several critical issues across the mobile monorepo:

1. Centralized Error Handling: Integrated DataErrorHandler mixin into all repository implementations, ensuring consistent mapping of Data Connect exceptions to domain AppExceptions.
2. Build Stabilization: Fixed numerous type mismatches, parameter signature errors in widgets (e.g., google_places_flutter itemBuilder), and naming conflicts (StaffSession, FirebaseAuth).
3. Code Quality: Applied 'dart fix' across all modified packages and manually cleared debug print statements and UI clutter.
4. Mono-repo alignment: Standardized Data Connect usage and aliasing ('dc.') for better maintainability.

Signed-off-by: Suriya <suriya@tenext.in>
This commit is contained in:
2026-02-06 13:28:57 +05:30
parent e0636e46a3
commit 5e7bf0d5c0
150 changed files with 1506 additions and 2547 deletions

View File

@@ -1,22 +1,24 @@
import 'package:firebase_data_connect/firebase_data_connect.dart';
import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc;
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 Map<String, String> _shiftToApplicationId = {};
String? _activeApplicationId;
class ClockInRepositoryImpl
with dc.DataErrorHandler
implements ClockInRepositoryInterface {
ClockInRepositoryImpl({
required dc.ExampleConnector dataConnect,
}) : _dataConnect = dataConnect;
final dc.ExampleConnector _dataConnect;
final Map<String, String> _shiftToApplicationId = <String, String>{};
String? _activeApplicationId;
Future<String> _getStaffId() async {
final StaffSession? session = StaffSessionStore.instance.session;
final dc.StaffSession? session = dc.StaffSessionStore.instance.session;
final String? staffId = session?.staff?.id;
if (staffId != null && staffId.isNotEmpty) {
return staffId;
@@ -24,7 +26,7 @@ class ClockInRepositoryImpl implements ClockInRepositoryInterface {
throw Exception('Staff session not found');
}
/// Helper to convert Data Connect Timestamp to DateTime
/// Helper to convert Data Connect fdc.Timestamp to DateTime
DateTime? _toDateTime(dynamic t) {
if (t == null) return null;
DateTime? dt;
@@ -34,7 +36,7 @@ class ClockInRepositoryImpl implements ClockInRepositoryInterface {
dt = DateTime.tryParse(t);
} else {
try {
if (t is Timestamp) {
if (t is fdc.Timestamp) {
dt = t.toDateTime();
}
} catch (_) {}
@@ -46,9 +48,7 @@ class ClockInRepositoryImpl implements ClockInRepositoryInterface {
} catch (_) {}
try {
if (dt == null) {
dt = DateTime.tryParse(t.toString());
}
dt ??= DateTime.tryParse(t.toString());
} catch (_) {}
}
@@ -58,13 +58,13 @@ class ClockInRepositoryImpl implements ClockInRepositoryInterface {
return null;
}
/// Helper to create Timestamp from DateTime
Timestamp _fromDateTime(DateTime d) {
// Assuming Timestamp.fromJson takes an ISO string
return Timestamp.fromJson(d.toUtc().toIso8601String());
/// Helper to create fdc.Timestamp from DateTime
fdc.Timestamp _fromDateTime(DateTime d) {
// Assuming fdc.Timestamp.fromJson takes an ISO string
return fdc.Timestamp.fromJson(d.toUtc().toIso8601String());
}
({Timestamp start, Timestamp end}) _utcDayRange(DateTime localDay) {
({fdc.Timestamp start, fdc.Timestamp end}) _utcDayRange(DateTime localDay) {
final DateTime dayStartUtc = DateTime.utc(
localDay.year,
localDay.month,
@@ -91,22 +91,24 @@ class ClockInRepositoryImpl implements ClockInRepositoryInterface {
String staffId,
) async {
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();
final ({fdc.Timestamp start, fdc.Timestamp end}) range = _utcDayRange(now);
final fdc.QueryResult<dc.GetApplicationsByStaffIdData,
dc.GetApplicationsByStaffIdVariables> result = await executeProtected(
() => _dataConnect
.getApplicationsByStaffId(staffId: staffId)
.dayStart(range.start)
.dayEnd(range.end)
.execute(),
);
final apps = result.data.applications;
if (apps.isEmpty) return const [];
final List<dc.GetApplicationsByStaffIdApplications> apps = result.data.applications;
if (apps.isEmpty) return const <dc.GetApplicationsByStaffIdApplications>[];
_shiftToApplicationId
..clear()
..addEntries(apps.map((app) => MapEntry(app.shiftId, app.id)));
..addEntries(apps.map((dc.GetApplicationsByStaffIdApplications app) => MapEntry(app.shiftId, app.id)));
apps.sort((a, b) {
apps.sort((dc.GetApplicationsByStaffIdApplications a, dc.GetApplicationsByStaffIdApplications b) {
final DateTime? aTime =
_toDateTime(a.shift.startTime) ?? _toDateTime(a.shift.date);
final DateTime? bTime =
@@ -122,28 +124,17 @@ class ClockInRepositoryImpl implements ClockInRepositoryInterface {
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;
}
}
@override
Future<List<Shift>> getTodaysShifts() async {
final String staffId = await _getStaffId();
final List<dc.GetApplicationsByStaffIdApplications> apps =
await _getTodaysApplications(staffId);
if (apps.isEmpty) return const [];
if (apps.isEmpty) return const <Shift>[];
final List<Shift> shifts = [];
for (final app in apps) {
final List<Shift> shifts = <Shift>[];
for (final dc.GetApplicationsByStaffIdApplications app in apps) {
final dc.GetApplicationsByStaffIdApplicationsShift shift = app.shift;
final DateTime? startDt = _toDateTime(app.shiftRole.startTime);
final DateTime? endDt = _toDateTime(app.shiftRole.endTime);
@@ -189,7 +180,7 @@ class ClockInRepositoryImpl implements ClockInRepositoryInterface {
}
dc.GetApplicationsByStaffIdApplications? activeApp;
for (final app in apps) {
for (final dc.GetApplicationsByStaffIdApplications app in apps) {
if (app.checkInTime != null && app.checkOutTime == null) {
if (activeApp == null) {
activeApp = app;
@@ -209,7 +200,7 @@ class ClockInRepositoryImpl implements ClockInRepositoryInterface {
}
_activeApplicationId = activeApp.id;
print('Active check-in appId=$_activeApplicationId');
return AttendanceStatus(
isCheckedIn: true,
checkInTime: _toDateTime(activeApp.checkInTime),
@@ -227,39 +218,22 @@ class ClockInRepositoryImpl implements ClockInRepositoryInterface {
dc.GetApplicationsByStaffIdApplications? app;
if (cachedAppId != null) {
try {
final apps = await _getTodaysApplications(staffId);
app = apps.firstWhere((a) => a.id == cachedAppId);
final List<dc.GetApplicationsByStaffIdApplications> apps = await _getTodaysApplications(staffId);
app = apps.firstWhere((dc.GetApplicationsByStaffIdApplications a) => a.id == cachedAppId);
} catch (_) {}
}
app ??= (await _getTodaysApplications(staffId))
.firstWhere((a) => a.shiftId == shiftId);
.firstWhere((dc.GetApplicationsByStaffIdApplications a) => a.shiftId == shiftId);
final Timestamp checkInTs = _fromDateTime(DateTime.now());
print(
'ClockIn request: appId=${app.id} shiftId=$shiftId '
'checkInTime=${checkInTs.toJson()}',
);
try {
await _dataConnect
.updateApplicationStatus(
id: app.id,
)
.checkInTime(checkInTs)
.execute();
_activeApplicationId = app.id;
} catch (e) {
print('ClockIn updateApplicationStatus error: $e');
print('ClockIn error type: ${e.runtimeType}');
try {
final dynamic err = e;
final dynamic details =
err.details ?? err.response ?? err.data ?? err.message;
if (details != null) {
print('ClockIn error details: $details');
}
} catch (_) {}
rethrow;
}
final fdc.Timestamp checkInTs = _fromDateTime(DateTime.now());
await executeProtected(() => _dataConnect
.updateApplicationStatus(
id: app!.id,
)
.checkInTime(checkInTs)
.execute());
_activeApplicationId = app.id;
return getAttendanceStatus();
}
@@ -270,25 +244,18 @@ class ClockInRepositoryImpl implements ClockInRepositoryInterface {
int? breakTimeMinutes,
String? applicationId,
}) async {
final String staffId = await _getStaffId();
await _getStaffId(); // Validate session
print(
'ClockOut request: applicationId=$applicationId '
'activeApplicationId=$_activeApplicationId',
);
final String? targetAppId = applicationId ?? _activeApplicationId;
if (targetAppId == null || targetAppId.isEmpty) {
throw Exception('No active application id for checkout');
}
final appResult = await _dataConnect
final fdc.QueryResult<dc.GetApplicationByIdData, dc.GetApplicationByIdVariables> appResult = await executeProtected(() => _dataConnect
.getApplicationById(id: targetAppId)
.execute();
final app = appResult.data.application;
print(
'ClockOut getApplicationById: id=${app?.id} '
'checkIn=${app?.checkInTime?.toJson()} '
'checkOut=${app?.checkOutTime?.toJson()}',
);
.execute());
final dc.GetApplicationByIdApplication? app = appResult.data.application;
if (app == null) {
throw Exception('Application not found for checkout');
}
@@ -296,12 +263,12 @@ class ClockInRepositoryImpl implements ClockInRepositoryInterface {
throw Exception('No active shift found to clock out');
}
await _dataConnect
await executeProtected(() => _dataConnect
.updateApplicationStatus(
id: targetAppId,
)
.checkOutTime(_fromDateTime(DateTime.now()))
.execute();
.execute());
return getAttendanceStatus();
}

View File

@@ -2,18 +2,18 @@ import 'package:krow_core/core.dart';
/// Represents the arguments required for the [ClockInUseCase].
class ClockInArguments extends UseCaseArgument {
/// The ID of the shift to clock in to.
final String shiftId;
/// Optional notes provided by the user during clock-in.
final String? notes;
/// Creates a [ClockInArguments] instance.
const ClockInArguments({
required this.shiftId,
this.notes,
});
/// The ID of the shift to clock in to.
final String shiftId;
/// Optional notes provided by the user during clock-in.
final String? notes;
@override
List<Object?> get props => [shiftId, notes];
List<Object?> get props => <Object?>[shiftId, notes];
}

View File

@@ -2,6 +2,13 @@ import 'package:krow_core/core.dart';
/// Represents the arguments required for the [ClockOutUseCase].
class ClockOutArguments extends UseCaseArgument {
/// Creates a [ClockOutArguments] instance.
const ClockOutArguments({
this.notes,
this.breakTimeMinutes,
this.applicationId,
});
/// Optional notes provided by the user during clock-out.
final String? notes;
@@ -11,13 +18,6 @@ class ClockOutArguments extends UseCaseArgument {
/// Optional application id for checkout.
final String? applicationId;
/// Creates a [ClockOutArguments] instance.
const ClockOutArguments({
this.notes,
this.breakTimeMinutes,
this.applicationId,
});
@override
List<Object?> get props => [notes, breakTimeMinutes, applicationId];
List<Object?> get props => <Object?>[notes, breakTimeMinutes, applicationId];
}

View File

@@ -5,9 +5,9 @@ import '../arguments/clock_in_arguments.dart';
/// Use case for clocking in a user.
class ClockInUseCase implements UseCase<ClockInArguments, AttendanceStatus> {
final ClockInRepositoryInterface _repository;
ClockInUseCase(this._repository);
final ClockInRepositoryInterface _repository;
@override
Future<AttendanceStatus> call(ClockInArguments arguments) {

View File

@@ -5,9 +5,9 @@ import '../arguments/clock_out_arguments.dart';
/// Use case for clocking out a user.
class ClockOutUseCase implements UseCase<ClockOutArguments, AttendanceStatus> {
final ClockInRepositoryInterface _repository;
ClockOutUseCase(this._repository);
final ClockInRepositoryInterface _repository;
@override
Future<AttendanceStatus> call(ClockOutArguments arguments) {

View File

@@ -4,9 +4,9 @@ import '../repositories/clock_in_repository_interface.dart';
/// Use case for getting the current attendance status (check-in/out times).
class GetAttendanceStatusUseCase implements NoInputUseCase<AttendanceStatus> {
final ClockInRepositoryInterface _repository;
GetAttendanceStatusUseCase(this._repository);
final ClockInRepositoryInterface _repository;
@override
Future<AttendanceStatus> call() {

View File

@@ -4,9 +4,9 @@ import '../repositories/clock_in_repository_interface.dart';
/// Use case for retrieving the user's scheduled shifts for today.
class GetTodaysShiftUseCase implements NoInputUseCase<List<Shift>> {
final ClockInRepositoryInterface _repository;
GetTodaysShiftUseCase(this._repository);
final ClockInRepositoryInterface _repository;
@override
Future<List<Shift>> call() {

View File

@@ -11,13 +11,6 @@ import 'clock_in_event.dart';
import 'clock_in_state.dart';
class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
final GetTodaysShiftUseCase _getTodaysShift;
final GetAttendanceStatusUseCase _getAttendanceStatus;
final ClockInUseCase _clockIn;
final ClockOutUseCase _clockOut;
// Mock Venue Location (e.g., Grand Hotel, NYC)
static const double allowedRadiusMeters = 500;
ClockInBloc({
required GetTodaysShiftUseCase getTodaysShift,
@@ -41,6 +34,13 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
add(ClockInPageLoaded());
}
final GetTodaysShiftUseCase _getTodaysShift;
final GetAttendanceStatusUseCase _getAttendanceStatus;
final ClockInUseCase _clockIn;
final ClockOutUseCase _clockOut;
// Mock Venue Location (e.g., Grand Hotel, NYC)
static const double allowedRadiusMeters = 500;
Future<void> _onLoaded(
ClockInPageLoaded event,
@@ -48,8 +48,8 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
) async {
emit(state.copyWith(status: ClockInStatus.loading));
try {
final shifts = await _getTodaysShift();
final status = await _getAttendanceStatus();
final List<Shift> shifts = await _getTodaysShift();
final AttendanceStatus 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
@@ -58,7 +58,7 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
if (status.activeShiftId != null) {
try {
selectedShift =
shifts.firstWhere((s) => s.id == status.activeShiftId);
shifts.firstWhere((Shift s) => s.id == status.activeShiftId);
} catch (_) {}
}
selectedShift ??= shifts.last;
@@ -93,7 +93,7 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
permission = await Geolocator.requestPermission();
}
final hasConsent = permission == LocationPermission.always || permission == LocationPermission.whileInUse;
final bool hasConsent = permission == LocationPermission.always || permission == LocationPermission.whileInUse;
emit(state.copyWith(hasLocationConsent: hasConsent));
@@ -105,9 +105,9 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
}
}
void _startLocationUpdates() async {
Future<void> _startLocationUpdates() async {
try {
final position = await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high);
final Position position = await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high);
double distance = 0;
bool isVerified = false; // Require location match by default if shift has location
@@ -195,7 +195,7 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
emit(state.copyWith(status: ClockInStatus.actionInProgress));
try {
final newStatus = await _clockIn(
final AttendanceStatus newStatus = await _clockIn(
ClockInArguments(shiftId: event.shiftId, notes: event.notes),
);
emit(state.copyWith(
@@ -216,7 +216,7 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
) async {
emit(state.copyWith(status: ClockInStatus.actionInProgress));
try {
final newStatus = await _clockOut(
final AttendanceStatus newStatus = await _clockOut(
ClockOutArguments(
notes: event.notes,
breakTimeMinutes: 0, // Should be passed from event if supported

View File

@@ -6,75 +6,75 @@ abstract class ClockInEvent extends Equatable {
const ClockInEvent();
@override
List<Object?> get props => [];
List<Object?> get props => <Object?>[];
}
class ClockInPageLoaded extends ClockInEvent {}
class ShiftSelected extends ClockInEvent {
final Shift shift;
const ShiftSelected(this.shift);
final Shift shift;
@override
List<Object?> get props => [shift];
List<Object?> get props => <Object?>[shift];
}
class DateSelected extends ClockInEvent {
final DateTime date;
const DateSelected(this.date);
final DateTime date;
@override
List<Object?> get props => [date];
List<Object?> get props => <Object?>[date];
}
class CheckInRequested extends ClockInEvent {
const CheckInRequested({required this.shiftId, this.notes});
final String shiftId;
final String? notes;
const CheckInRequested({required this.shiftId, this.notes});
@override
List<Object?> get props => [shiftId, notes];
List<Object?> get props => <Object?>[shiftId, notes];
}
class CheckOutRequested extends ClockInEvent {
const CheckOutRequested({this.notes, this.breakTimeMinutes});
final String? notes;
final int? breakTimeMinutes;
const CheckOutRequested({this.notes, this.breakTimeMinutes});
@override
List<Object?> get props => [notes, breakTimeMinutes];
List<Object?> get props => <Object?>[notes, breakTimeMinutes];
}
class CheckInModeChanged extends ClockInEvent {
final String mode;
const CheckInModeChanged(this.mode);
final String mode;
@override
List<Object?> get props => [mode];
List<Object?> get props => <Object?>[mode];
}
class CommuteModeToggled extends ClockInEvent {
final bool isEnabled;
const CommuteModeToggled(this.isEnabled);
final bool isEnabled;
@override
List<Object?> get props => [isEnabled];
List<Object?> get props => <Object?>[isEnabled];
}
class RequestLocationPermission extends ClockInEvent {}
class LocationUpdated extends ClockInEvent {
const LocationUpdated({required this.position, required this.distance, required this.isVerified});
final Position position;
final double distance;
final bool isVerified;
const LocationUpdated({required this.position, required this.distance, required this.isVerified});
@override
List<Object?> get props => [position, distance, isVerified];
List<Object?> get props => <Object?>[position, distance, isVerified];
}

View File

@@ -6,6 +6,22 @@ import 'package:geolocator/geolocator.dart';
enum ClockInStatus { initial, loading, success, failure, actionInProgress }
class ClockInState extends Equatable {
const ClockInState({
this.status = ClockInStatus.initial,
this.todayShifts = const <Shift>[],
this.selectedShift,
this.attendance = const AttendanceStatus(),
required this.selectedDate,
this.checkInMode = 'swipe',
this.errorMessage,
this.currentLocation,
this.distanceFromVenue,
this.isLocationVerified = false,
this.isCommuteModeOn = false,
this.hasLocationConsent = false,
this.etaMinutes,
});
final ClockInStatus status;
final List<Shift> todayShifts;
final Shift? selectedShift;
@@ -21,22 +37,6 @@ class ClockInState extends Equatable {
final bool hasLocationConsent;
final int? etaMinutes;
const ClockInState({
this.status = ClockInStatus.initial,
this.todayShifts = const [],
this.selectedShift,
this.attendance = const AttendanceStatus(),
required this.selectedDate,
this.checkInMode = 'swipe',
this.errorMessage,
this.currentLocation,
this.distanceFromVenue,
this.isLocationVerified = false,
this.isCommuteModeOn = false,
this.hasLocationConsent = false,
this.etaMinutes,
});
ClockInState copyWith({
ClockInStatus? status,
List<Shift>? todayShifts,
@@ -70,7 +70,7 @@ class ClockInState extends Equatable {
}
@override
List<Object?> get props => [
List<Object?> get props => <Object?>[
status,
todayShifts,
selectedShift,

View File

@@ -1,17 +1,9 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:geolocator/geolocator.dart';
import 'package:permission_handler/permission_handler.dart';
// --- State ---
class ClockInState extends Equatable {
final bool isLoading;
final bool isLocationVerified;
final String? error;
final Position? currentLocation;
final double? distanceFromVenue;
final bool isClockedIn;
final DateTime? clockInTime;
const ClockInState({
this.isLoading = false,
@@ -22,6 +14,13 @@ class ClockInState extends Equatable {
this.isClockedIn = false,
this.clockInTime,
});
final bool isLoading;
final bool isLocationVerified;
final String? error;
final Position? currentLocation;
final double? distanceFromVenue;
final bool isClockedIn;
final DateTime? clockInTime;
ClockInState copyWith({
bool? isLoading,
@@ -44,7 +43,7 @@ class ClockInState extends Equatable {
}
@override
List<Object?> get props => [
List<Object?> get props => <Object?>[
isLoading,
isLocationVerified,
error,
@@ -56,13 +55,13 @@ class ClockInState extends Equatable {
}
// --- Cubit ---
class ClockInCubit extends Cubit<ClockInState> {
class ClockInCubit extends Cubit<ClockInState> { // 500m radius
ClockInCubit() : super(const ClockInState());
// Mock Venue Location (e.g., Grand Hotel, NYC)
static const double venueLat = 40.7128;
static const double venueLng = -74.0060;
static const double allowedRadiusMeters = 500; // 500m radius
ClockInCubit() : super(const ClockInState());
static const double allowedRadiusMeters = 500;
Future<void> checkLocationPermission() async {
emit(state.copyWith(isLoading: true, error: null));
@@ -95,18 +94,18 @@ class ClockInCubit extends Cubit<ClockInState> {
Future<void> _getCurrentLocation() async {
try {
final position = await Geolocator.getCurrentPosition(
final Position position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high,
);
final distance = Geolocator.distanceBetween(
final double distance = Geolocator.distanceBetween(
position.latitude,
position.longitude,
venueLat,
venueLng,
);
final isWithinRadius = distance <= allowedRadiusMeters;
final bool isWithinRadius = distance <= allowedRadiusMeters;
emit(state.copyWith(
isLoading: false,

View File

@@ -36,7 +36,7 @@ class _ClockInPageState extends State<ClockInPage> {
return BlocProvider<ClockInBloc>.value(
value: _bloc,
child: BlocConsumer<ClockInBloc, ClockInState>(
listener: (context, state) {
listener: (BuildContext context, ClockInState state) {
if (state.status == ClockInStatus.failure &&
state.errorMessage != null) {
ScaffoldMessenger.of(
@@ -44,7 +44,7 @@ class _ClockInPageState extends State<ClockInPage> {
).showSnackBar(SnackBar(content: Text(state.errorMessage!)));
}
},
builder: (context, state) {
builder: (BuildContext context, ClockInState state) {
if (state.status == ClockInStatus.loading &&
state.todayShifts.isEmpty) {
return const Scaffold(
@@ -52,23 +52,23 @@ class _ClockInPageState extends State<ClockInPage> {
);
}
final todayShifts = state.todayShifts;
final selectedShift = state.selectedShift;
final activeShiftId = state.attendance.activeShiftId;
final List<Shift> todayShifts = state.todayShifts;
final Shift? selectedShift = state.selectedShift;
final String? activeShiftId = state.attendance.activeShiftId;
final bool isActiveSelected =
selectedShift != null && selectedShift.id == activeShiftId;
final checkInTime =
final DateTime? checkInTime =
isActiveSelected ? state.attendance.checkInTime : null;
final checkOutTime =
final DateTime? checkOutTime =
isActiveSelected ? state.attendance.checkOutTime : null;
final isCheckedIn =
final bool isCheckedIn =
state.attendance.isCheckedIn && isActiveSelected;
// Format times for display
final checkInStr = checkInTime != null
final String checkInStr = checkInTime != null
? DateFormat('h:mm a').format(checkInTime)
: '--:-- --';
final checkOutStr = checkOutTime != null
final String checkOutStr = checkOutTime != null
? DateFormat('h:mm a').format(checkOutTime)
: '--:-- --';
@@ -94,7 +94,7 @@ class _ClockInPageState extends State<ClockInPage> {
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
children: <Widget>[
// Commute Tracker (shows before date selector when applicable)
if (selectedShift != null)
CommuteTracker(
@@ -103,15 +103,15 @@ class _ClockInPageState extends State<ClockInPage> {
isCommuteModeOn: state.isCommuteModeOn,
distanceMeters: state.distanceFromVenue,
etaMinutes: state.etaMinutes,
onCommuteToggled: (value) {
onCommuteToggled: (bool value) {
_bloc.add(CommuteModeToggled(value));
},
),
// Date Selector
DateSelector(
selectedDate: state.selectedDate,
onSelect: (date) => _bloc.add(DateSelected(date)),
shiftDates: [
onSelect: (DateTime date) => _bloc.add(DateSelected(date)),
shiftDates: <String>[
DateFormat('yyyy-MM-dd').format(DateTime.now()),
],
),
@@ -136,7 +136,7 @@ class _ClockInPageState extends State<ClockInPage> {
Column(
children: todayShifts
.map(
(shift) => GestureDetector(
(Shift shift) => GestureDetector(
onTap: () =>
_bloc.add(ShiftSelected(shift)),
child: Container(
@@ -162,12 +162,12 @@ class _ClockInPageState extends State<ClockInPage> {
child: Row(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
children: <Widget>[
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
children: <Widget>[
Text(
shift.id ==
selectedShift?.id
@@ -208,7 +208,7 @@ class _ClockInPageState extends State<ClockInPage> {
Column(
crossAxisAlignment:
CrossAxisAlignment.end,
children: [
children: <Widget>[
Text(
"${_formatTime(shift.startTime)} - ${_formatTime(shift.endTime)}",
style: const TextStyle(
@@ -236,7 +236,7 @@ class _ClockInPageState extends State<ClockInPage> {
),
// Swipe To Check In / Checked Out State / No Shift State
if (selectedShift != null && checkOutTime == null) ...[
if (selectedShift != null && checkOutTime == null) ...<Widget>[
if (!isCheckedIn &&
!_isCheckInAllowed(selectedShift))
Container(
@@ -247,7 +247,7 @@ class _ClockInPageState extends State<ClockInPage> {
borderRadius: BorderRadius.circular(16),
),
child: Column(
children: [
children: <Widget>[
const Icon(
LucideIcons.clock,
size: 48,
@@ -296,7 +296,7 @@ class _ClockInPageState extends State<ClockInPage> {
onCheckOut: () {
showDialog(
context: context,
builder: (context) => LunchBreakDialog(
builder: (BuildContext context) => LunchBreakDialog(
onComplete: () {
Navigator.of(
context,
@@ -308,7 +308,7 @@ class _ClockInPageState extends State<ClockInPage> {
},
),
] else if (selectedShift != null &&
checkOutTime != null) ...[
checkOutTime != null) ...<Widget>[
// Shift Completed State
Container(
padding: const EdgeInsets.all(24),
@@ -320,7 +320,7 @@ class _ClockInPageState extends State<ClockInPage> {
), // emerald-200
),
child: Column(
children: [
children: <Widget>[
Container(
width: 48,
height: 48,
@@ -354,7 +354,7 @@ class _ClockInPageState extends State<ClockInPage> {
],
),
),
] else ...[
] else ...<Widget>[
// No Shift State
Container(
width: double.infinity,
@@ -364,8 +364,8 @@ class _ClockInPageState extends State<ClockInPage> {
borderRadius: BorderRadius.circular(16),
),
child: const Column(
children: [
const Text(
children: <Widget>[
Text(
"No confirmed shifts for today",
style: TextStyle(
fontSize: 16,
@@ -374,8 +374,8 @@ class _ClockInPageState extends State<ClockInPage> {
),
textAlign: TextAlign.center,
),
const SizedBox(height: 4),
const Text(
SizedBox(height: 4),
Text(
"Accept a shift to clock in",
style: TextStyle(
fontSize: 14,
@@ -389,7 +389,7 @@ class _ClockInPageState extends State<ClockInPage> {
],
// Checked In Banner
if (isCheckedIn && checkInTime != null) ...[
if (isCheckedIn && checkInTime != null) ...<Widget>[
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(12),
@@ -403,11 +403,11 @@ class _ClockInPageState extends State<ClockInPage> {
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
children: <Widget>[
Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
children: <Widget>[
const Text(
"Checked in at",
style: TextStyle(
@@ -468,7 +468,7 @@ class _ClockInPageState extends State<ClockInPage> {
String value,
String currentMode,
) {
final isSelected = currentMode == value;
final bool isSelected = currentMode == value;
return Expanded(
child: GestureDetector(
onTap: () => _bloc.add(CheckInModeChanged(value)),
@@ -478,18 +478,18 @@ class _ClockInPageState extends State<ClockInPage> {
color: isSelected ? Colors.white : Colors.transparent,
borderRadius: BorderRadius.circular(8),
boxShadow: isSelected
? [
? <BoxShadow>[
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 2,
offset: const Offset(0, 1),
),
]
: [],
: <BoxShadow>[],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
children: <Widget>[
Icon(
icon,
size: 16,
@@ -520,12 +520,12 @@ class _ClockInPageState extends State<ClockInPage> {
barrierDismissible: false,
builder: (BuildContext dialogContext) {
return StatefulBuilder(
builder: (context, setState) {
builder: (BuildContext context, setState) {
return AlertDialog(
title: Text(scanned ? 'Tag Scanned!' : 'Scan NFC Tag'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
children: <Widget>[
Container(
width: 96,
height: 96,
@@ -559,7 +559,7 @@ class _ClockInPageState extends State<ClockInPage> {
textAlign: TextAlign.center,
style: TextStyle(fontSize: 14, color: Colors.grey.shade600),
),
if (!scanned) ...[
if (!scanned) ...<Widget>[
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
@@ -620,14 +620,14 @@ class _ClockInPageState extends State<ClockInPage> {
if (timeStr.isEmpty) return '';
try {
// Try parsing as ISO string first (which contains date)
final dt = DateTime.parse(timeStr);
final DateTime dt = DateTime.parse(timeStr);
return DateFormat('h:mm a').format(dt);
} catch (_) {
// Fallback for strict "HH:mm" or "HH:mm:ss" strings
try {
final parts = timeStr.split(':');
final List<String> parts = timeStr.split(':');
if (parts.length >= 2) {
final dt = DateTime(2022, 1, 1, int.parse(parts[0]), int.parse(parts[1]));
final DateTime dt = DateTime(2022, 1, 1, int.parse(parts[0]), int.parse(parts[1]));
return DateFormat('h:mm a').format(dt);
}
return timeStr;
@@ -638,12 +638,11 @@ class _ClockInPageState extends State<ClockInPage> {
}
bool _isCheckInAllowed(Shift shift) {
if (shift == null) return false;
try {
// Parse shift date (e.g. 2024-01-31T09:00:00)
// The Shift entity has 'date' which is the start DateTime string
final shiftStart = DateTime.parse(shift.startTime);
final windowStart = shiftStart.subtract(const Duration(minutes: 15));
final DateTime shiftStart = DateTime.parse(shift.startTime);
final DateTime windowStart = shiftStart.subtract(const Duration(minutes: 15));
return DateTime.now().isAfter(windowStart);
} catch (e) {
// Fallback: If parsing fails, allow check in to avoid blocking.
@@ -652,10 +651,9 @@ class _ClockInPageState extends State<ClockInPage> {
}
String _getCheckInAvailabilityTime(Shift shift) {
if (shift == null) return '';
try {
final shiftStart = DateTime.parse(shift.startTime.trim());
final windowStart = shiftStart.subtract(const Duration(minutes: 15));
final DateTime shiftStart = DateTime.parse(shift.startTime.trim());
final DateTime windowStart = shiftStart.subtract(const Duration(minutes: 15));
return DateFormat('h:mm a').format(windowStart);
} catch (e) {
return 'soon';

View File

@@ -4,11 +4,6 @@ import 'package:lucide_icons/lucide_icons.dart';
enum AttendanceType { checkin, checkout, breaks, days }
class AttendanceCard extends StatelessWidget {
final AttendanceType type;
final String title;
final String value;
final String subtitle;
final String? scheduledTime;
const AttendanceCard({
super.key,
@@ -18,10 +13,15 @@ class AttendanceCard extends StatelessWidget {
required this.subtitle,
this.scheduledTime,
});
final AttendanceType type;
final String title;
final String value;
final String subtitle;
final String? scheduledTime;
@override
Widget build(BuildContext context) {
final styles = _getStyles(type);
final _AttendanceStyle styles = _getStyles(type);
return Container(
padding: const EdgeInsets.all(12),
@@ -29,7 +29,7 @@ class AttendanceCard extends StatelessWidget {
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.grey.shade100),
boxShadow: [
boxShadow: <BoxShadow>[
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 2,
@@ -40,7 +40,7 @@ class AttendanceCard extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
children: <Widget>[
Container(
width: 32,
height: 32,
@@ -72,7 +72,7 @@ class AttendanceCard extends StatelessWidget {
),
),
),
if (scheduledTime != null) ...[
if (scheduledTime != null) ...<Widget>[
const SizedBox(height: 2),
Text(
"Scheduled: $scheduledTime",
@@ -123,13 +123,13 @@ class AttendanceCard extends StatelessWidget {
}
class _AttendanceStyle {
final IconData icon;
final Color bgColor;
final Color iconColor;
_AttendanceStyle({
required this.icon,
required this.bgColor,
required this.iconColor,
});
final IconData icon;
final Color bgColor;
final Color iconColor;
}

View File

@@ -12,13 +12,6 @@ enum CommuteMode {
}
class CommuteTracker extends StatefulWidget {
final Shift? shift;
final Function(CommuteMode)? onModeChange;
final ValueChanged<bool>? onCommuteToggled;
final bool hasLocationConsent;
final bool isCommuteModeOn;
final double? distanceMeters;
final int? etaMinutes;
const CommuteTracker({
super.key,
@@ -30,6 +23,13 @@ class CommuteTracker extends StatefulWidget {
this.distanceMeters,
this.etaMinutes,
});
final Shift? shift;
final Function(CommuteMode)? onModeChange;
final ValueChanged<bool>? onCommuteToggled;
final bool hasLocationConsent;
final bool isCommuteModeOn;
final double? distanceMeters;
final int? etaMinutes;
@override
State<CommuteTracker> createState() => _CommuteTrackerState();
@@ -65,7 +65,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
if (widget.shift == null) return CommuteMode.lockedNoShift;
// For demo purposes, check if we're within 24 hours of shift
final now = DateTime.now();
final DateTime now = DateTime.now();
DateTime shiftStart;
try {
// Try parsing startTime as full datetime first
@@ -81,8 +81,8 @@ class _CommuteTrackerState extends State<CommuteTracker> {
);
}
}
final hoursUntilShift = shiftStart.difference(now).inHours;
final inCommuteWindow = hoursUntilShift <= 24 && hoursUntilShift >= 0;
final int hoursUntilShift = shiftStart.difference(now).inHours;
final bool inCommuteWindow = hoursUntilShift <= 24 && hoursUntilShift >= 0;
if (_localIsCommuteOn) {
// Check if arrived (mock: if distance < 200m)
@@ -102,7 +102,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
}
String _formatDistance(double meters) {
final miles = meters / 1609.34;
final double miles = meters / 1609.34;
return miles < 0.1
? '${meters.round()} m'
: '${miles.toStringAsFixed(1)} mi';
@@ -110,7 +110,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
int _getMinutesUntilShift() {
if (widget.shift == null) return 0;
final now = DateTime.now();
final DateTime now = DateTime.now();
DateTime shiftStart;
try {
// Try parsing startTime as full datetime first
@@ -131,7 +131,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
@override
Widget build(BuildContext context) {
final mode = _getAppMode();
final CommuteMode mode = _getAppMode();
// Notify parent of mode change
WidgetsBinding.instance.addPostFrameCallback((_) {
@@ -164,13 +164,13 @@ class _CommuteTrackerState extends State<CommuteTracker> {
gradient: const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
colors: <Color>[
Color(0xFFEFF6FF), // blue-50
Color(0xFFECFEFF), // cyan-50
],
),
borderRadius: BorderRadius.circular(12),
boxShadow: [
boxShadow: <BoxShadow>[
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 2,
@@ -180,10 +180,10 @@ class _CommuteTrackerState extends State<CommuteTracker> {
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
children: <Widget>[
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
children: <Widget>[
Container(
width: 32,
height: 32,
@@ -198,11 +198,11 @@ class _CommuteTrackerState extends State<CommuteTracker> {
),
),
const SizedBox(width: 12),
Expanded(
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
children: <Widget>[
Text(
'Enable Commute Tracking?',
style: TextStyle(
fontSize: 14,
@@ -210,7 +210,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
color: Color(0xFF0F172A), // slate-900
),
),
const SizedBox(height: 4),
SizedBox(height: 4),
Text(
'Share location 1hr before shift so your manager can see you\'re on the way.',
style: TextStyle(
@@ -225,7 +225,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
),
const SizedBox(height: 12),
Row(
children: [
children: <Widget>[
Expanded(
child: OutlinedButton(
onPressed: () {
@@ -268,7 +268,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
boxShadow: <BoxShadow>[
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 2,
@@ -277,7 +277,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
],
),
child: Row(
children: [
children: <Widget>[
Container(
width: 32,
height: 32,
@@ -295,9 +295,9 @@ class _CommuteTrackerState extends State<CommuteTracker> {
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
children: <Widget>[
Row(
children: [
children: <Widget>[
const Text(
'On My Way',
style: TextStyle(
@@ -308,7 +308,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
),
const SizedBox(width: 8),
Row(
children: [
children: <Widget>[
const Icon(
LucideIcons.clock,
size: 12,
@@ -338,11 +338,11 @@ class _CommuteTrackerState extends State<CommuteTracker> {
),
Switch(
value: _localIsCommuteOn,
onChanged: (value) {
onChanged: (bool value) {
setState(() => _localIsCommuteOn = value);
widget.onCommuteToggled?.call(value);
},
activeColor: AppColors.krowBlue,
activeThumbColor: AppColors.krowBlue,
),
],
),
@@ -356,7 +356,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
colors: <Color>[
Color(0xFF2563EB), // blue-600
Color(0xFF0891B2), // cyan-600
],
@@ -364,19 +364,19 @@ class _CommuteTrackerState extends State<CommuteTracker> {
),
child: SafeArea(
child: Column(
children: [
children: <Widget>[
Expanded(
child: Center(
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
children: <Widget>[
TweenAnimationBuilder(
tween: Tween<double>(begin: 1.0, end: 1.1),
duration: const Duration(seconds: 1),
curve: Curves.easeInOut,
builder: (context, double scale, child) {
builder: (BuildContext context, double scale, Widget? child) {
return Transform.scale(
scale: scale,
child: Container(
@@ -418,7 +418,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
textAlign: TextAlign.center,
),
const SizedBox(height: 32),
if (widget.distanceMeters != null) ...[
if (widget.distanceMeters != null) ...<Widget>[
Container(
width: double.infinity,
constraints: const BoxConstraints(maxWidth: 300),
@@ -431,7 +431,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
),
),
child: Column(
children: [
children: <Widget>[
Text(
'Distance to Site',
style: TextStyle(
@@ -451,7 +451,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
],
),
),
if (widget.etaMinutes != null) ...[
if (widget.etaMinutes != null) ...<Widget>[
const SizedBox(height: 12),
Container(
width: double.infinity,
@@ -465,7 +465,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
),
),
child: Column(
children: [
children: <Widget>[
Text(
'Estimated Arrival',
style: TextStyle(
@@ -530,13 +530,13 @@ class _CommuteTrackerState extends State<CommuteTracker> {
gradient: const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
colors: <Color>[
Color(0xFFECFDF5), // emerald-50
Color(0xFFD1FAE5), // green-50
],
),
borderRadius: BorderRadius.circular(12),
boxShadow: [
boxShadow: <BoxShadow>[
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 8,
@@ -545,7 +545,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
],
),
child: Column(
children: [
children: <Widget>[
Container(
width: 64,
height: 64,

View File

@@ -2,21 +2,21 @@ import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
class DateSelector extends StatelessWidget {
final DateTime selectedDate;
final ValueChanged<DateTime> onSelect;
final List<String> shiftDates;
const DateSelector({
super.key,
required this.selectedDate,
required this.onSelect,
this.shiftDates = const [],
this.shiftDates = const <String>[],
});
final DateTime selectedDate;
final ValueChanged<DateTime> onSelect;
final List<String> shiftDates;
@override
Widget build(BuildContext context) {
final today = DateTime.now();
final dates = List.generate(7, (index) {
final DateTime today = DateTime.now();
final List<DateTime> dates = List.generate(7, (int index) {
return today.add(Duration(days: index - 3));
});
@@ -24,10 +24,10 @@ class DateSelector extends StatelessWidget {
height: 80,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: dates.map((date) {
final isSelected = _isSameDay(date, selectedDate);
final isToday = _isSameDay(date, today);
final hasShift = shiftDates.contains(_formatDateIso(date));
children: dates.map((DateTime date) {
final bool isSelected = _isSameDay(date, selectedDate);
final bool isToday = _isSameDay(date, today);
final bool hasShift = shiftDates.contains(_formatDateIso(date));
return Expanded(
child: GestureDetector(
@@ -39,18 +39,18 @@ class DateSelector extends StatelessWidget {
color: isSelected ? const Color(0xFF0032A0) : Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: isSelected
? [
? <BoxShadow>[
BoxShadow(
color: const Color(0xFF0032A0).withOpacity(0.3),
blurRadius: 10,
offset: const Offset(0, 4),
),
]
: [],
: <BoxShadow>[],
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
children: <Widget>[
Text(
DateFormat('d').format(date),
style: TextStyle(

View File

@@ -3,14 +3,14 @@ import 'package:design_system/design_system.dart';
import 'package:lucide_icons/lucide_icons.dart';
class LocationMapPlaceholder extends StatelessWidget {
final bool isVerified;
final double? distance;
const LocationMapPlaceholder({
super.key,
required this.isVerified,
this.distance,
});
final bool isVerified;
final double? distance;
@override
Widget build(BuildContext context) {
@@ -31,12 +31,12 @@ class LocationMapPlaceholder extends StatelessWidget {
),
),
child: Stack(
children: [
children: <Widget>[
// Fallback UI if image fails (which it will without key)
const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
children: <Widget>[
Icon(LucideIcons.mapPin, size: 48, color: UiColors.iconSecondary),
SizedBox(height: 8),
Text('Map View (GPS)', style: TextStyle(color: UiColors.textSecondary)),
@@ -54,7 +54,7 @@ class LocationMapPlaceholder extends StatelessWidget {
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
boxShadow: <BoxShadow>[
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 8,
@@ -63,7 +63,7 @@ class LocationMapPlaceholder extends StatelessWidget {
],
),
child: Row(
children: [
children: <Widget>[
Icon(
isVerified ? LucideIcons.checkCircle : LucideIcons.alertCircle,
color: isVerified ? UiColors.textSuccess : UiColors.destructive,
@@ -73,7 +73,7 @@ class LocationMapPlaceholder extends StatelessWidget {
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
children: <Widget>[
Text(
isVerified ? 'Location Verified' : 'Location Check',
style: UiTypography.body1b.copyWith(color: UiColors.textPrimary),

View File

@@ -2,9 +2,9 @@ import 'package:flutter/material.dart';
import 'package:lucide_icons/lucide_icons.dart';
class LunchBreakDialog extends StatefulWidget {
final VoidCallback onComplete;
const LunchBreakDialog({super.key, required this.onComplete});
final VoidCallback onComplete;
@override
State<LunchBreakDialog> createState() => _LunchBreakDialogState();
@@ -23,7 +23,7 @@ class _LunchBreakDialogState extends State<LunchBreakDialog> {
String _additionalNotes = '';
final List<String> _timeOptions = _generateTimeOptions();
final List<String> _noLunchReasons = [
final List<String> _noLunchReasons = <String>[
'Unpredictable Workflows',
'Poor Time Management',
'Lack of coverage or short Staff',
@@ -32,12 +32,12 @@ class _LunchBreakDialogState extends State<LunchBreakDialog> {
];
static List<String> _generateTimeOptions() {
List<String> options = [];
final List<String> options = <String>[];
for (int h = 0; h < 24; h++) {
for (int m = 0; m < 60; m += 15) {
final hour = h % 12 == 0 ? 12 : h % 12;
final ampm = h < 12 ? 'am' : 'pm';
final timeStr = '$hour:${m.toString().padLeft(2, '0')}$ampm';
final int hour = h % 12 == 0 ? 12 : h % 12;
final String ampm = h < 12 ? 'am' : 'pm';
final String timeStr = '$hour:${m.toString().padLeft(2, '0')}$ampm';
options.add(timeStr);
}
}
@@ -78,7 +78,7 @@ class _LunchBreakDialogState extends State<LunchBreakDialog> {
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
children: <Widget>[
Container(
width: 80,
height: 80,
@@ -104,7 +104,7 @@ class _LunchBreakDialogState extends State<LunchBreakDialog> {
),
const SizedBox(height: 24),
Row(
children: [
children: <Widget>[
Expanded(
child: GestureDetector(
onTap: () {
@@ -171,7 +171,7 @@ class _LunchBreakDialogState extends State<LunchBreakDialog> {
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
children: <Widget>[
const Text(
"When did you take lunch?",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
@@ -179,13 +179,13 @@ class _LunchBreakDialogState extends State<LunchBreakDialog> {
const SizedBox(height: 24),
// Mock Inputs
Row(
children: [
children: <Widget>[
Expanded(
child: DropdownButtonFormField<String>(
isExpanded: true,
value: _breakStart,
items: _timeOptions.map((t) => DropdownMenuItem(value: t, child: Text(t, style: const TextStyle(fontSize: 13)))).toList(),
onChanged: (v) => setState(() => _breakStart = v),
initialValue: _breakStart,
items: _timeOptions.map((String t) => DropdownMenuItem(value: t, child: Text(t, style: const TextStyle(fontSize: 13)))).toList(),
onChanged: (String? v) => setState(() => _breakStart = v),
decoration: const InputDecoration(
labelText: 'Start',
contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8),
@@ -196,9 +196,9 @@ class _LunchBreakDialogState extends State<LunchBreakDialog> {
Expanded(
child: DropdownButtonFormField<String>(
isExpanded: true,
value: _breakEnd,
items: _timeOptions.map((t) => DropdownMenuItem(value: t, child: Text(t, style: const TextStyle(fontSize: 13)))).toList(),
onChanged: (v) => setState(() => _breakEnd = v),
initialValue: _breakEnd,
items: _timeOptions.map((String t) => DropdownMenuItem(value: t, child: Text(t, style: const TextStyle(fontSize: 13)))).toList(),
onChanged: (String? v) => setState(() => _breakEnd = v),
decoration: const InputDecoration(
labelText: 'End',
contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8),
@@ -230,17 +230,17 @@ class _LunchBreakDialogState extends State<LunchBreakDialog> {
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
children: <Widget>[
const Text(
"Why didn't you take lunch?",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
..._noLunchReasons.map((reason) => RadioListTile<String>(
..._noLunchReasons.map((String reason) => RadioListTile<String>(
title: Text(reason),
value: reason,
groupValue: _noLunchReason,
onChanged: (val) => setState(() => _noLunchReason = val),
onChanged: (String? val) => setState(() => _noLunchReason = val),
)),
const SizedBox(height: 24),
@@ -264,14 +264,14 @@ class _LunchBreakDialogState extends State<LunchBreakDialog> {
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
children: <Widget>[
const Text(
"Additional Notes",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
TextField(
onChanged: (v) => _additionalNotes = v,
onChanged: (String v) => _additionalNotes = v,
decoration: const InputDecoration(
hintText: 'Add any details...',
border: OutlineInputBorder(),
@@ -300,7 +300,7 @@ class _LunchBreakDialogState extends State<LunchBreakDialog> {
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
children: <Widget>[
const Icon(LucideIcons.checkCircle, size: 64, color: Colors.green),
const SizedBox(height: 24),
const Text(

View File

@@ -2,11 +2,6 @@ import 'package:flutter/material.dart';
import 'package:lucide_icons/lucide_icons.dart';
class SwipeToCheckIn extends StatefulWidget {
final VoidCallback? onCheckIn;
final VoidCallback? onCheckOut;
final bool isLoading;
final String mode; // 'swipe' or 'nfc'
final bool isCheckedIn;
const SwipeToCheckIn({
super.key,
@@ -16,6 +11,11 @@ class SwipeToCheckIn extends StatefulWidget {
this.mode = 'swipe',
this.isCheckedIn = false,
});
final VoidCallback? onCheckIn;
final VoidCallback? onCheckOut;
final bool isLoading;
final String mode; // 'swipe' or 'nfc'
final bool isCheckedIn;
@override
State<SwipeToCheckIn> createState() => _SwipeToCheckInState();
@@ -50,7 +50,7 @@ class _SwipeToCheckInState extends State<SwipeToCheckIn>
void _onDragEnd(DragEndDetails details, double maxWidth) {
if (_isComplete || widget.isLoading) return;
final threshold = (maxWidth - _handleSize - 8) * 0.8;
final double threshold = (maxWidth - _handleSize - 8) * 0.8;
if (_dragValue > threshold) {
setState(() {
_dragValue = maxWidth - _handleSize - 8;
@@ -72,7 +72,7 @@ class _SwipeToCheckInState extends State<SwipeToCheckIn>
@override
Widget build(BuildContext context) {
final baseColor = widget.isCheckedIn
final Color baseColor = widget.isCheckedIn
? const Color(0xFF10B981)
: const Color(0xFF0032A0);
@@ -94,7 +94,7 @@ class _SwipeToCheckInState extends State<SwipeToCheckIn>
decoration: BoxDecoration(
color: baseColor,
borderRadius: BorderRadius.circular(16),
boxShadow: [
boxShadow: <BoxShadow>[
BoxShadow(
color: baseColor.withOpacity(0.4),
blurRadius: 25,
@@ -105,7 +105,7 @@ class _SwipeToCheckInState extends State<SwipeToCheckIn>
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
children: <Widget>[
const Icon(LucideIcons.wifi, color: Colors.white),
const SizedBox(width: 12),
Text(
@@ -127,19 +127,19 @@ class _SwipeToCheckInState extends State<SwipeToCheckIn>
}
return LayoutBuilder(
builder: (context, constraints) {
final maxWidth = constraints.maxWidth;
final maxDrag = maxWidth - _handleSize - 8;
builder: (BuildContext context, BoxConstraints constraints) {
final double maxWidth = constraints.maxWidth;
final double maxDrag = maxWidth - _handleSize - 8;
// Calculate background color based on drag
final progress = _dragValue / maxDrag;
final startColor = widget.isCheckedIn
final double progress = _dragValue / maxDrag;
final Color startColor = widget.isCheckedIn
? const Color(0xFF10B981)
: const Color(0xFF0032A0);
final endColor = widget.isCheckedIn
final Color endColor = widget.isCheckedIn
? const Color(0xFF0032A0)
: const Color(0xFF10B981);
final currentColor =
final Color currentColor =
Color.lerp(startColor, endColor, progress) ?? startColor;
return Container(
@@ -147,7 +147,7 @@ class _SwipeToCheckInState extends State<SwipeToCheckIn>
decoration: BoxDecoration(
color: currentColor,
borderRadius: BorderRadius.circular(16),
boxShadow: [
boxShadow: <BoxShadow>[
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 4,
@@ -156,7 +156,7 @@ class _SwipeToCheckInState extends State<SwipeToCheckIn>
],
),
child: Stack(
children: [
children: <Widget>[
Center(
child: Opacity(
opacity: 1.0 - progress,
@@ -187,15 +187,15 @@ class _SwipeToCheckInState extends State<SwipeToCheckIn>
left: 4 + _dragValue,
top: 4,
child: GestureDetector(
onHorizontalDragUpdate: (d) => _onDragUpdate(d, maxWidth),
onHorizontalDragEnd: (d) => _onDragEnd(d, maxWidth),
onHorizontalDragUpdate: (DragUpdateDetails d) => _onDragUpdate(d, maxWidth),
onHorizontalDragEnd: (DragEndDetails d) => _onDragEnd(d, maxWidth),
child: Container(
width: _handleSize,
height: _handleSize,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
boxShadow: <BoxShadow>[
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 2,

View File

@@ -1,4 +1,4 @@
library staff_clock_in;
library;
export 'src/staff_clock_in_module.dart';
export 'src/presentation/pages/clock_in_page.dart';