Vaibhav Surve 34100a4d9e -- Enhance layout with floating action button and navigation improvements
- Added a floating action button to the Layout widget for better accessibility.
- Updated the left bar navigation items for clarity and consistency.
- Introduced Daily Progress Report and Daily Task Planning screens with comprehensive UI.
- Implemented filtering and refreshing functionalities in task planning.
- Improved user experience with better spacing and layout adjustments.
- Updated pubspec.yaml to include new dependencies for image handling and path management.
2025-05-28 17:35:42 +05:30

772 lines
38 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_breadcrumb.dart';
import 'package:marco/helpers/widgets/my_breadcrumb_item.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/view/layouts/layout.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';
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
Widget build(BuildContext context) {
return Layout(
child: GetBuilder<AttendanceController>(
init: attendanceController,
tag: 'attendance_dashboard_controller',
builder: (controller) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: MySpacing.x(flexSpacing),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
MyText.titleMedium("Attendance",
fontSize: 18, fontWeight: 600),
MyBreadcrumb(
children: [
MyBreadcrumbItem(name: 'Dashboard'),
MyBreadcrumbItem(name: 'Attendance', active: true),
],
),
],
),
),
MySpacing.height(flexSpacing),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
MyText.bodyMedium(
"Filter",
fontWeight: 600,
),
// Wrap with Tooltip and InkWell for interactive feedback
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 =
result['projectId'] as String?;
final selectedView = result['selectedTab'] as String?;
if (selectedProjectId != null &&
selectedProjectId !=
attendanceController.selectedProjectId) {
attendanceController.selectedProjectId =
selectedProjectId;
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 =
attendanceController.selectedProjectId;
if (projectId != null && projectId.isNotEmpty) {
try {
await attendanceController
.fetchEmployeesByProject(projectId);
await attendanceController
.fetchAttendanceLogs(projectId);
await attendanceController
.fetchRegularizationLogs(projectId);
await attendanceController
.fetchProjectData(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,
),
),
),
),
),
],
),
Padding(
padding: MySpacing.x(flexSpacing / 2),
child: MyFlex(children: [
MyFlexItem(
sizes: 'lg-12 md-12 sm-12',
child: selectedTab == 'todaysAttendance'
? employeeListTab()
: selectedTab == 'attendanceLogs'
? employeeLog()
: regularizationScreen(),
),
]),
),
],
);
},
),
);
}
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,
),
),
],
),
),
MyCard.bordered(
borderRadiusAll: 4,
border: Border.all(color: Colors.grey.withOpacity(0.2)),
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
paddingAll: 8,
child: isLoading
? Center(child: CircularProgressIndicator())
: employees.isEmpty
? Center(
child: MyText.bodySmall(
"No Employees Assigned to This Project",
fontWeight: 600,
),
)
: Column(
children: List.generate(employees.length, (index) {
final employee = employees[index];
return Column(
children: [
Padding(
padding: 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: [
Row(
children: [
MyText.bodyMedium(
employee.name,
fontWeight: 600,
overflow:
TextOverflow.ellipsis,
maxLines: 1,
),
MySpacing.width(6),
MyText.bodySmall(
'(${employee.designation})',
fontWeight: 600,
overflow:
TextOverflow.ellipsis,
maxLines: 1,
color: Colors.grey[
700], // optional styling
),
],
),
MySpacing.height(8),
(employee.checkIn != null ||
employee.checkOut != null)
? Row(
children: [
if (employee.checkIn !=
null) ...[
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) ...[
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,
),
),
],
],
)
: 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,
);
}),
],
),
),
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: [
if (attendanceController.isLoadingAttendanceLogs.value)
const Padding(
padding: EdgeInsets.symmetric(vertical: 32),
child: Center(child: CircularProgressIndicator()),
)
else if (logs.isEmpty)
MyText.bodySmall(
"No Attendance Logs Found for this Project",
fontWeight: 600,
)
else
Column(
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: 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) ...[
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) ...[
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,
),
),
],
],
)
: 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;
return MyCard.bordered(
borderRadiusAll: 4,
border: Border.all(color: Colors.grey.withOpacity(0.2)),
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
paddingAll: 8,
child: attendanceController.isLoadingRegularizationLogs.value
? const Padding(
padding: EdgeInsets.symmetric(vertical: 32.0),
child: Center(child: CircularProgressIndicator()),
)
: employees.isEmpty
? MyText.bodySmall(
"No Regularization Requests Found for this Project",
fontWeight: 600,
)
: Column(
children: List.generate(employees.length, (index) {
final employee = employees[index];
return Column(
children: [
Padding(
padding: 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) ...[
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) ...[
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,
),
],
)
],
),
),
],
),
),
),
if (index != employees.length - 1)
Divider(
color: Colors.grey.withOpacity(0.3),
thickness: 1,
height: 1,
),
],
);
}),
),
);
}),
],
);
}
}