842 lines
37 KiB
Dart

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:marco/helpers/theme/app_theme.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
import 'package:marco/helpers/utils/my_shadow.dart';
import 'package:marco/helpers/widgets/my_card.dart';
import 'package:marco/helpers/widgets/my_container.dart';
import 'package:marco/helpers/widgets/my_flex.dart';
import 'package:marco/helpers/widgets/my_flex_item.dart';
import 'package:marco/helpers/widgets/my_spacing.dart';
import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/controller/dashboard/attendance_screen_controller.dart';
import 'package:marco/controller/permission_controller.dart';
import 'package:intl/intl.dart';
import 'package:marco/helpers/widgets/avatar.dart';
import 'package:marco/model/attendance/log_details_view.dart';
import 'package:marco/model/attendance/attendence_action_button.dart';
import 'package:marco/model/attendance/regualrize_action_button.dart';
import 'package:marco/model/attendance/attendence_filter_sheet.dart';
import 'package:marco/controller/project_controller.dart';
class AttendanceScreen extends StatefulWidget {
AttendanceScreen({super.key});
@override
State<AttendanceScreen> createState() => _AttendanceScreenState();
}
class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
final AttendanceController attendanceController =
Get.put(AttendanceController());
final PermissionController permissionController =
Get.put(PermissionController());
String selectedTab = 'todaysAttendance';
@override
void initState() {
super.initState();
final projectController = Get.find<ProjectController>();
final attendanceController = Get.find<AttendanceController>();
WidgetsBinding.instance.addPostFrameCallback((_) async {
// Listen for future changes in selected project
ever<String?>(projectController.selectedProjectId!, (projectId) async {
if (projectId != null && projectId.isNotEmpty) {
try {
await attendanceController.loadAttendanceData(projectId);
attendanceController.update(['attendance_dashboard_controller']);
} catch (e) {
debugPrint("Error updating data on project change: $e");
}
}
});
// Load data initially if project is already selected
final initialProjectId = projectController.selectedProjectId?.value;
if (initialProjectId != null && initialProjectId.isNotEmpty) {
try {
await attendanceController.loadAttendanceData(initialProjectId);
attendanceController.update(['attendance_dashboard_controller']);
} catch (e) {
debugPrint("Error loading initial data: $e");
}
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: PreferredSize(
preferredSize: const Size.fromHeight(80),
child: AppBar(
backgroundColor: const Color(0xFFF5F5F5),
elevation: 0.5,
foregroundColor: Colors.black,
titleSpacing: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios_new,
color: Colors.black, size: 20),
onPressed: () {
Get.offNamed('/dashboard');
},
),
title: Padding(
padding: const EdgeInsets.only(top: 12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
MyText.titleLarge(
'Attendance',
fontWeight: 700,
color: Colors.black,
),
const SizedBox(height: 4),
GetBuilder<ProjectController>(
builder: (projectController) {
final projectName =
projectController.selectedProject?.name ??
'Select Project';
return MyText.bodySmall(
projectName,
fontWeight: 600,
maxLines: 1,
overflow: TextOverflow.ellipsis,
color: Colors.grey[700],
);
},
),
],
),
),
),
),
body: SafeArea(
child: SingleChildScrollView(
padding: MySpacing.x(0),
child: GetBuilder<AttendanceController>(
init: attendanceController,
tag: 'attendance_dashboard_controller',
builder: (controller) {
final selectedProjectId =
Get.find<ProjectController>().selectedProjectId?.value;
final bool noProjectSelected =
selectedProjectId == null || selectedProjectId.isEmpty;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MySpacing.height(flexSpacing),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
MyText.bodyMedium("Filter", fontWeight: 600),
Tooltip(
message: 'Filter Project',
child: InkWell(
borderRadius: BorderRadius.circular(24),
onTap: () async {
final result = await showModalBottomSheet<
Map<String, dynamic>>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.white,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12)),
),
builder: (context) => AttendanceFilterBottomSheet(
controller: attendanceController,
permissionController: permissionController,
selectedTab: selectedTab,
),
);
if (result != null) {
final selectedProjectId =
Get.find<ProjectController>()
.selectedProjectId
?.value;
final selectedView =
result['selectedTab'] as String?;
if (selectedProjectId != null) {
try {
await attendanceController
.fetchEmployeesByProject(
selectedProjectId);
await attendanceController
.fetchAttendanceLogs(selectedProjectId);
await attendanceController
.fetchRegularizationLogs(
selectedProjectId);
await attendanceController
.fetchProjectData(selectedProjectId);
} catch (_) {}
attendanceController.update(
['attendance_dashboard_controller']);
}
if (selectedView != null &&
selectedView != selectedTab) {
setState(() {
selectedTab = selectedView;
});
}
}
},
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(
Icons.filter_list_alt,
color: Colors.blueAccent,
size: 28,
),
),
),
),
),
const SizedBox(width: 4),
MyText.bodyMedium("Refresh", fontWeight: 600),
Tooltip(
message: 'Refresh Data',
child: InkWell(
borderRadius: BorderRadius.circular(24),
onTap: () async {
final projectId = Get.find<ProjectController>()
.selectedProjectId
?.value;
if (projectId != null && projectId.isNotEmpty) {
try {
await attendanceController
.loadAttendanceData(projectId);
attendanceController.update(
['attendance_dashboard_controller']);
} catch (e) {
debugPrint("Error refreshing data: $e");
}
}
},
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(
Icons.refresh,
color: Colors.green,
size: 28,
),
),
),
),
),
],
),
MySpacing.height(flexSpacing),
MyFlex(children: [
MyFlexItem(
sizes: 'lg-12 md-12 sm-12',
child: noProjectSelected
? Center(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: MyText.titleMedium(
'No Records Found',
fontWeight: 600,
color: Colors.grey[600],
),
),
)
: selectedTab == 'todaysAttendance'
? employeeListTab()
: selectedTab == 'attendanceLogs'
? employeeLog()
: regularizationScreen(),
),
]),
],
);
},
),
),
),
);
}
String _formatDate(DateTime date) {
return "${date.day}/${date.month}/${date.year}";
}
Widget employeeListTab() {
return Obx(() {
final isLoading = attendanceController.isLoadingEmployees.value;
final employees = attendanceController.employees;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
child: Row(
children: [
Expanded(
child: MyText.titleMedium(
"Today's Attendance",
fontWeight: 600,
),
),
MyText.bodySmall(
_formatDate(DateTime.now()),
fontWeight: 600,
color: Colors.grey[700],
overflow: TextOverflow.ellipsis,
),
],
),
),
if (isLoading)
const SizedBox(
height: 120,
child: Center(child: CircularProgressIndicator()),
)
else if (employees.isEmpty)
SizedBox(
height: 120,
child: Center(
child: MyText.bodySmall(
"No Employees Assigned to This Project",
fontWeight: 600,
),
),
)
else
MyCard.bordered(
borderRadiusAll: 4,
border: Border.all(color: Colors.grey.withOpacity(0.2)),
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
paddingAll: 8,
child: Column(
children: List.generate(employees.length, (index) {
final employee = employees[index];
return Column(
children: [
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: MyContainer(
paddingAll: 5,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Avatar(
firstName: employee.firstName,
lastName: employee.lastName,
size: 31,
),
MySpacing.width(16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Wrap(
crossAxisAlignment:
WrapCrossAlignment.center,
spacing:
6, // spacing between name and designation
children: [
MyText.bodyMedium(
employee.name,
fontWeight: 600,
overflow: TextOverflow.visible,
maxLines: null,
),
MyText.bodySmall(
'(${employee.designation})',
fontWeight: 600,
overflow: TextOverflow.visible,
maxLines: null,
color: Colors.grey[700],
),
],
),
MySpacing.height(8),
(employee.checkIn != null ||
employee.checkOut != null)
? Row(
children: [
if (employee.checkIn != null) ...[
const Icon(
Icons.arrow_circle_right,
size: 16,
color: Colors.green),
MySpacing.width(4),
Expanded(
child: MyText.bodySmall(
DateFormat('hh:mm a')
.format(
employee.checkIn!),
fontWeight: 600,
overflow:
TextOverflow.ellipsis,
),
),
MySpacing.width(16),
],
if (employee.checkOut !=
null) ...[
const Icon(
Icons.arrow_circle_left,
size: 16,
color: Colors.red),
MySpacing.width(4),
Expanded(
child: MyText.bodySmall(
DateFormat('hh:mm a')
.format(
employee.checkOut!),
fontWeight: 600,
overflow:
TextOverflow.ellipsis,
),
),
],
],
)
: const SizedBox.shrink(),
MySpacing.height(12),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
AttendanceActionButton(
employee: employee,
attendanceController:
attendanceController,
),
if (employee.checkIn != null) ...[
MySpacing.width(8),
AttendanceLogViewButton(
employee: employee,
attendanceController:
attendanceController,
),
],
],
),
],
),
),
],
),
),
),
if (index != employees.length - 1)
Divider(
color: Colors.grey.withOpacity(0.3),
thickness: 1,
height: 1,
),
],
);
}),
),
),
],
);
});
}
Widget employeeLog() {
return Obx(() {
final logs = List.of(attendanceController.attendanceLogs);
logs.sort((a, b) {
final aDate = a.checkIn ?? DateTime(0);
final bDate = b.checkIn ?? DateTime(0);
return bDate.compareTo(aDate);
});
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: MyText.titleMedium(
"Attendance Logs",
fontWeight: 600,
),
),
Obx(() {
if (attendanceController.isLoading.value) {
return const SizedBox(
height: 20,
width: 20,
child: LinearProgressIndicator(),
);
}
final dateFormat = DateFormat('dd MMM yyyy');
final dateRangeText = attendanceController
.startDateAttendance !=
null &&
attendanceController.endDateAttendance != null
? '${dateFormat.format(attendanceController.endDateAttendance!)} - ${dateFormat.format(attendanceController.startDateAttendance!)}'
: 'Select date range';
return MyText.bodySmall(
dateRangeText,
fontWeight: 600,
color: Colors.grey[700],
overflow: TextOverflow.ellipsis,
);
}),
],
),
),
if (attendanceController.isLoadingAttendanceLogs.value)
const SizedBox(
height: 120,
child: Center(child: CircularProgressIndicator()),
)
else if (logs.isEmpty)
SizedBox(
height: 120,
child: Center(
child: MyText.bodySmall(
"No Attendance Logs Found for this Project",
fontWeight: 600,
),
),
)
else
MyCard.bordered(
borderRadiusAll: 4,
border: Border.all(color: Colors.grey.withOpacity(0.2)),
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
paddingAll: 8,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: List.generate(logs.length, (index) {
final employee = logs[index];
final currentDate = employee.checkIn != null
? DateFormat('dd MMM yyyy').format(employee.checkIn!)
: '';
final previousDate =
index > 0 && logs[index - 1].checkIn != null
? DateFormat('dd MMM yyyy')
.format(logs[index - 1].checkIn!)
: '';
final showDateHeader =
index == 0 || currentDate != previousDate;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (showDateHeader)
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: MyText.bodyMedium(
currentDate,
fontWeight: 700,
),
),
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: MyContainer(
paddingAll: 8,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Avatar(
firstName: employee.firstName,
lastName: employee.lastName,
size: 31,
),
MySpacing.width(16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Flexible(
child: MyText.bodyMedium(
employee.name,
fontWeight: 600,
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
MySpacing.width(6),
Flexible(
child: MyText.bodySmall(
'(${employee.designation})',
fontWeight: 600,
overflow: TextOverflow.ellipsis,
maxLines: 1,
color: Colors.grey[700],
),
),
],
),
MySpacing.height(8),
(employee.checkIn != null ||
employee.checkOut != null)
? Row(
children: [
if (employee.checkIn != null) ...[
const Icon(
Icons.arrow_circle_right,
size: 16,
color: Colors.green),
MySpacing.width(4),
Expanded(
child: MyText.bodySmall(
DateFormat('hh:mm a')
.format(
employee.checkIn!),
fontWeight: 600,
overflow:
TextOverflow.ellipsis,
),
),
MySpacing.width(16),
],
if (employee.checkOut !=
null) ...[
const Icon(
Icons.arrow_circle_left,
size: 16,
color: Colors.red),
MySpacing.width(4),
Expanded(
child: MyText.bodySmall(
DateFormat('hh:mm a')
.format(
employee.checkOut!),
fontWeight: 600,
overflow:
TextOverflow.ellipsis,
),
),
],
],
)
: const SizedBox.shrink(),
MySpacing.height(12),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Flexible(
child: AttendanceActionButton(
employee: employee,
attendanceController:
attendanceController,
),
),
MySpacing.width(8),
Flexible(
child: AttendanceLogViewButton(
employee: employee,
attendanceController:
attendanceController,
),
),
],
),
],
),
),
],
),
),
),
if (index != logs.length - 1)
Divider(
color: Colors.grey.withOpacity(0.3),
thickness: 1,
height: 1,
),
],
);
}),
),
),
],
);
});
}
Widget regularizationScreen() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 4.0),
child: MyText.titleMedium(
"Regularization Requests",
fontWeight: 600,
),
),
Obx(() {
final employees = attendanceController.regularizationLogs;
if (attendanceController.isLoadingRegularizationLogs.value) {
return SizedBox(
height: 120,
child: const Center(child: CircularProgressIndicator()),
);
}
if (employees.isEmpty) {
return SizedBox(
height: 120,
child: Center(
child: MyText.bodySmall(
"No Regularization Requests Found for this Project",
fontWeight: 600,
),
),
);
}
return MyCard.bordered(
borderRadiusAll: 4,
border: Border.all(color: Colors.grey.withOpacity(0.2)),
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
paddingAll: 8,
child: Column(
children: List.generate(employees.length, (index) {
final employee = employees[index];
return Column(
children: [
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: MyContainer(
paddingAll: 8,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Avatar(
firstName: employee.firstName,
lastName: employee.lastName,
size: 31,
),
MySpacing.width(16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Flexible(
child: MyText.bodyMedium(
employee.name,
fontWeight: 600,
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
MySpacing.width(6),
Flexible(
child: MyText.bodySmall(
'(${employee.role})',
fontWeight: 600,
overflow: TextOverflow.ellipsis,
maxLines: 1,
color: Colors.grey[700],
),
),
],
),
MySpacing.height(8),
Row(
children: [
if (employee.checkIn != null) ...[
const Icon(Icons.arrow_circle_right,
size: 16, color: Colors.green),
MySpacing.width(4),
Expanded(
child: MyText.bodySmall(
DateFormat('hh:mm a')
.format(employee.checkIn!),
fontWeight: 600,
overflow: TextOverflow.ellipsis,
),
),
MySpacing.width(16),
],
if (employee.checkOut != null) ...[
const Icon(Icons.arrow_circle_left,
size: 16, color: Colors.red),
MySpacing.width(4),
Expanded(
child: MyText.bodySmall(
DateFormat('hh:mm a')
.format(employee.checkOut!),
fontWeight: 600,
overflow: TextOverflow.ellipsis,
),
),
],
],
),
MySpacing.height(12),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
RegularizeActionButton(
attendanceController:
attendanceController,
log: employee,
uniqueLogKey: employee.employeeId,
action: ButtonActions.approve,
),
const SizedBox(width: 8),
RegularizeActionButton(
attendanceController:
attendanceController,
log: employee,
uniqueLogKey: employee.employeeId,
action: ButtonActions.reject,
),
const SizedBox(width: 8),
if (employee.checkIn != null) ...[
AttendanceLogViewButton(
employee: employee,
attendanceController:
attendanceController,
),
],
],
),
],
),
),
],
),
),
),
if (index != employees.length - 1)
Divider(
color: Colors.grey.withOpacity(0.3),
thickness: 1,
height: 1,
),
],
);
}),
),
);
}),
],
);
}
}