feat: Implement Staff Clock In feature with attendance tracking and commute management
- Added AppColors for consistent theming across the feature. - Created AttendanceCard widget to display attendance information. - Developed CommuteTracker widget to manage and display commute status. - Implemented DateSelector for selecting shift dates. - Added LunchBreakDialog for logging lunch breaks with multiple steps. - Created SwipeToCheckIn widget for checking in and out via swipe or NFC. - Established StaffClockInModule for dependency injection and routing. - Updated pubspec.yaml with necessary dependencies for the feature.
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
/// Locales: 2
|
||||
/// Strings: 1026 (513 per locale)
|
||||
///
|
||||
/// Built on 2026-01-26 at 03:26 UTC
|
||||
/// Built on 2026-01-26 at 05:52 UTC
|
||||
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint, unused_import
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../../domain/repositories/clock_in_repository_interface.dart';
|
||||
|
||||
/// Implementation of [ClockInRepositoryInterface].
|
||||
///
|
||||
/// Delegates shift data retrieval to [ShiftsRepositoryMock] and manages purely
|
||||
/// local state for attendance (check-in/out) for the prototype phase.
|
||||
class ClockInRepositoryImpl implements ClockInRepositoryInterface {
|
||||
final ShiftsRepositoryMock _shiftsMock;
|
||||
|
||||
// Local state for the session (mocking backend state)
|
||||
bool _isCheckedIn = false;
|
||||
DateTime? _checkInTime;
|
||||
DateTime? _checkOutTime;
|
||||
String? _activeShiftId;
|
||||
|
||||
ClockInRepositoryImpl({ShiftsRepositoryMock? shiftsMock})
|
||||
: _shiftsMock = shiftsMock ?? ShiftsRepositoryMock();
|
||||
|
||||
@override
|
||||
Future<Shift?> getTodaysShift() async {
|
||||
final shifts = await _shiftsMock.getMyShifts();
|
||||
|
||||
if (shifts.isEmpty) return null;
|
||||
|
||||
final now = DateTime.now();
|
||||
final todayStr = DateFormat('yyyy-MM-dd').format(now);
|
||||
|
||||
// Find a shift effectively for today, or mock one
|
||||
try {
|
||||
return shifts.firstWhere((s) => s.date == todayStr);
|
||||
} catch (_) {
|
||||
final original = shifts.first;
|
||||
// Mock "today's" shift based on the first available shift
|
||||
return Shift(
|
||||
id: original.id,
|
||||
title: original.title,
|
||||
clientName: original.clientName,
|
||||
logoUrl: original.logoUrl,
|
||||
hourlyRate: original.hourlyRate,
|
||||
location: original.location,
|
||||
locationAddress: original.locationAddress,
|
||||
date: todayStr,
|
||||
startTime: original.startTime, // Use original times or calculate
|
||||
endTime: original.endTime,
|
||||
createdDate: original.createdDate,
|
||||
status: 'assigned',
|
||||
latitude: original.latitude,
|
||||
longitude: original.longitude,
|
||||
description: original.description,
|
||||
managers: original.managers,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> getAttendanceStatus() async {
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
return _getCurrentStatusMap();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> clockIn({required String shiftId, String? notes}) async {
|
||||
await Future.delayed(const Duration(seconds: 1)); // Simulate network
|
||||
|
||||
_isCheckedIn = true;
|
||||
_checkInTime = DateTime.now();
|
||||
_activeShiftId = shiftId;
|
||||
_checkOutTime = null; // Reset for new check-in? Or keep for history?
|
||||
// Simple mock logic: reset check-out on new check-in.
|
||||
|
||||
return _getCurrentStatusMap();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> clockOut({String? notes, int? breakTimeMinutes}) async {
|
||||
await Future.delayed(const Duration(seconds: 1)); // Simulate network
|
||||
|
||||
_isCheckedIn = false;
|
||||
_checkOutTime = DateTime.now();
|
||||
|
||||
return _getCurrentStatusMap();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Map<String, dynamic>>> getActivityLog() async {
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
// Mock data
|
||||
return [
|
||||
{
|
||||
'date': DateTime.now().subtract(const Duration(days: 1)),
|
||||
'start': '09:00 AM',
|
||||
'end': '05:00 PM',
|
||||
'hours': '8h',
|
||||
},
|
||||
{
|
||||
'date': DateTime.now().subtract(const Duration(days: 2)),
|
||||
'start': '09:00 AM',
|
||||
'end': '05:00 PM',
|
||||
'hours': '8h',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
Map<String, dynamic> _getCurrentStatusMap() {
|
||||
return {
|
||||
'isCheckedIn': _isCheckedIn,
|
||||
'checkInTime': _checkInTime,
|
||||
'checkOutTime': _checkOutTime,
|
||||
'activeShiftId': _activeShiftId,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
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,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [shiftId, notes];
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import 'package:krow_core/core.dart';
|
||||
|
||||
/// Represents the arguments required for the [ClockOutUseCase].
|
||||
class ClockOutArguments extends UseCaseArgument {
|
||||
/// Optional notes provided by the user during clock-out.
|
||||
final String? notes;
|
||||
|
||||
/// Optional break time in minutes.
|
||||
final int? breakTimeMinutes;
|
||||
|
||||
/// Creates a [ClockOutArguments] instance.
|
||||
const ClockOutArguments({
|
||||
this.notes,
|
||||
this.breakTimeMinutes,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [notes, breakTimeMinutes];
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
/// Repository interface for Clock In/Out functionality
|
||||
abstract class ClockInRepository {
|
||||
|
||||
/// Retrieves the shift assigned to the user for the current day.
|
||||
/// Returns null if no shift is assigned for today.
|
||||
Future<Shift?> getTodaysShift();
|
||||
|
||||
/// Gets the current attendance status (e.g., checked in or not, times).
|
||||
/// This helps in restoring the UI state if the app was killed.
|
||||
Future<AttendanceStatus> getAttendanceStatus();
|
||||
|
||||
/// Checks the user in for the specified [shiftId].
|
||||
/// Returns the updated [AttendanceStatus].
|
||||
Future<AttendanceStatus> clockIn({required String shiftId, String? notes});
|
||||
|
||||
/// Checks the user out for the currently active shift.
|
||||
/// Optionally accepts [breakTimeMinutes] if tracked.
|
||||
Future<AttendanceStatus> clockOut({String? notes, int? breakTimeMinutes});
|
||||
|
||||
/// Retrieves a list of recent clock-in/out activities.
|
||||
Future<List<Map<String, dynamic>>> getActivityLog();
|
||||
}
|
||||
|
||||
/// Simple entity to hold attendance state
|
||||
class AttendanceStatus {
|
||||
final bool isCheckedIn;
|
||||
final DateTime? checkInTime;
|
||||
final DateTime? checkOutTime;
|
||||
final String? activeShiftId;
|
||||
|
||||
const AttendanceStatus({
|
||||
this.isCheckedIn = false,
|
||||
this.checkInTime,
|
||||
this.checkOutTime,
|
||||
this.activeShiftId,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
/// Interface for the Clock In feature repository.
|
||||
///
|
||||
/// Defines the methods for managing clock-in/out operations and retrieving
|
||||
/// related shift and attendance data.
|
||||
abstract interface class ClockInRepositoryInterface {
|
||||
/// Retrieves the shift scheduled for today.
|
||||
Future<Shift?> getTodaysShift();
|
||||
|
||||
/// Retrieves the current attendance status (check-in time, check-out time, etc.).
|
||||
///
|
||||
/// Returns a Map containing:
|
||||
/// - 'isCheckedIn': bool
|
||||
/// - 'checkInTime': DateTime?
|
||||
/// - 'checkOutTime': DateTime?
|
||||
Future<Map<String, dynamic>> getAttendanceStatus();
|
||||
|
||||
/// Clocks the user in for a specific shift.
|
||||
Future<Map<String, dynamic>> clockIn({
|
||||
required String shiftId,
|
||||
String? notes,
|
||||
});
|
||||
|
||||
/// Clocks the user out of the current shift.
|
||||
Future<Map<String, dynamic>> clockOut({
|
||||
String? notes,
|
||||
int? breakTimeMinutes,
|
||||
});
|
||||
|
||||
/// Retrieves the history of clock-in/out activity.
|
||||
///
|
||||
/// Returns a list of maps, where each map represents an activity entry.
|
||||
Future<List<Map<String, dynamic>>> getActivityLog();
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import 'package:krow_core/core.dart';
|
||||
import '../repositories/clock_in_repository_interface.dart';
|
||||
import '../arguments/clock_in_arguments.dart';
|
||||
|
||||
/// Use case for clocking in a user.
|
||||
class ClockInUseCase implements UseCase<ClockInArguments, Map<String, dynamic>> {
|
||||
final ClockInRepositoryInterface _repository;
|
||||
|
||||
ClockInUseCase(this._repository);
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> call(ClockInArguments arguments) {
|
||||
return _repository.clockIn(
|
||||
shiftId: arguments.shiftId,
|
||||
notes: arguments.notes,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import 'package:krow_core/core.dart';
|
||||
import '../repositories/clock_in_repository_interface.dart';
|
||||
import '../arguments/clock_out_arguments.dart';
|
||||
|
||||
/// Use case for clocking out a user.
|
||||
class ClockOutUseCase implements UseCase<ClockOutArguments, Map<String, dynamic>> {
|
||||
final ClockInRepositoryInterface _repository;
|
||||
|
||||
ClockOutUseCase(this._repository);
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> call(ClockOutArguments arguments) {
|
||||
return _repository.clockOut(
|
||||
notes: arguments.notes,
|
||||
breakTimeMinutes: arguments.breakTimeMinutes,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import 'package:krow_core/core.dart';
|
||||
import '../repositories/clock_in_repository_interface.dart';
|
||||
|
||||
/// Use case for retrieving the activity log.
|
||||
class GetActivityLogUseCase implements NoInputUseCase<List<Map<String, dynamic>>> {
|
||||
final ClockInRepositoryInterface _repository;
|
||||
|
||||
GetActivityLogUseCase(this._repository);
|
||||
|
||||
@override
|
||||
Future<List<Map<String, dynamic>>> call() {
|
||||
return _repository.getActivityLog();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import 'package:krow_core/core.dart';
|
||||
import '../repositories/clock_in_repository_interface.dart';
|
||||
|
||||
/// Use case for getting the current attendance status (check-in/out times).
|
||||
class GetAttendanceStatusUseCase implements NoInputUseCase<Map<String, dynamic>> {
|
||||
final ClockInRepositoryInterface _repository;
|
||||
|
||||
GetAttendanceStatusUseCase(this._repository);
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> call() {
|
||||
return _repository.getAttendanceStatus();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../repositories/clock_in_repository_interface.dart';
|
||||
|
||||
/// Use case for retrieving the user's scheduled shift for today.
|
||||
class GetTodaysShiftUseCase implements NoInputUseCase<Shift?> {
|
||||
final ClockInRepositoryInterface _repository;
|
||||
|
||||
GetTodaysShiftUseCase(this._repository);
|
||||
|
||||
@override
|
||||
Future<Shift?> call() {
|
||||
return _repository.getTodaysShift();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../domain/usecases/get_todays_shift_usecase.dart';
|
||||
import '../../domain/usecases/get_attendance_status_usecase.dart';
|
||||
import '../../domain/usecases/clock_in_usecase.dart';
|
||||
import '../../domain/usecases/clock_out_usecase.dart';
|
||||
import '../../domain/usecases/get_activity_log_usecase.dart';
|
||||
import '../../domain/arguments/clock_in_arguments.dart';
|
||||
import '../../domain/arguments/clock_out_arguments.dart';
|
||||
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;
|
||||
final GetActivityLogUseCase _getActivityLog;
|
||||
|
||||
ClockInBloc({
|
||||
required GetTodaysShiftUseCase getTodaysShift,
|
||||
required GetAttendanceStatusUseCase getAttendanceStatus,
|
||||
required ClockInUseCase clockIn,
|
||||
required ClockOutUseCase clockOut,
|
||||
required GetActivityLogUseCase getActivityLog,
|
||||
}) : _getTodaysShift = getTodaysShift,
|
||||
_getAttendanceStatus = getAttendanceStatus,
|
||||
_clockIn = clockIn,
|
||||
_clockOut = clockOut,
|
||||
_getActivityLog = getActivityLog,
|
||||
super(ClockInState(selectedDate: DateTime.now())) {
|
||||
on<ClockInPageLoaded>(_onLoaded);
|
||||
on<DateSelected>(_onDateSelected);
|
||||
on<CheckInRequested>(_onCheckIn);
|
||||
on<CheckOutRequested>(_onCheckOut);
|
||||
on<CheckInModeChanged>(_onModeChanged);
|
||||
}
|
||||
|
||||
AttendanceStatus _mapToStatus(Map<String, dynamic> map) {
|
||||
return AttendanceStatus(
|
||||
isCheckedIn: map['isCheckedIn'] as bool? ?? false,
|
||||
checkInTime: map['checkInTime'] as DateTime?,
|
||||
checkOutTime: map['checkOutTime'] as DateTime?,
|
||||
activeShiftId: map['activeShiftId'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onLoaded(
|
||||
ClockInPageLoaded event,
|
||||
Emitter<ClockInState> emit,
|
||||
) async {
|
||||
emit(state.copyWith(status: ClockInStatus.loading));
|
||||
try {
|
||||
final shift = await _getTodaysShift();
|
||||
final statusMap = await _getAttendanceStatus();
|
||||
final activity = await _getActivityLog();
|
||||
|
||||
emit(state.copyWith(
|
||||
status: ClockInStatus.success,
|
||||
todayShift: shift,
|
||||
attendance: _mapToStatus(statusMap),
|
||||
activityLog: activity,
|
||||
));
|
||||
} catch (e) {
|
||||
emit(state.copyWith(
|
||||
status: ClockInStatus.failure,
|
||||
errorMessage: e.toString(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
void _onDateSelected(
|
||||
DateSelected event,
|
||||
Emitter<ClockInState> emit,
|
||||
) {
|
||||
emit(state.copyWith(selectedDate: event.date));
|
||||
}
|
||||
|
||||
void _onModeChanged(
|
||||
CheckInModeChanged event,
|
||||
Emitter<ClockInState> emit,
|
||||
) {
|
||||
emit(state.copyWith(checkInMode: event.mode));
|
||||
}
|
||||
|
||||
Future<void> _onCheckIn(
|
||||
CheckInRequested event,
|
||||
Emitter<ClockInState> emit,
|
||||
) async {
|
||||
emit(state.copyWith(status: ClockInStatus.actionInProgress));
|
||||
try {
|
||||
final newStatusMap = await _clockIn(
|
||||
ClockInArguments(shiftId: event.shiftId, notes: event.notes),
|
||||
);
|
||||
emit(state.copyWith(
|
||||
status: ClockInStatus.success,
|
||||
attendance: _mapToStatus(newStatusMap),
|
||||
));
|
||||
} catch (e) {
|
||||
emit(state.copyWith(
|
||||
status: ClockInStatus.failure,
|
||||
errorMessage: e.toString(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onCheckOut(
|
||||
CheckOutRequested event,
|
||||
Emitter<ClockInState> emit,
|
||||
) async {
|
||||
emit(state.copyWith(status: ClockInStatus.actionInProgress));
|
||||
try {
|
||||
final newStatusMap = await _clockOut(
|
||||
ClockOutArguments(
|
||||
notes: event.notes,
|
||||
breakTimeMinutes: 0, // Should be passed from event if supported
|
||||
),
|
||||
);
|
||||
emit(state.copyWith(
|
||||
status: ClockInStatus.success,
|
||||
attendance: _mapToStatus(newStatusMap),
|
||||
));
|
||||
} catch (e) {
|
||||
emit(state.copyWith(
|
||||
status: ClockInStatus.failure,
|
||||
errorMessage: e.toString(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
abstract class ClockInEvent extends Equatable {
|
||||
const ClockInEvent();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
class ClockInPageLoaded extends ClockInEvent {}
|
||||
|
||||
class DateSelected extends ClockInEvent {
|
||||
final DateTime date;
|
||||
|
||||
const DateSelected(this.date);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [date];
|
||||
}
|
||||
|
||||
class CheckInRequested extends ClockInEvent {
|
||||
final String shiftId;
|
||||
final String? notes;
|
||||
|
||||
const CheckInRequested({required this.shiftId, this.notes});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [shiftId, notes];
|
||||
}
|
||||
|
||||
class CheckOutRequested extends ClockInEvent {
|
||||
final String? notes;
|
||||
final int? breakTimeMinutes;
|
||||
|
||||
const CheckOutRequested({this.notes, this.breakTimeMinutes});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [notes, breakTimeMinutes];
|
||||
}
|
||||
|
||||
class CheckInModeChanged extends ClockInEvent {
|
||||
final String mode;
|
||||
|
||||
const CheckInModeChanged(this.mode);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [mode];
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
enum ClockInStatus { initial, loading, success, failure, actionInProgress }
|
||||
|
||||
/// View model representing the user's current attendance state.
|
||||
class AttendanceStatus extends Equatable {
|
||||
final bool isCheckedIn;
|
||||
final DateTime? checkInTime;
|
||||
final DateTime? checkOutTime;
|
||||
final String? activeShiftId;
|
||||
|
||||
const AttendanceStatus({
|
||||
this.isCheckedIn = false,
|
||||
this.checkInTime,
|
||||
this.checkOutTime,
|
||||
this.activeShiftId,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [isCheckedIn, checkInTime, checkOutTime, activeShiftId];
|
||||
}
|
||||
|
||||
class ClockInState extends Equatable {
|
||||
final ClockInStatus status;
|
||||
final Shift? todayShift;
|
||||
final AttendanceStatus attendance;
|
||||
final List<Map<String, dynamic>> activityLog;
|
||||
final DateTime selectedDate;
|
||||
final String checkInMode;
|
||||
final String? errorMessage;
|
||||
|
||||
const ClockInState({
|
||||
this.status = ClockInStatus.initial,
|
||||
this.todayShift,
|
||||
this.attendance = const AttendanceStatus(),
|
||||
this.activityLog = const [],
|
||||
required this.selectedDate,
|
||||
this.checkInMode = 'swipe',
|
||||
this.errorMessage,
|
||||
});
|
||||
|
||||
ClockInState copyWith({
|
||||
ClockInStatus? status,
|
||||
Shift? todayShift,
|
||||
AttendanceStatus? attendance,
|
||||
List<Map<String, dynamic>>? activityLog,
|
||||
DateTime? selectedDate,
|
||||
String? checkInMode,
|
||||
String? errorMessage,
|
||||
}) {
|
||||
return ClockInState(
|
||||
status: status ?? this.status,
|
||||
todayShift: todayShift ?? this.todayShift,
|
||||
attendance: attendance ?? this.attendance,
|
||||
activityLog: activityLog ?? this.activityLog,
|
||||
selectedDate: selectedDate ?? this.selectedDate,
|
||||
checkInMode: checkInMode ?? this.checkInMode,
|
||||
errorMessage: errorMessage,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
status,
|
||||
todayShift,
|
||||
attendance,
|
||||
activityLog,
|
||||
selectedDate,
|
||||
checkInMode,
|
||||
errorMessage,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,774 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../theme/app_colors.dart';
|
||||
import '../bloc/clock_in_bloc.dart';
|
||||
import '../bloc/clock_in_event.dart';
|
||||
import '../bloc/clock_in_state.dart';
|
||||
import '../widgets/attendance_card.dart';
|
||||
import '../widgets/date_selector.dart';
|
||||
import '../widgets/swipe_to_check_in.dart';
|
||||
import '../widgets/lunch_break_modal.dart';
|
||||
import '../widgets/commute_tracker.dart';
|
||||
|
||||
class ClockInPage extends StatefulWidget {
|
||||
const ClockInPage({super.key});
|
||||
|
||||
@override
|
||||
State<ClockInPage> createState() => _ClockInPageState();
|
||||
}
|
||||
|
||||
class _ClockInPageState extends State<ClockInPage> {
|
||||
late final ClockInBloc _bloc;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_bloc = Modular.get<ClockInBloc>();
|
||||
_bloc.add(ClockInPageLoaded());
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: _bloc,
|
||||
child: BlocConsumer<ClockInBloc, ClockInState>(
|
||||
listener: (context, state) {
|
||||
if (state.status == ClockInStatus.failure && state.errorMessage != null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(state.errorMessage!)),
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
if (state.status == ClockInStatus.loading && state.todayShift == null) {
|
||||
return const Scaffold(
|
||||
body: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
final todayShift = state.todayShift;
|
||||
final checkInTime = state.attendance.checkInTime;
|
||||
final checkOutTime = state.attendance.checkOutTime;
|
||||
final isCheckedIn = state.attendance.isCheckedIn;
|
||||
|
||||
// Format times for display
|
||||
final checkInStr = checkInTime != null
|
||||
? DateFormat('h:mm a').format(checkInTime)
|
||||
: '--:-- --';
|
||||
final checkOutStr = checkOutTime != null
|
||||
? DateFormat('h:mm a').format(checkOutTime)
|
||||
: '--:-- --';
|
||||
|
||||
return Scaffold(
|
||||
body: Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Color(0xFFF8FAFC), // slate-50
|
||||
Colors.white,
|
||||
],
|
||||
),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.only(bottom: 100),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildHeader(),
|
||||
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Column(
|
||||
children: [
|
||||
// Commute Tracker (shows before date selector when applicable)
|
||||
if (todayShift != null)
|
||||
CommuteTracker(
|
||||
shift: todayShift,
|
||||
hasLocationConsent: false, // Mock value
|
||||
isCommuteModeOn: false, // Mock value
|
||||
distanceMeters: 500, // Mock value for demo
|
||||
etaMinutes: 8, // Mock value for demo
|
||||
),
|
||||
|
||||
// Date Selector
|
||||
DateSelector(
|
||||
selectedDate: state.selectedDate,
|
||||
onSelect: (date) => _bloc.add(DateSelected(date)),
|
||||
shiftDates: [
|
||||
DateFormat('yyyy-MM-dd').format(DateTime.now()),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Today Attendance Section
|
||||
const Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
"Today Attendance",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.krowCharcoal,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
GridView.count(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
crossAxisCount: 2,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 12,
|
||||
childAspectRatio: 1.0,
|
||||
children: [
|
||||
AttendanceCard(
|
||||
type: AttendanceType.checkin,
|
||||
title: "Check In",
|
||||
value: checkInStr,
|
||||
subtitle: checkInTime != null
|
||||
? "On Time"
|
||||
: "Pending",
|
||||
scheduledTime: "09:00 AM",
|
||||
),
|
||||
AttendanceCard(
|
||||
type: AttendanceType.checkout,
|
||||
title: "Check Out",
|
||||
value: checkOutStr,
|
||||
subtitle: checkOutTime != null
|
||||
? "Go Home"
|
||||
: "Pending",
|
||||
scheduledTime: "05:00 PM",
|
||||
),
|
||||
AttendanceCard(
|
||||
type: AttendanceType.breaks,
|
||||
title: "Break Time",
|
||||
value: "00:30 min",
|
||||
subtitle: "Scheduled 00:30 min",
|
||||
),
|
||||
const AttendanceCard(
|
||||
type: AttendanceType.days,
|
||||
title: "Total Days",
|
||||
value: "28",
|
||||
subtitle: "Working Days",
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Your Activity Header
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
"Your Activity",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.krowCharcoal,
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
debugPrint('Navigating to shifts...');
|
||||
},
|
||||
child: const Row(
|
||||
children: [
|
||||
Text(
|
||||
"View all",
|
||||
style: TextStyle(
|
||||
color: AppColors.krowBlue,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
LucideIcons.chevronRight,
|
||||
size: 16,
|
||||
color: AppColors.krowBlue,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Check-in Mode Toggle
|
||||
const Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
"Check-in Method",
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF334155), // slate-700
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF1F5F9), // slate-100
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
_buildModeTab("Swipe", LucideIcons.mapPin, 'swipe', state.checkInMode),
|
||||
_buildModeTab("NFC Tap", LucideIcons.wifi, 'nfc', state.checkInMode),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Selected Shift Info Card
|
||||
if (todayShift != null)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: const Color(0xFFE2E8F0),
|
||||
), // slate-200
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 2,
|
||||
offset: const Offset(0, 1),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
"TODAY'S SHIFT",
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.krowBlue,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
todayShift.title,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF1E293B), // slate-800
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"${todayShift.clientName} • ${todayShift.location}",
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF64748B), // slate-500
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
const Text(
|
||||
"9:00 AM - 5:00 PM",
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF475569), // slate-600
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"\$${todayShift.hourlyRate}/hr",
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.krowBlue,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Swipe To Check In / Checked Out State / No Shift State
|
||||
if (todayShift != null && checkOutTime == null) ...[
|
||||
SwipeToCheckIn(
|
||||
isCheckedIn: isCheckedIn,
|
||||
mode: state.checkInMode,
|
||||
isLoading: state.status == ClockInStatus.actionInProgress,
|
||||
onCheckIn: () async {
|
||||
// Show NFC dialog if mode is 'nfc'
|
||||
if (state.checkInMode == 'nfc') {
|
||||
await _showNFCDialog(context);
|
||||
} else {
|
||||
_bloc.add(CheckInRequested(shiftId: todayShift.id));
|
||||
}
|
||||
},
|
||||
onCheckOut: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => LunchBreakDialog(
|
||||
onComplete: () {
|
||||
Navigator.of(context).pop(); // Close dialog first
|
||||
_bloc.add(const CheckOutRequested());
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
] else if (todayShift != null && checkOutTime != null) ...[
|
||||
// Shift Completed State
|
||||
Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFECFDF5), // emerald-50
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: const Color(0xFFA7F3D0),
|
||||
), // emerald-200
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFFD1FAE5), // emerald-100
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
LucideIcons.check,
|
||||
color: Color(0xFF059669), // emerald-600
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Text(
|
||||
"Shift Completed!",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF065F46), // emerald-800
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const Text(
|
||||
"Great work today",
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF059669), // emerald-600
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
// No Shift State
|
||||
Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF1F5F9), // slate-100
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
const Text(
|
||||
"No confirmed shifts for today",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF475569), // slate-600
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const Text(
|
||||
"Accept a shift to clock in",
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF64748B), // slate-500
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
// Checked In Banner
|
||||
if (isCheckedIn && checkInTime != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFECFDF5), // emerald-50
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: const Color(0xFFA7F3D0),
|
||||
), // emerald-200
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
"Checked in at",
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF059669),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
DateFormat('h:mm a').format(checkInTime),
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF065F46),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFFD1FAE5),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
LucideIcons.check,
|
||||
color: Color(0xFF059669),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Recent Activity List
|
||||
...state.activityLog.map(
|
||||
(activity) => Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF8F9FA),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: const Color(0xFFF1F5F9),
|
||||
), // slate-100
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.krowBlue.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Icon(
|
||||
LucideIcons.mapPin,
|
||||
color: AppColors.krowBlue,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
DateFormat(
|
||||
'MMM d',
|
||||
).format(activity['date'] as DateTime),
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF0F172A), // slate-900
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"${activity['start']} - ${activity['end']}",
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF64748B), // slate-500
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Text(
|
||||
activity['hours'] as String,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.krowBlue,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildModeTab(String label, IconData icon, String value, String currentMode) {
|
||||
final isSelected = currentMode == value;
|
||||
return Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => _bloc.add(CheckInModeChanged(value)),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? Colors.white : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
boxShadow: isSelected
|
||||
? [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 2,
|
||||
offset: const Offset(0, 1),
|
||||
),
|
||||
]
|
||||
: [],
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: 16,
|
||||
color: isSelected ? Colors.black : Colors.grey,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: isSelected ? Colors.black : Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 24, 20, 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: AppColors.krowBlue.withOpacity(0.2),
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
child: CircleAvatar(
|
||||
backgroundColor: AppColors.krowBlue.withOpacity(0.1),
|
||||
child: const Text(
|
||||
'K',
|
||||
style: TextStyle(
|
||||
color: AppColors.krowBlue,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Good Morning',
|
||||
style: TextStyle(color: AppColors.krowMuted, fontSize: 12),
|
||||
),
|
||||
Text(
|
||||
'Krower',
|
||||
style: TextStyle(
|
||||
color: AppColors.krowCharcoal,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Warehouse Assistant',
|
||||
style: TextStyle(color: AppColors.krowMuted, fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(
|
||||
20,
|
||||
), // Rounded full for this page per design
|
||||
border: Border.all(color: Colors.grey.shade100),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 2,
|
||||
offset: const Offset(0, 1),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: const Icon(
|
||||
LucideIcons.bell,
|
||||
color: AppColors.krowMuted,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showNFCDialog(BuildContext context) async {
|
||||
bool scanned = false;
|
||||
|
||||
// Using a local navigator context since we are in a dialog
|
||||
await showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (BuildContext dialogContext) {
|
||||
return StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
return AlertDialog(
|
||||
title: Text(scanned ? 'Tag Scanned!' : 'Scan NFC Tag'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 96,
|
||||
height: 96,
|
||||
decoration: BoxDecoration(
|
||||
color: scanned
|
||||
? Colors.green.shade50
|
||||
: Colors.blue.shade50,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
scanned ? LucideIcons.check : LucideIcons.nfc,
|
||||
size: 48,
|
||||
color: scanned
|
||||
? Colors.green.shade600
|
||||
: Colors.blue.shade600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
scanned ? 'Processing check-in...' : 'Ready to scan',
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
scanned
|
||||
? 'Please wait...'
|
||||
: 'Hold your phone near the NFC tag at the clock-in station',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey.shade600),
|
||||
),
|
||||
if (!scanned) ...[
|
||||
const SizedBox(height: 24),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 56,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () async {
|
||||
setState(() {
|
||||
scanned = true;
|
||||
});
|
||||
// Simulate NFC scan delay
|
||||
await Future.delayed(
|
||||
const Duration(milliseconds: 1000),
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
Navigator.of(dialogContext).pop();
|
||||
// Trigger BLoC event
|
||||
// Need to access the bloc from the outer context or via passed reference
|
||||
// Since _bloc is a field of the page state, we can use it if we are inside the page class
|
||||
// But this dialog is just a function call.
|
||||
// It's safer to just return a result
|
||||
},
|
||||
icon: const Icon(LucideIcons.nfc, size: 24),
|
||||
label: const Text(
|
||||
'Tap to Scan',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF0047FF),
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// After dialog closes, trigger the event if scan was successful (simulated)
|
||||
// In real app, we would check the dialog result
|
||||
if (scanned && _bloc.state.todayShift != null) {
|
||||
_bloc.add(CheckInRequested(shiftId: _bloc.state.todayShift!.id));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AppColors {
|
||||
static const Color krowBlue = Color(0xFF0A39DF);
|
||||
static const Color krowYellow = Color(0xFFFFED4A);
|
||||
static const Color krowCharcoal = Color(0xFF121826);
|
||||
static const Color krowMuted = Color(0xFF6A7382);
|
||||
static const Color krowBorder = Color(0xFFE3E6E9);
|
||||
static const Color krowBackground = Color(0xFFFAFBFC);
|
||||
|
||||
static const Color white = Colors.white;
|
||||
static const Color black = Colors.black;
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import 'package:flutter/material.dart';
|
||||
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,
|
||||
required this.type,
|
||||
required this.title,
|
||||
required this.value,
|
||||
required this.subtitle,
|
||||
this.scheduledTime,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final styles = _getStyles(type);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: Colors.grey.shade100),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 2,
|
||||
offset: const Offset(0, 1),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: styles.bgColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(styles.icon, size: 16, color: styles.iconColor),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF64748B), // slate-500
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF0F172A), // slate-900
|
||||
),
|
||||
),
|
||||
if (scheduledTime != null) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
"Scheduled: $scheduledTime",
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
color: Color(0xFF94A3B8), // slate-400
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
subtitle,
|
||||
style: const TextStyle(fontSize: 12, color: Color(0xFF0032A0)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_AttendanceStyle _getStyles(AttendanceType type) {
|
||||
switch (type) {
|
||||
case AttendanceType.checkin:
|
||||
return _AttendanceStyle(
|
||||
icon: LucideIcons.logIn,
|
||||
bgColor: const Color(0xFF0032A0).withOpacity(0.1),
|
||||
iconColor: const Color(0xFF0032A0),
|
||||
);
|
||||
case AttendanceType.checkout:
|
||||
return _AttendanceStyle(
|
||||
icon: LucideIcons.logOut,
|
||||
bgColor: const Color(0xFF333F48).withOpacity(0.1),
|
||||
iconColor: const Color(0xFF333F48),
|
||||
);
|
||||
case AttendanceType.breaks:
|
||||
return _AttendanceStyle(
|
||||
icon: LucideIcons.coffee,
|
||||
bgColor: const Color(0xFFF9E547).withOpacity(0.2),
|
||||
iconColor: const Color(0xFF4C460D),
|
||||
);
|
||||
case AttendanceType.days:
|
||||
return _AttendanceStyle(
|
||||
icon: LucideIcons.calendar,
|
||||
bgColor: Colors.green.withOpacity(0.1),
|
||||
iconColor: Colors.green,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _AttendanceStyle {
|
||||
final IconData icon;
|
||||
final Color bgColor;
|
||||
final Color iconColor;
|
||||
|
||||
_AttendanceStyle({
|
||||
required this.icon,
|
||||
required this.bgColor,
|
||||
required this.iconColor,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,542 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import '../theme/app_colors.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
enum CommuteMode {
|
||||
lockedNoShift,
|
||||
needsConsent,
|
||||
preShiftCommuteAllowed,
|
||||
commuteModeActive,
|
||||
arrivedCanClockIn,
|
||||
}
|
||||
|
||||
class CommuteTracker extends StatefulWidget {
|
||||
final Shift? shift;
|
||||
final Function(CommuteMode)? onModeChange;
|
||||
final bool hasLocationConsent;
|
||||
final bool isCommuteModeOn;
|
||||
final double? distanceMeters;
|
||||
final int? etaMinutes;
|
||||
|
||||
const CommuteTracker({
|
||||
super.key,
|
||||
this.shift,
|
||||
this.onModeChange,
|
||||
this.hasLocationConsent = false,
|
||||
this.isCommuteModeOn = false,
|
||||
this.distanceMeters,
|
||||
this.etaMinutes,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CommuteTracker> createState() => _CommuteTrackerState();
|
||||
}
|
||||
|
||||
class _CommuteTrackerState extends State<CommuteTracker> {
|
||||
bool _localHasConsent = false;
|
||||
bool _localIsCommuteOn = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_localHasConsent = widget.hasLocationConsent;
|
||||
_localIsCommuteOn = widget.isCommuteModeOn;
|
||||
}
|
||||
|
||||
CommuteMode _getAppMode() {
|
||||
if (widget.shift == null) return CommuteMode.lockedNoShift;
|
||||
|
||||
// For demo purposes, check if we're within 24 hours of shift
|
||||
final now = DateTime.now();
|
||||
final shiftStart = DateTime.parse(
|
||||
'${widget.shift!.date} ${widget.shift!.startTime}',
|
||||
);
|
||||
final hoursUntilShift = shiftStart.difference(now).inHours;
|
||||
final inCommuteWindow = hoursUntilShift <= 24 && hoursUntilShift >= 0;
|
||||
|
||||
if (_localIsCommuteOn) {
|
||||
// Check if arrived (mock: if distance < 200m)
|
||||
if (widget.distanceMeters != null && widget.distanceMeters! <= 200) {
|
||||
return CommuteMode.arrivedCanClockIn;
|
||||
}
|
||||
return CommuteMode.commuteModeActive;
|
||||
}
|
||||
|
||||
if (inCommuteWindow) {
|
||||
return _localHasConsent
|
||||
? CommuteMode.preShiftCommuteAllowed
|
||||
: CommuteMode.needsConsent;
|
||||
}
|
||||
|
||||
return CommuteMode.lockedNoShift;
|
||||
}
|
||||
|
||||
String _formatDistance(double meters) {
|
||||
final miles = meters / 1609.34;
|
||||
return miles < 0.1
|
||||
? '${meters.round()} m'
|
||||
: '${miles.toStringAsFixed(1)} mi';
|
||||
}
|
||||
|
||||
int _getMinutesUntilShift() {
|
||||
if (widget.shift == null) return 0;
|
||||
final now = DateTime.now();
|
||||
final shiftStart = DateTime.parse(
|
||||
'${widget.shift!.date} ${widget.shift!.startTime}',
|
||||
);
|
||||
return shiftStart.difference(now).inMinutes;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final mode = _getAppMode();
|
||||
|
||||
// Notify parent of mode change
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
widget.onModeChange?.call(mode);
|
||||
});
|
||||
|
||||
switch (mode) {
|
||||
case CommuteMode.lockedNoShift:
|
||||
return const SizedBox.shrink();
|
||||
|
||||
case CommuteMode.needsConsent:
|
||||
return _buildConsentCard();
|
||||
|
||||
case CommuteMode.preShiftCommuteAllowed:
|
||||
return _buildPreShiftCard();
|
||||
|
||||
case CommuteMode.commuteModeActive:
|
||||
return _buildActiveCommuteScreen();
|
||||
|
||||
case CommuteMode.arrivedCanClockIn:
|
||||
return _buildArrivedCard();
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildConsentCard() {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 20),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
Color(0xFFEFF6FF), // blue-50
|
||||
Color(0xFFECFEFF), // cyan-50
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 2,
|
||||
offset: const Offset(0, 1),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFF2563EB), // blue-600
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
LucideIcons.mapPin,
|
||||
size: 16,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Enable Commute Tracking?',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF0F172A), // slate-900
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Share location 1hr before shift so your manager can see you\'re on the way.',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF475569), // slate-600
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: () {
|
||||
setState(() => _localHasConsent = false);
|
||||
},
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
side: const BorderSide(color: Color(0xFFE2E8F0)),
|
||||
),
|
||||
child: const Text('Not Now', style: TextStyle(fontSize: 12)),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
setState(() => _localHasConsent = true);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF2563EB), // blue-600
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
),
|
||||
child: const Text(
|
||||
'Enable',
|
||||
style: TextStyle(fontSize: 12, color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPreShiftCard() {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 20),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 2,
|
||||
offset: const Offset(0, 1),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFFF1F5F9), // slate-100
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
LucideIcons.navigation,
|
||||
size: 16,
|
||||
color: Color(0xFF475569), // slate-600
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Text(
|
||||
'On My Way',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF0F172A), // slate-900
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
LucideIcons.clock,
|
||||
size: 12,
|
||||
color: Color(0xFF64748B), // slate-500
|
||||
),
|
||||
const SizedBox(width: 2),
|
||||
Text(
|
||||
'Shift starts in ${_getMinutesUntilShift()} min',
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
color: Color(0xFF64748B), // slate-500
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const Text(
|
||||
'Track arrival',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Color(0xFF64748B), // slate-500
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Switch(
|
||||
value: _localIsCommuteOn,
|
||||
onChanged: (value) {
|
||||
setState(() => _localIsCommuteOn = value);
|
||||
},
|
||||
activeColor: AppColors.krowBlue,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActiveCommuteScreen() {
|
||||
return Container(
|
||||
height: MediaQuery.of(context).size.height,
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
Color(0xFF2563EB), // blue-600
|
||||
Color(0xFF0891B2), // cyan-600
|
||||
],
|
||||
),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
TweenAnimationBuilder(
|
||||
tween: Tween<double>(begin: 1.0, end: 1.1),
|
||||
duration: const Duration(seconds: 1),
|
||||
curve: Curves.easeInOut,
|
||||
builder: (context, double scale, child) {
|
||||
return Transform.scale(
|
||||
scale: scale,
|
||||
child: Container(
|
||||
width: 96,
|
||||
height: 96,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
LucideIcons.navigation,
|
||||
size: 48,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
onEnd: () {
|
||||
// Restart animation
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const Text(
|
||||
'On My Way',
|
||||
style: TextStyle(
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Your manager can see you\'re heading to the site',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.blue.shade100,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
if (widget.distanceMeters != null) ...[
|
||||
Container(
|
||||
width: double.infinity,
|
||||
constraints: const BoxConstraints(maxWidth: 300),
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
'Distance to Site',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.blue.shade100,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
_formatDistance(widget.distanceMeters!),
|
||||
style: const TextStyle(
|
||||
fontSize: 36,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (widget.etaMinutes != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
constraints: const BoxConstraints(maxWidth: 300),
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
'Estimated Arrival',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.blue.shade100,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${widget.etaMinutes} min',
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
const SizedBox(height: 32),
|
||||
Text(
|
||||
'Most app features are locked while commute mode is on. You\'ll be able to clock in once you arrive.',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.blue.shade100,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: OutlinedButton(
|
||||
onPressed: () {
|
||||
setState(() => _localIsCommuteOn = false);
|
||||
},
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: Colors.white,
|
||||
side: BorderSide(color: Colors.white.withOpacity(0.3)),
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
minimumSize: const Size(double.infinity, 48),
|
||||
),
|
||||
child: const Text('Turn Off Commute Mode'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildArrivedCard() {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 20),
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
Color(0xFFECFDF5), // emerald-50
|
||||
Color(0xFFD1FAE5), // green-50
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 64,
|
||||
height: 64,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFF10B981), // emerald-500
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
LucideIcons.checkCircle,
|
||||
size: 32,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'You\'ve Arrived! 🎉',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF0F172A), // slate-900
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'You\'re at the shift location. Ready to clock in?',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF475569), // slate-600
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
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 [],
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final today = DateTime.now();
|
||||
final dates = List.generate(7, (index) {
|
||||
return today.add(Duration(days: index - 3));
|
||||
});
|
||||
|
||||
return SizedBox(
|
||||
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));
|
||||
|
||||
return Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => onSelect(date),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? const Color(0xFF0032A0) : Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: isSelected
|
||||
? [
|
||||
BoxShadow(
|
||||
color: const Color(0xFF0032A0).withOpacity(0.3),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
]
|
||||
: [],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
DateFormat('d').format(date),
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isSelected
|
||||
? Colors.white
|
||||
: const Color(0xFF0F172A),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
DateFormat('E').format(date),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: isSelected
|
||||
? Colors.white.withOpacity(0.8)
|
||||
: const Color(0xFF94A3B8),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
if (hasShift)
|
||||
Container(
|
||||
width: 6,
|
||||
height: 6,
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? Colors.white
|
||||
: const Color(0xFF0032A0),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
)
|
||||
else if (isToday && !isSelected)
|
||||
Container(
|
||||
width: 6,
|
||||
height: 6,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade300,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
)
|
||||
else
|
||||
const SizedBox(height: 6),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bool _isSameDay(DateTime a, DateTime b) {
|
||||
return a.year == b.year && a.month == b.month && a.day == b.day;
|
||||
}
|
||||
|
||||
String _formatDateIso(DateTime date) {
|
||||
return DateFormat('yyyy-MM-dd').format(date);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
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});
|
||||
|
||||
@override
|
||||
State<LunchBreakDialog> createState() => _LunchBreakDialogState();
|
||||
}
|
||||
|
||||
class _LunchBreakDialogState extends State<LunchBreakDialog> {
|
||||
int _step = 1;
|
||||
bool? _tookLunch;
|
||||
// ignore: unused_field
|
||||
String? _breakStart = '12:00pm';
|
||||
// ignore: unused_field
|
||||
String? _breakEnd = '12:30pm';
|
||||
// ignore: unused_field
|
||||
String? _noLunchReason;
|
||||
// ignore: unused_field
|
||||
String _additionalNotes = '';
|
||||
|
||||
final List<String> _timeOptions = _generateTimeOptions();
|
||||
final List<String> _noLunchReasons = [
|
||||
'Unpredictable Workflows',
|
||||
'Poor Time Management',
|
||||
'Lack of coverage or short Staff',
|
||||
'No Lunch Area',
|
||||
'Other (Please specify)',
|
||||
];
|
||||
|
||||
static List<String> _generateTimeOptions() {
|
||||
List<String> options = [];
|
||||
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';
|
||||
options.add(timeStr);
|
||||
}
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Dialog(
|
||||
backgroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: _buildCurrentStep(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCurrentStep() {
|
||||
switch (_step) {
|
||||
case 1:
|
||||
return _buildStep1();
|
||||
case 2:
|
||||
return _buildStep2();
|
||||
case 102: // 2b: No lunch reason
|
||||
return _buildStep2b();
|
||||
case 3:
|
||||
return _buildStep3();
|
||||
case 4:
|
||||
return _buildStep4();
|
||||
default:
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildStep1() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
LucideIcons.coffee,
|
||||
size: 40,
|
||||
color: Color(0xFF6A7382),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const Text(
|
||||
"Did You Take\na Lunch?",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF121826),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_tookLunch = false;
|
||||
_step = 102; // Go to No Lunch Reason
|
||||
});
|
||||
},
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
side: BorderSide(color: Colors.grey.shade300),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
"No",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF121826),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_tookLunch = true;
|
||||
_step = 2; // Go to Time Input
|
||||
});
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF0032A0),
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
"Yes",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStep2() {
|
||||
// Time input
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
"When did you take lunch?",
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
// Mock Inputs
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: DropdownButtonFormField<String>(
|
||||
value: _breakStart,
|
||||
items: _timeOptions.map((t) => DropdownMenuItem(value: t, child: Text(t))).toList(),
|
||||
onChanged: (v) => setState(() => _breakStart = v),
|
||||
decoration: const InputDecoration(labelText: 'Start'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: DropdownButtonFormField<String>(
|
||||
value: _breakEnd,
|
||||
items: _timeOptions.map((t) => DropdownMenuItem(value: t, child: Text(t))).toList(),
|
||||
onChanged: (v) => setState(() => _breakEnd = v),
|
||||
decoration: const InputDecoration(labelText: 'End'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
setState(() => _step = 3);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF0032A0),
|
||||
minimumSize: const Size(double.infinity, 48),
|
||||
),
|
||||
child: const Text("Next", style: TextStyle(color: Colors.white)),
|
||||
),
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
Widget _buildStep2b() {
|
||||
// No lunch reason
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const Text(
|
||||
"Why didn't you take lunch?",
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
..._noLunchReasons.map((reason) => RadioListTile<String>(
|
||||
title: Text(reason),
|
||||
value: reason,
|
||||
groupValue: _noLunchReason,
|
||||
onChanged: (val) => setState(() => _noLunchReason = val),
|
||||
)),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
setState(() => _step = 3);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF0032A0),
|
||||
minimumSize: const Size(double.infinity, 48),
|
||||
),
|
||||
child: const Text("Next", style: TextStyle(color: Colors.white)),
|
||||
),
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
Widget _buildStep3() {
|
||||
// Additional Notes
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
"Additional Notes",
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
onChanged: (v) => _additionalNotes = v,
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Add any details...',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
maxLines: 3,
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
setState(() => _step = 4);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF0032A0),
|
||||
minimumSize: const Size(double.infinity, 48),
|
||||
),
|
||||
child: const Text("Submit", style: TextStyle(color: Colors.white)),
|
||||
),
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
Widget _buildStep4() {
|
||||
// Success
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(LucideIcons.checkCircle, size: 64, color: Colors.green),
|
||||
const SizedBox(height: 24),
|
||||
const Text(
|
||||
"Break Logged!",
|
||||
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
onPressed: widget.onComplete,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF0032A0),
|
||||
minimumSize: const Size(double.infinity, 48),
|
||||
),
|
||||
child: const Text("Close", style: TextStyle(color: Colors.white)),
|
||||
),
|
||||
],
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
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,
|
||||
this.onCheckIn,
|
||||
this.onCheckOut,
|
||||
this.isLoading = false,
|
||||
this.mode = 'swipe',
|
||||
this.isCheckedIn = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<SwipeToCheckIn> createState() => _SwipeToCheckInState();
|
||||
}
|
||||
|
||||
class _SwipeToCheckInState extends State<SwipeToCheckIn>
|
||||
with SingleTickerProviderStateMixin {
|
||||
double _dragValue = 0.0;
|
||||
final double _handleSize = 48.0;
|
||||
bool _isComplete = false;
|
||||
|
||||
@override
|
||||
void didUpdateWidget(SwipeToCheckIn oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.isCheckedIn != oldWidget.isCheckedIn) {
|
||||
setState(() {
|
||||
_isComplete = false;
|
||||
_dragValue = 0.0;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _onDragUpdate(DragUpdateDetails details, double maxWidth) {
|
||||
if (_isComplete || widget.isLoading) return;
|
||||
setState(() {
|
||||
_dragValue = (_dragValue + details.delta.dx).clamp(
|
||||
0.0,
|
||||
maxWidth - _handleSize - 8,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void _onDragEnd(DragEndDetails details, double maxWidth) {
|
||||
if (_isComplete || widget.isLoading) return;
|
||||
final threshold = (maxWidth - _handleSize - 8) * 0.8;
|
||||
if (_dragValue > threshold) {
|
||||
setState(() {
|
||||
_dragValue = maxWidth - _handleSize - 8;
|
||||
_isComplete = true;
|
||||
});
|
||||
Future.delayed(const Duration(milliseconds: 300), () {
|
||||
if (widget.isCheckedIn) {
|
||||
widget.onCheckOut?.call();
|
||||
} else {
|
||||
widget.onCheckIn?.call();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
_dragValue = 0.0;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final baseColor = widget.isCheckedIn
|
||||
? const Color(0xFF10B981)
|
||||
: const Color(0xFF0032A0);
|
||||
|
||||
if (widget.mode == 'nfc') {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
if (widget.isLoading) return;
|
||||
// Simulate completion for NFC tap
|
||||
Future.delayed(const Duration(milliseconds: 300), () {
|
||||
if (widget.isCheckedIn) {
|
||||
widget.onCheckOut?.call();
|
||||
} else {
|
||||
widget.onCheckIn?.call();
|
||||
}
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
height: 56,
|
||||
decoration: BoxDecoration(
|
||||
color: baseColor,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: baseColor.withOpacity(0.4),
|
||||
blurRadius: 25,
|
||||
offset: const Offset(0, 10),
|
||||
spreadRadius: -5,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(LucideIcons.wifi, color: Colors.white),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
widget.isLoading
|
||||
? (widget.isCheckedIn
|
||||
? "Checking out..."
|
||||
: "Checking in...")
|
||||
: (widget.isCheckedIn ? "NFC Check Out" : "NFC Check In"),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final maxWidth = constraints.maxWidth;
|
||||
final maxDrag = maxWidth - _handleSize - 8;
|
||||
|
||||
// Calculate background color based on drag
|
||||
final progress = _dragValue / maxDrag;
|
||||
final startColor = widget.isCheckedIn
|
||||
? const Color(0xFF10B981)
|
||||
: const Color(0xFF0032A0);
|
||||
final endColor = widget.isCheckedIn
|
||||
? const Color(0xFF0032A0)
|
||||
: const Color(0xFF10B981);
|
||||
final currentColor =
|
||||
Color.lerp(startColor, endColor, progress) ?? startColor;
|
||||
|
||||
return Container(
|
||||
height: 56,
|
||||
decoration: BoxDecoration(
|
||||
color: currentColor,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
Center(
|
||||
child: Opacity(
|
||||
opacity: 1.0 - progress,
|
||||
child: Text(
|
||||
widget.isCheckedIn
|
||||
? "Swipe to Check Out"
|
||||
: "Swipe to Check In",
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_isComplete)
|
||||
Center(
|
||||
child: Text(
|
||||
widget.isCheckedIn ? "Check Out!" : "Check In!",
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 4 + _dragValue,
|
||||
top: 4,
|
||||
child: GestureDetector(
|
||||
onHorizontalDragUpdate: (d) => _onDragUpdate(d, maxWidth),
|
||||
onHorizontalDragEnd: (d) => _onDragEnd(d, maxWidth),
|
||||
child: Container(
|
||||
width: _handleSize,
|
||||
height: _handleSize,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 2,
|
||||
offset: const Offset(0, 1),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
_isComplete
|
||||
? LucideIcons.check
|
||||
: LucideIcons.arrowRight,
|
||||
color: startColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||
import 'data/repositories/clock_in_repository_impl.dart';
|
||||
import 'domain/repositories/clock_in_repository_interface.dart';
|
||||
import 'domain/usecases/clock_in_usecase.dart';
|
||||
import 'domain/usecases/clock_out_usecase.dart';
|
||||
import 'domain/usecases/get_activity_log_usecase.dart';
|
||||
import 'domain/usecases/get_attendance_status_usecase.dart';
|
||||
import 'domain/usecases/get_todays_shift_usecase.dart';
|
||||
import 'presentation/bloc/clock_in_bloc.dart';
|
||||
import 'presentation/pages/clock_in_page.dart';
|
||||
|
||||
class StaffClockInModule extends Module {
|
||||
@override
|
||||
void binds(Injector i) {
|
||||
// Data Sources (Mocks from data_connect)
|
||||
i.add<ShiftsRepositoryMock>(ShiftsRepositoryMock.new);
|
||||
|
||||
// Repositories
|
||||
i.add<ClockInRepositoryInterface>(ClockInRepositoryImpl.new);
|
||||
|
||||
// Use Cases
|
||||
i.add<GetTodaysShiftUseCase>(GetTodaysShiftUseCase.new);
|
||||
i.add<GetAttendanceStatusUseCase>(GetAttendanceStatusUseCase.new);
|
||||
i.add<ClockInUseCase>(ClockInUseCase.new);
|
||||
i.add<ClockOutUseCase>(ClockOutUseCase.new);
|
||||
i.add<GetActivityLogUseCase>(GetActivityLogUseCase.new);
|
||||
|
||||
// BLoC
|
||||
i.add<ClockInBloc>(ClockInBloc.new);
|
||||
}
|
||||
|
||||
@override
|
||||
void routes(r) {
|
||||
r.child('/', child: (context) => const ClockInPage());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
library staff_clock_in;
|
||||
|
||||
export 'src/staff_clock_in_module.dart';
|
||||
export 'src/presentation/pages/clock_in_page.dart';
|
||||
30
apps/mobile/packages/features/staff/clock_in/pubspec.yaml
Normal file
30
apps/mobile/packages/features/staff/clock_in/pubspec.yaml
Normal file
@@ -0,0 +1,30 @@
|
||||
name: staff_clock_in
|
||||
description: Staff Clock In Feature
|
||||
version: 0.0.1
|
||||
publish_to: 'none'
|
||||
resolution: workspace
|
||||
|
||||
environment:
|
||||
sdk: '>=3.0.0 <4.0.0'
|
||||
flutter: ">=1.17.0"
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_bloc: ^8.1.3
|
||||
equatable: ^2.0.5
|
||||
intl: ^0.20.2
|
||||
lucide_icons: ^0.257.0
|
||||
flutter_modular: ^6.3.2
|
||||
|
||||
# Internal packages
|
||||
core_localization:
|
||||
path: ../../../core_localization
|
||||
design_system:
|
||||
path: ../../../design_system
|
||||
krow_domain:
|
||||
path: ../../../domain
|
||||
krow_data_connect:
|
||||
path: ../../../data_connect
|
||||
krow_core:
|
||||
path: ../../../core
|
||||
@@ -14,6 +14,7 @@ import 'package:staff_shifts/staff_shifts.dart';
|
||||
import 'package:staff_payments/staff_payements.dart';
|
||||
import 'package:staff_time_card/staff_time_card.dart';
|
||||
import 'package:staff_availability/staff_availability.dart';
|
||||
import 'package:staff_clock_in/staff_clock_in.dart';
|
||||
|
||||
import 'package:staff_main/src/presentation/blocs/staff_main_cubit.dart';
|
||||
import 'package:staff_main/src/presentation/constants/staff_main_routes.dart';
|
||||
@@ -44,10 +45,9 @@ class StaffMainModule extends Module {
|
||||
StaffMainRoutes.home,
|
||||
module: StaffHomeModule(),
|
||||
),
|
||||
ChildRoute<dynamic>(
|
||||
ModuleRoute<dynamic>(
|
||||
StaffMainRoutes.clockIn,
|
||||
child: (BuildContext context) =>
|
||||
const PlaceholderPage(title: 'Clock In'),
|
||||
module: StaffClockInModule(),
|
||||
),
|
||||
ModuleRoute<dynamic>(
|
||||
StaffMainRoutes.profile,
|
||||
@@ -74,5 +74,9 @@ class StaffMainModule extends Module {
|
||||
module: StaffTimeCardModule(),
|
||||
);
|
||||
r.module('/availability', module: StaffAvailabilityModule());
|
||||
r.module(
|
||||
'/clock-in',
|
||||
module: StaffClockInModule(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,9 @@ dependencies:
|
||||
path: ../profile_sections/finances/time_card
|
||||
staff_availability:
|
||||
path: ../availability
|
||||
|
||||
staff_clock_in:
|
||||
path: ../clock_in
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
@@ -1100,6 +1100,13 @@ packages:
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.0.1"
|
||||
staff_clock_in:
|
||||
dependency: transitive
|
||||
description:
|
||||
path: "packages/features/staff/clock_in"
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.0.1"
|
||||
staff_documents:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
Reference in New Issue
Block a user