362 lines
12 KiB
Dart
362 lines
12 KiB
Dart
![]() |
import 'dart:async';
|
||
|
import 'package:flutter/material.dart';
|
||
|
import 'package:intl/intl.dart';
|
||
|
import 'package:my_attendance/screens/history_screen.dart';
|
||
|
import 'package:my_attendance/screens/leave_request_screen.dart';
|
||
|
import 'package:my_attendance/screens/correction_screen.dart';
|
||
|
import 'package:my_attendance/services/attendance_service.dart';
|
||
|
import 'package:my_attendance/services/settings_service.dart';
|
||
|
import 'package:my_attendance/widgets/settings_dialog.dart';
|
||
|
import 'package:my_attendance/widgets/info_dialog.dart';
|
||
|
import 'package:provider/provider.dart';
|
||
|
import 'package:my_attendance/models/attendance_entry.dart';
|
||
|
import 'package:my_attendance/widgets/stats_dialog.dart';
|
||
|
|
||
|
class HomeScreen extends StatefulWidget {
|
||
|
const HomeScreen({super.key});
|
||
|
|
||
|
@override
|
||
|
State<HomeScreen> createState() => _HomeScreenState();
|
||
|
}
|
||
|
|
||
|
class _HomeScreenState extends State<HomeScreen> {
|
||
|
Timer? _timer;
|
||
|
|
||
|
@override
|
||
|
void initState() {
|
||
|
super.initState();
|
||
|
// Periodically update the UI to check if logout is available
|
||
|
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||
|
if (mounted) {
|
||
|
setState(() {});
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
@override
|
||
|
void dispose() {
|
||
|
_timer?.cancel();
|
||
|
super.dispose();
|
||
|
}
|
||
|
|
||
|
void _showSettingsDialog() {
|
||
|
showDialog(context: context, builder: (context) => const SettingsDialog());
|
||
|
}
|
||
|
|
||
|
@override
|
||
|
Widget build(BuildContext context) {
|
||
|
final attendanceService = Provider.of<AttendanceService>(context);
|
||
|
final settingsService = Provider.of<SettingsService>(context);
|
||
|
|
||
|
final todayEntry = attendanceService.todayEntry;
|
||
|
final isLoggedIn = todayEntry != null &&
|
||
|
todayEntry.entryType == EntryType.work &&
|
||
|
todayEntry.logoutTime == null;
|
||
|
final hasLoggedOut = todayEntry != null &&
|
||
|
todayEntry.entryType == EntryType.work &&
|
||
|
todayEntry.logoutTime != null;
|
||
|
final isDayFinalized = todayEntry != null &&
|
||
|
(todayEntry.entryType == EntryType.leave ||
|
||
|
todayEntry.entryType == EntryType.wfh);
|
||
|
|
||
|
bool canLogOut = false;
|
||
|
if (isLoggedIn) {
|
||
|
final workDuration = settingsService.settings.dailyWorkDuration;
|
||
|
if (todayEntry.loginTime != null) {
|
||
|
final timeWorked = DateTime.now().difference(todayEntry.loginTime!);
|
||
|
canLogOut = timeWorked >= workDuration;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return Scaffold(
|
||
|
appBar: AppBar(
|
||
|
title: const Text('My Attendance'),
|
||
|
actions: [
|
||
|
IconButton(
|
||
|
icon: const Icon(Icons.history),
|
||
|
onPressed: () =>
|
||
|
Navigator.of(context).pushNamed(HistoryScreen.routeName),
|
||
|
),
|
||
|
IconButton(
|
||
|
icon: const Icon(Icons.bar_chart),
|
||
|
onPressed: () async {
|
||
|
await Provider.of<AttendanceService>(
|
||
|
context,
|
||
|
listen: false,
|
||
|
).calculateStats();
|
||
|
showDialog(context: context, builder: (_) => const StatsDialog());
|
||
|
},
|
||
|
),
|
||
|
IconButton(
|
||
|
icon: const Icon(Icons.edit_calendar_outlined),
|
||
|
onPressed: () =>
|
||
|
Navigator.of(context).pushNamed(CorrectionScreen.routeName),
|
||
|
),
|
||
|
PopupMenuButton<String>(
|
||
|
icon: const Icon(Icons.settings),
|
||
|
onSelected: (value) {
|
||
|
if (value == 'working_hour') {
|
||
|
_showSettingsDialog();
|
||
|
} else if (value == 'info') {
|
||
|
showDialog(
|
||
|
context: context,
|
||
|
builder: (context) => const InfoDialog(),
|
||
|
);
|
||
|
}
|
||
|
},
|
||
|
itemBuilder: (context) => [
|
||
|
const PopupMenuItem(
|
||
|
value: 'working_hour',
|
||
|
child: Text('Working Hour'),
|
||
|
),
|
||
|
const PopupMenuItem(
|
||
|
value: 'info',
|
||
|
child: Text('Info'),
|
||
|
),
|
||
|
],
|
||
|
),
|
||
|
],
|
||
|
),
|
||
|
body: SafeArea(
|
||
|
child: Center(
|
||
|
child: Padding(
|
||
|
padding: const EdgeInsets.all(24.0),
|
||
|
child: Column(
|
||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||
|
children: [
|
||
|
if (attendanceService.isLoading && todayEntry == null)
|
||
|
const Center(child: CircularProgressIndicator())
|
||
|
else
|
||
|
Container(
|
||
|
padding: const EdgeInsets.all(24),
|
||
|
decoration: BoxDecoration(
|
||
|
gradient: LinearGradient(
|
||
|
colors: [
|
||
|
Theme.of(context).colorScheme.surfaceContainerHighest,
|
||
|
Theme.of(context).colorScheme.surfaceContainer,
|
||
|
],
|
||
|
begin: Alignment.topLeft,
|
||
|
end: Alignment.bottomRight,
|
||
|
),
|
||
|
borderRadius: BorderRadius.circular(16),
|
||
|
),
|
||
|
child: isLoggedIn
|
||
|
? _buildLoggedInUI(
|
||
|
context,
|
||
|
todayEntry.loginTime!,
|
||
|
settingsService.settings.dailyWorkDuration,
|
||
|
)
|
||
|
: hasLoggedOut
|
||
|
? _buildLoggedOutUI(context, todayEntry)
|
||
|
: isDayFinalized
|
||
|
? _buildFinalizedUI(context, todayEntry)
|
||
|
: _buildNotLoggedInUI(context),
|
||
|
),
|
||
|
const SizedBox(height: 32),
|
||
|
ElevatedButton.icon(
|
||
|
onPressed: isLoggedIn || hasLoggedOut || isDayFinalized
|
||
|
? null
|
||
|
: attendanceService.logIn,
|
||
|
style: ElevatedButton.styleFrom(
|
||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||
|
textStyle: const TextStyle(
|
||
|
fontSize: 18,
|
||
|
fontWeight: FontWeight.bold,
|
||
|
),
|
||
|
shape: RoundedRectangleBorder(
|
||
|
borderRadius: BorderRadius.circular(12),
|
||
|
),
|
||
|
),
|
||
|
icon: const Icon(Icons.login),
|
||
|
label: const Text('Log In'),
|
||
|
),
|
||
|
const SizedBox(height: 16),
|
||
|
ElevatedButton.icon(
|
||
|
onPressed:
|
||
|
isLoggedIn && canLogOut ? attendanceService.logOut : null,
|
||
|
style: ElevatedButton.styleFrom(
|
||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||
|
backgroundColor: Theme.of(context).colorScheme.error,
|
||
|
foregroundColor: Theme.of(context).colorScheme.onError,
|
||
|
textStyle: const TextStyle(
|
||
|
fontSize: 18,
|
||
|
fontWeight: FontWeight.bold,
|
||
|
),
|
||
|
shape: RoundedRectangleBorder(
|
||
|
borderRadius: BorderRadius.circular(12),
|
||
|
),
|
||
|
),
|
||
|
icon: const Icon(Icons.logout),
|
||
|
label: const Text('Log Out'),
|
||
|
),
|
||
|
const SizedBox(height: 24),
|
||
|
const Divider(),
|
||
|
const SizedBox(height: 24),
|
||
|
Row(
|
||
|
children: [
|
||
|
Expanded(
|
||
|
child: OutlinedButton.icon(
|
||
|
onPressed: isDayFinalized || isLoggedIn || hasLoggedOut
|
||
|
? null
|
||
|
: () {
|
||
|
Navigator.of(
|
||
|
context,
|
||
|
).pushNamed(LeaveRequestScreen.routeName);
|
||
|
},
|
||
|
icon: const Icon(Icons.beach_access),
|
||
|
label: const Text('Request Leave'),
|
||
|
style: OutlinedButton.styleFrom(
|
||
|
shape: RoundedRectangleBorder(
|
||
|
borderRadius: BorderRadius.circular(12),
|
||
|
),
|
||
|
),
|
||
|
),
|
||
|
),
|
||
|
const SizedBox(width: 16),
|
||
|
Expanded(
|
||
|
child: OutlinedButton.icon(
|
||
|
onPressed: isDayFinalized || isLoggedIn || hasLoggedOut
|
||
|
? null
|
||
|
: attendanceService.markAsWfh,
|
||
|
icon: const Icon(Icons.home_work),
|
||
|
label: const Text('Work From Home'),
|
||
|
style: OutlinedButton.styleFrom(
|
||
|
shape: RoundedRectangleBorder(
|
||
|
borderRadius: BorderRadius.circular(12),
|
||
|
),
|
||
|
),
|
||
|
),
|
||
|
),
|
||
|
],
|
||
|
),
|
||
|
],
|
||
|
),
|
||
|
),
|
||
|
),
|
||
|
),
|
||
|
);
|
||
|
}
|
||
|
|
||
|
Widget _buildLoggedInUI(
|
||
|
BuildContext context,
|
||
|
DateTime loginTime,
|
||
|
Duration workDuration,
|
||
|
) {
|
||
|
final now = DateTime.now();
|
||
|
final timeWorked = now.difference(loginTime);
|
||
|
final remaining = workDuration - timeWorked;
|
||
|
|
||
|
return Column(
|
||
|
children: [
|
||
|
Text(
|
||
|
'Logged in at: ${DateFormat.jm().format(loginTime)}',
|
||
|
style: Theme.of(context).textTheme.titleLarge,
|
||
|
textAlign: TextAlign.center,
|
||
|
),
|
||
|
const SizedBox(height: 24),
|
||
|
Text('Time Worked', style: Theme.of(context).textTheme.bodyMedium),
|
||
|
Text(
|
||
|
_formatDuration(timeWorked),
|
||
|
style: Theme.of(
|
||
|
context,
|
||
|
).textTheme.displaySmall?.copyWith(fontWeight: FontWeight.bold),
|
||
|
),
|
||
|
const SizedBox(height: 16),
|
||
|
if (remaining > Duration.zero) ...[
|
||
|
Text('Time Remaining', style: Theme.of(context).textTheme.bodyMedium),
|
||
|
Text(
|
||
|
_formatDuration(remaining),
|
||
|
style: Theme.of(context).textTheme.headlineMedium,
|
||
|
),
|
||
|
],
|
||
|
],
|
||
|
);
|
||
|
}
|
||
|
|
||
|
Widget _buildLoggedOutUI(BuildContext context, AttendanceEntry entry) {
|
||
|
if (entry.loginTime == null || entry.logoutTime == null) {
|
||
|
return const Text("Data Error"); // Should not happen for this UI state
|
||
|
}
|
||
|
final totalWork = entry.logoutTime!.difference(entry.loginTime!);
|
||
|
return Column(
|
||
|
children: [
|
||
|
Text(
|
||
|
'Checked out for today.',
|
||
|
style: Theme.of(context).textTheme.titleLarge,
|
||
|
textAlign: TextAlign.center,
|
||
|
),
|
||
|
const SizedBox(height: 16),
|
||
|
Text('Total Work', style: Theme.of(context).textTheme.bodyMedium),
|
||
|
Text(
|
||
|
_formatDuration(totalWork),
|
||
|
style: Theme.of(
|
||
|
context,
|
||
|
).textTheme.displaySmall?.copyWith(fontWeight: FontWeight.bold),
|
||
|
),
|
||
|
],
|
||
|
);
|
||
|
}
|
||
|
|
||
|
Widget _buildFinalizedUI(BuildContext context, AttendanceEntry entry) {
|
||
|
String message = 'Status for today:';
|
||
|
String status = '';
|
||
|
|
||
|
if (entry.entryType == EntryType.wfh) {
|
||
|
status = 'Work From Home';
|
||
|
} else if (entry.entryType == EntryType.leave) {
|
||
|
switch (entry.leaveType) {
|
||
|
case LeaveType.sick:
|
||
|
status = 'On Sick Leave';
|
||
|
break;
|
||
|
case LeaveType.annual:
|
||
|
status = 'On Annual Leave';
|
||
|
break;
|
||
|
case LeaveType.nationalHoliday:
|
||
|
status = 'National Holiday';
|
||
|
break;
|
||
|
case LeaveType.companyWfh:
|
||
|
status = 'Company WFH';
|
||
|
break;
|
||
|
default:
|
||
|
status = 'On Leave';
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return Column(
|
||
|
children: [
|
||
|
Text(
|
||
|
message,
|
||
|
style: Theme.of(context).textTheme.titleLarge,
|
||
|
textAlign: TextAlign.center,
|
||
|
),
|
||
|
const SizedBox(height: 16),
|
||
|
Text(
|
||
|
status,
|
||
|
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||
|
fontWeight: FontWeight.bold,
|
||
|
color: Theme.of(context).colorScheme.primary,
|
||
|
),
|
||
|
textAlign: TextAlign.center,
|
||
|
),
|
||
|
],
|
||
|
);
|
||
|
}
|
||
|
|
||
|
Widget _buildNotLoggedInUI(BuildContext context) {
|
||
|
return Text(
|
||
|
'Not logged in for today.',
|
||
|
style: Theme.of(context).textTheme.headlineMedium,
|
||
|
textAlign: TextAlign.center,
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
String _formatDuration(Duration d) {
|
||
|
d = Duration(seconds: d.inSeconds); // Truncate to seconds
|
||
|
final hours = d.inHours.toString().padLeft(2, '0');
|
||
|
final minutes = d.inMinutes.remainder(60).toString().padLeft(2, '0');
|
||
|
final seconds = d.inSeconds.remainder(60).toString().padLeft(2, '0');
|
||
|
return '$hours:$minutes:$seconds';
|
||
|
}
|