feat: integrate ClockInPageLoaded event to initialize state on ClockInBloc

This commit is contained in:
Achintha Isuru
2026-01-30 16:49:10 -05:00
parent f1ccc97fae
commit 9038d6533e
2 changed files with 405 additions and 398 deletions

View File

@@ -33,6 +33,8 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
on<CheckInRequested>(_onCheckIn);
on<CheckOutRequested>(_onCheckOut);
on<CheckInModeChanged>(_onModeChanged);
add(ClockInPageLoaded());
}
AttendanceStatus _mapToStatus(Map<String, dynamic> map) {

View File

@@ -1,18 +1,19 @@
import 'package:design_system/design_system.dart';
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 'package:lucide_icons/lucide_icons.dart';
import '../bloc/clock_in_bloc.dart';
import '../bloc/clock_in_event.dart';
import '../bloc/clock_in_state.dart';
import '../theme/app_colors.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';
import '../widgets/date_selector.dart';
import '../widgets/lunch_break_modal.dart';
import '../widgets/swipe_to_check_in.dart';
class ClockInPage extends StatefulWidget {
const ClockInPage({super.key});
@@ -28,23 +29,24 @@ class _ClockInPageState extends State<ClockInPage> {
void initState() {
super.initState();
_bloc = Modular.get<ClockInBloc>();
_bloc.add(ClockInPageLoaded());
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
return BlocProvider<ClockInBloc>.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!)),
);
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) {
if (state.status == ClockInStatus.loading &&
state.todayShift == null) {
return const Scaffold(
body: Center(child: CircularProgressIndicator()),
);
@@ -64,416 +66,408 @@ class _ClockInPageState extends State<ClockInPage> {
: '--:-- --';
return Scaffold(
backgroundColor: Colors.transparent,
body: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color(0xFFF8FAFC), // slate-50
Colors.white,
],
),
appBar: UiAppBar(
titleWidget: Text(
'Clock In to your Shift',
style: UiTypography.title1m.textPrimary,
),
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()),
],
showBackButton: false,
centerTitle: false,
),
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.only(
bottom: UiConstants.space24,
top: UiConstants.space6,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
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
),
const SizedBox(height: 20),
// 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,
),
// 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,
),
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",
// TODO: Connect to Data Connect when 'breakDuration' field is added to Shift schema.
value: "00:30 min",
subtitle: "Scheduled 00:30 min",
),
const AttendanceCard(
type: AttendanceType.days,
title: "Total Days",
// TODO: Connect to Data Connect when 'staffStats' or similar aggregation API is available.
// Currently avoided to prevent fetching full shift history for a simple count.
value: "28",
subtitle: "Working Days",
),
],
),
const SizedBox(height: 24),
// Your Activity Header
const Text(
"Your Activity",
textAlign: TextAlign.start,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColors.krowCharcoal,
),
),
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: [
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",
// TODO: Connect to Data Connect when 'breakDuration' field is added to Shift schema.
value: "00:30 min",
subtitle: "Scheduled 00:30 min",
),
const AttendanceCard(
type: AttendanceType.days,
title: "Total Days",
// TODO: Connect to Data Connect when 'staffStats' or similar aggregation API is available.
// Currently avoided to prevent fetching full shift history for a simple count.
value: "28",
subtitle: "Working Days",
_buildModeTab(
"Swipe",
LucideIcons.mapPin,
'swipe',
state.checkInMode,
),
// _buildModeTab("NFC Tap", LucideIcons.wifi, 'nfc', state.checkInMode),
],
),
const SizedBox(height: 24),
),
const SizedBox(height: 16),
// Your Activity Header
// Your Activity Header
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
"Your Activity",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColors.krowCharcoal,
// 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),
),
),
GestureDetector(
onTap: () {
debugPrint('Navigating to shifts...');
},
child: Row(
children: const [
Text(
"View all",
],
),
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(
color: AppColors.krowBlue,
fontSize: 12,
fontWeight: FontWeight.w500,
fontSize: 14,
color: Color(0xFF475569), // slate-600
),
),
SizedBox(width: 4),
Icon(
LucideIcons.chevronRight,
size: 16,
color: AppColors.krowBlue,
Text(
"\$${todayShift.hourlyRate}/hr",
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
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());
},
),
// 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
),
] 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,
),
child: Column(
children: [
Container(
width: 48,
height: 48,
decoration: const BoxDecoration(
color: Color(0xFFD1FAE5), // emerald-100
shape: BoxShape.circle,
),
const SizedBox(height: 12),
const Text(
"Shift Completed!",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Color(0xFF065F46), // emerald-800
),
child: const Icon(
LucideIcons.check,
color: Color(0xFF059669), // emerald-600
size: 24,
),
const SizedBox(height: 4),
const Text(
"Great work today",
style: TextStyle(
fontSize: 14,
color: Color(0xFF059669), // emerald-600
),
),
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,
),
],
),
),
] 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),
),
// 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),
),
),
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),
),
],
),
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),
const SizedBox(height: 16),
// Recent Activity List
if (state.activityLog.isNotEmpty) ...state.activityLog.map(
// Recent Activity List
if (state.activityLog.isNotEmpty)
...state.activityLog.map(
(activity) => Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(12),
@@ -490,7 +484,9 @@ class _ClockInPageState extends State<ClockInPage> {
width: 40,
height: 40,
decoration: BoxDecoration(
color: AppColors.krowBlue.withOpacity(0.1),
color: AppColors.krowBlue.withOpacity(
0.1,
),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
@@ -502,23 +498,28 @@ class _ClockInPageState extends State<ClockInPage> {
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
DateFormat(
'MMM d',
).format(activity['date'] as DateTime),
DateFormat('MMM d').format(
activity['date'] as DateTime,
),
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Color(0xFF0F172A), // slate-900
color: Color(
0xFF0F172A,
), // slate-900
),
),
Text(
"${activity['start']} - ${activity['end']}",
style: const TextStyle(
fontSize: 12,
color: Color(0xFF64748B), // slate-500
color: Color(
0xFF64748B,
), // slate-500
),
),
],
@@ -542,7 +543,6 @@ class _ClockInPageState extends State<ClockInPage> {
),
],
),
),
),
),
);
@@ -551,7 +551,12 @@ class _ClockInPageState extends State<ClockInPage> {
);
}
Widget _buildModeTab(String label, IconData icon, String value, String currentMode) {
Widget _buildModeTab(
String label,
IconData icon,
String value,
String currentMode,
) {
final isSelected = currentMode == value;
return Expanded(
child: GestureDetector(
@@ -678,7 +683,7 @@ class _ClockInPageState extends State<ClockInPage> {
Future<void> _showNFCDialog(BuildContext context) async {
bool scanned = false;
// Using a local navigator context since we are in a dialog
await showDialog(
context: context,
@@ -771,11 +776,11 @@ class _ClockInPageState extends State<ClockInPage> {
);
},
);
// 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));
_bloc.add(CheckInRequested(shiftId: _bloc.state.todayShift!.id));
}
}
}