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
|
/// Locales: 2
|
||||||
/// Strings: 1026 (513 per locale)
|
/// 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
|
// coverage:ignore-file
|
||||||
// ignore_for_file: type=lint, unused_import
|
// 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_payments/staff_payements.dart';
|
||||||
import 'package:staff_time_card/staff_time_card.dart';
|
import 'package:staff_time_card/staff_time_card.dart';
|
||||||
import 'package:staff_availability/staff_availability.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/blocs/staff_main_cubit.dart';
|
||||||
import 'package:staff_main/src/presentation/constants/staff_main_routes.dart';
|
import 'package:staff_main/src/presentation/constants/staff_main_routes.dart';
|
||||||
@@ -44,10 +45,9 @@ class StaffMainModule extends Module {
|
|||||||
StaffMainRoutes.home,
|
StaffMainRoutes.home,
|
||||||
module: StaffHomeModule(),
|
module: StaffHomeModule(),
|
||||||
),
|
),
|
||||||
ChildRoute<dynamic>(
|
ModuleRoute<dynamic>(
|
||||||
StaffMainRoutes.clockIn,
|
StaffMainRoutes.clockIn,
|
||||||
child: (BuildContext context) =>
|
module: StaffClockInModule(),
|
||||||
const PlaceholderPage(title: 'Clock In'),
|
|
||||||
),
|
),
|
||||||
ModuleRoute<dynamic>(
|
ModuleRoute<dynamic>(
|
||||||
StaffMainRoutes.profile,
|
StaffMainRoutes.profile,
|
||||||
@@ -74,5 +74,9 @@ class StaffMainModule extends Module {
|
|||||||
module: StaffTimeCardModule(),
|
module: StaffTimeCardModule(),
|
||||||
);
|
);
|
||||||
r.module('/availability', module: StaffAvailabilityModule());
|
r.module('/availability', module: StaffAvailabilityModule());
|
||||||
|
r.module(
|
||||||
|
'/clock-in',
|
||||||
|
module: StaffClockInModule(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,9 @@ dependencies:
|
|||||||
path: ../profile_sections/finances/time_card
|
path: ../profile_sections/finances/time_card
|
||||||
staff_availability:
|
staff_availability:
|
||||||
path: ../availability
|
path: ../availability
|
||||||
|
staff_clock_in:
|
||||||
|
path: ../clock_in
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|||||||
@@ -1100,6 +1100,13 @@ packages:
|
|||||||
relative: true
|
relative: true
|
||||||
source: path
|
source: path
|
||||||
version: "0.0.1"
|
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:
|
staff_documents:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
Reference in New Issue
Block a user