Vaibhav_Feature-#768 #59

Closed
vaibhav.surve wants to merge 74 commits from Vaibhav_Feature-#768 into Feature_Expense
6 changed files with 854 additions and 945 deletions
Showing only changes of commit fe66f35be7 - Show all commits

View File

@ -1,22 +1,25 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:image_picker/image_picker.dart';
import 'package:geolocator/geolocator.dart';
import 'package:intl/intl.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/helpers/widgets/my_image_compressor.dart';
import 'package:marco/model/attendance_model.dart';
import 'package:marco/model/project_model.dart';
import 'package:marco/model/employee_model.dart';
import 'package:marco/model/attendance_log_model.dart';
import 'package:marco/model/regularization_log_model.dart';
import 'package:marco/model/attendance_log_view_model.dart';
import 'package:marco/controller/project_controller.dart';
class AttendanceController extends GetxController {
// Data models
List<AttendanceModel> attendances = [];
List<ProjectModel> projects = [];
List<EmployeeModel> employees = [];
@ -24,19 +27,18 @@ class AttendanceController extends GetxController {
List<RegularizationLogModel> regularizationLogs = [];
List<AttendanceLogViewModel> attendenceLogsView = [];
// States
String selectedTab = 'Employee List';
DateTime? startDateAttendance;
DateTime? endDateAttendance;
RxBool isLoading = true.obs;
RxBool isLoadingProjects = true.obs;
RxBool isLoadingEmployees = true.obs;
RxBool isLoadingAttendanceLogs = true.obs;
RxBool isLoadingRegularizationLogs = true.obs;
RxBool isLoadingLogView = true.obs;
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
final isLoading = true.obs;
final isLoadingProjects = true.obs;
final isLoadingEmployees = true.obs;
final isLoadingAttendanceLogs = true.obs;
final isLoadingRegularizationLogs = true.obs;
final isLoadingLogView = true.obs;
final uploadingStates = <String, RxBool>{}.obs;
@override
void onInit() {
@ -56,76 +58,46 @@ class AttendanceController extends GetxController {
logSafe("Default date range set: $startDateAttendance to $endDateAttendance");
}
Future<bool> _handleLocationPermission() async {
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
logSafe('Location permissions are denied', level: LogLevel.warning);
return false;
}
}
if (permission == LocationPermission.deniedForever) {
logSafe('Location permissions are permanently denied', level: LogLevel.error);
return false;
}
return true;
}
// ------------------ Project & Employee ------------------
Future<void> fetchProjects() async {
isLoadingProjects.value = true;
isLoading.value = true;
final response = await ApiService.getProjects();
if (response != null && response.isNotEmpty) {
projects = response.map((json) => ProjectModel.fromJson(json)).toList();
projects = response.map((e) => ProjectModel.fromJson(e)).toList();
logSafe("Projects fetched: ${projects.length}");
} else {
logSafe("Failed to fetch projects or no projects available.", level: LogLevel.error);
projects = [];
logSafe("Failed to fetch projects or no projects available.", level: LogLevel.error);
}
isLoadingProjects.value = false;
isLoading.value = false;
update(['attendance_dashboard_controller']);
}
Future<void> loadAttendanceData(String projectId) async {
await fetchEmployeesByProject(projectId);
await fetchAttendanceLogs(projectId);
await fetchRegularizationLogs(projectId);
await fetchProjectData(projectId);
}
Future<void> fetchProjectData(String? projectId) async {
if (projectId == null) return;
isLoading.value = true;
await Future.wait([
fetchEmployeesByProject(projectId),
fetchAttendanceLogs(projectId, dateFrom: startDateAttendance, dateTo: endDateAttendance),
fetchRegularizationLogs(projectId),
]);
isLoading.value = false;
logSafe("Project data fetched for project ID: $projectId");
}
Future<void> fetchEmployeesByProject(String? projectId) async {
if (projectId == null) return;
isLoadingEmployees.value = true;
final response = await ApiService.getEmployeesByProject(projectId);
if (response != null) {
employees = response.map((json) => EmployeeModel.fromJson(json)).toList();
employees = response.map((e) => EmployeeModel.fromJson(e)).toList();
for (var emp in employees) {
uploadingStates[emp.id] = false.obs;
}
logSafe("Employees fetched: ${employees.length} for project $projectId");
update();
} else {
logSafe("Failed to fetch employees for project $projectId", level: LogLevel.error);
}
isLoadingEmployees.value = false;
update();
}
// ------------------ Attendance Capture ------------------
Future<bool> captureAndUploadAttendance(
String id,
String employeeId,
@ -137,6 +109,7 @@ class AttendanceController extends GetxController {
}) async {
try {
uploadingStates[employeeId]?.value = true;
XFile? image;
if (imageCapture) {
image = await ImagePicker().pickImage(source: ImageSource.camera, imageQuality: 80);
@ -144,24 +117,39 @@ class AttendanceController extends GetxController {
logSafe("Image capture cancelled.", level: LogLevel.warning);
return false;
}
final compressedBytes = await compressImageToUnder100KB(File(image.path));
if (compressedBytes == null) {
logSafe("Image compression failed.", level: LogLevel.error);
return false;
}
final compressedFile = await saveCompressedImageToFile(compressedBytes);
image = XFile(compressedFile.path);
}
final hasLocationPermission = await _handleLocationPermission();
if (!hasLocationPermission) return false;
final position = await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high);
final imageName = imageCapture ? ApiService.generateImageName(employeeId, employees.length + 1) : "";
if (!await _handleLocationPermission()) return false;
final position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high);
final imageName = imageCapture
? ApiService.generateImageName(employeeId, employees.length + 1)
: "";
final result = await ApiService.uploadAttendanceImage(
id, employeeId, image, position.latitude, position.longitude,
imageName: imageName, projectId: projectId, comment: comment,
action: action, imageCapture: imageCapture, markTime: markTime,
id,
employeeId,
image,
position.latitude,
position.longitude,
imageName: imageName,
projectId: projectId,
comment: comment,
action: action,
imageCapture: imageCapture,
markTime: markTime,
);
logSafe("Attendance uploaded for $employeeId, action: $action");
return result;
} catch (e, stacktrace) {
@ -172,8 +160,133 @@ class AttendanceController extends GetxController {
}
}
Future<void> selectDateRangeForAttendance(BuildContext context, AttendanceController controller) async {
Future<bool> _handleLocationPermission() async {
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
logSafe('Location permissions are denied', level: LogLevel.warning);
return false;
}
}
if (permission == LocationPermission.deniedForever) {
logSafe('Location permissions are permanently denied', level: LogLevel.error);
return false;
}
return true;
}
// ------------------ Attendance Logs ------------------
Future<void> fetchAttendanceLogs(String? projectId, {DateTime? dateFrom, DateTime? dateTo}) async {
if (projectId == null) return;
isLoadingAttendanceLogs.value = true;
final response = await ApiService.getAttendanceLogs(projectId, dateFrom: dateFrom, dateTo: dateTo);
if (response != null) {
attendanceLogs = response.map((e) => AttendanceLogModel.fromJson(e)).toList();
logSafe("Attendance logs fetched: ${attendanceLogs.length}");
} else {
logSafe("Failed to fetch attendance logs for project $projectId", level: LogLevel.error);
}
isLoadingAttendanceLogs.value = false;
update();
}
Map<String, List<AttendanceLogModel>> groupLogsByCheckInDate() {
final groupedLogs = <String, List<AttendanceLogModel>>{};
for (var logItem in attendanceLogs) {
final checkInDate = logItem.checkIn != null
? DateFormat('dd MMM yyyy').format(logItem.checkIn!)
: 'Unknown';
groupedLogs.putIfAbsent(checkInDate, () => []).add(logItem);
}
final sortedEntries = groupedLogs.entries.toList()
..sort((a, b) {
if (a.key == 'Unknown') return 1;
if (b.key == 'Unknown') return -1;
final dateA = DateFormat('dd MMM yyyy').parse(a.key);
final dateB = DateFormat('dd MMM yyyy').parse(b.key);
return dateB.compareTo(dateA);
});
return Map<String, List<AttendanceLogModel>>.fromEntries(sortedEntries);
}
// ------------------ Regularization Logs ------------------
Future<void> fetchRegularizationLogs(String? projectId) async {
if (projectId == null) return;
isLoadingRegularizationLogs.value = true;
final response = await ApiService.getRegularizationLogs(projectId);
if (response != null) {
regularizationLogs = response.map((e) => RegularizationLogModel.fromJson(e)).toList();
logSafe("Regularization logs fetched: ${regularizationLogs.length}");
} else {
logSafe("Failed to fetch regularization logs for project $projectId", level: LogLevel.error);
}
isLoadingRegularizationLogs.value = false;
update();
}
// ------------------ Attendance Log View ------------------
Future<void> fetchLogsView(String? id) async {
if (id == null) return;
isLoadingLogView.value = true;
final response = await ApiService.getAttendanceLogView(id);
if (response != null) {
attendenceLogsView = response.map((e) => AttendanceLogViewModel.fromJson(e)).toList();
attendenceLogsView.sort((a, b) =>
(b.activityTime ?? DateTime(2000)).compareTo(a.activityTime ?? DateTime(2000)));
logSafe("Attendance log view fetched for ID: $id");
} else {
logSafe("Failed to fetch attendance log view for ID $id", level: LogLevel.error);
}
isLoadingLogView.value = false;
update();
}
// ------------------ Combined Load ------------------
Future<void> loadAttendanceData(String projectId) async {
isLoading.value = true;
await fetchProjectData(projectId);
isLoading.value = false;
}
Future<void> fetchProjectData(String? projectId) async {
if (projectId == null) return;
await Future.wait([
fetchEmployeesByProject(projectId),
fetchAttendanceLogs(projectId,
dateFrom: startDateAttendance, dateTo: endDateAttendance),
fetchRegularizationLogs(projectId),
]);
logSafe("Project data fetched for project ID: $projectId");
}
// ------------------ UI Interaction ------------------
Future<void> selectDateRangeForAttendance(
BuildContext context, AttendanceController controller) async {
final today = DateTime.now();
final picked = await showDateRangePicker(
context: context,
firstDate: DateTime(2022),
@ -190,14 +303,13 @@ class AttendanceController extends GetxController {
child: Theme(
data: Theme.of(context).copyWith(
colorScheme: ColorScheme.light(
primary: const Color.fromARGB(255, 95, 132, 255),
primary: const Color(0xFF5F84FF),
onPrimary: Colors.white,
onSurface: Colors.teal.shade800,
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(foregroundColor: Colors.teal),
),
dialogTheme: DialogThemeData(backgroundColor: Colors.white),
),
child: child!,
),
@ -210,6 +322,7 @@ class AttendanceController extends GetxController {
startDateAttendance = picked.start;
endDateAttendance = picked.end;
logSafe("Date range selected: $startDateAttendance to $endDateAttendance");
await controller.fetchAttendanceLogs(
Get.find<ProjectController>().selectedProject?.id,
dateFrom: picked.start,
@ -217,78 +330,4 @@ class AttendanceController extends GetxController {
);
}
}
Future<void> fetchAttendanceLogs(String? projectId, {DateTime? dateFrom, DateTime? dateTo}) async {
if (projectId == null) return;
isLoadingAttendanceLogs.value = true;
isLoading.value = true;
final response = await ApiService.getAttendanceLogs(projectId, dateFrom: dateFrom, dateTo: dateTo);
if (response != null) {
attendanceLogs = response.map((json) => AttendanceLogModel.fromJson(json)).toList();
logSafe("Attendance logs fetched: ${attendanceLogs.length}");
update();
} else {
logSafe("Failed to fetch attendance logs for project $projectId", level: LogLevel.error);
}
isLoadingAttendanceLogs.value = false;
isLoading.value = false;
}
Map<String, List<AttendanceLogModel>> groupLogsByCheckInDate() {
final groupedLogs = <String, List<AttendanceLogModel>>{};
for (var logItem in attendanceLogs) {
final checkInDate = logItem.checkIn != null
? DateFormat('dd MMM yyyy').format(logItem.checkIn!)
: 'Unknown';
groupedLogs.putIfAbsent(checkInDate, () => []);
groupedLogs[checkInDate]!.add(logItem);
}
final sortedEntries = groupedLogs.entries.toList()
..sort((a, b) {
if (a.key == 'Unknown') return 1;
if (b.key == 'Unknown') return -1;
final dateA = DateFormat('dd MMM yyyy').parse(a.key);
final dateB = DateFormat('dd MMM yyyy').parse(b.key);
return dateB.compareTo(dateA);
});
final sortedMap = Map<String, List<AttendanceLogModel>>.fromEntries(sortedEntries);
logSafe("Logs grouped and sorted by check-in date.");
return sortedMap;
}
Future<void> fetchRegularizationLogs(String? projectId, {DateTime? dateFrom, DateTime? dateTo}) async {
if (projectId == null) return;
isLoadingRegularizationLogs.value = true;
isLoading.value = true;
final response = await ApiService.getRegularizationLogs(projectId);
if (response != null) {
regularizationLogs = response.map((json) => RegularizationLogModel.fromJson(json)).toList();
logSafe("Regularization logs fetched: ${regularizationLogs.length}");
update();
} else {
logSafe("Failed to fetch regularization logs for project $projectId", level: LogLevel.error);
}
isLoadingRegularizationLogs.value = false;
isLoading.value = false;
}
Future<void> fetchLogsView(String? id) async {
if (id == null) return;
isLoadingLogView.value = true;
isLoading.value = true;
final response = await ApiService.getAttendanceLogView(id);
if (response != null) {
attendenceLogsView = response.map((json) => AttendanceLogViewModel.fromJson(json)).toList();
attendenceLogsView.sort((a, b) {
if (a.activityTime == null || b.activityTime == null) return 0;
return b.activityTime!.compareTo(a.activityTime!);
});
logSafe("Attendance log view fetched for ID: $id");
update();
} else {
logSafe("Failed to fetch attendance log view for ID $id", level: LogLevel.error);
}
isLoadingLogView.value = false;
isLoading.value = false;
}
}

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:marco/controller/permission_controller.dart';
import 'package:marco/controller/dashboard/attendance_screen_controller.dart';
import 'package:intl/intl.dart';
import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/helpers/utils/permission_constants.dart';
import 'package:marco/helpers/utils/base_bottom_sheet.dart';
@ -19,7 +19,7 @@ class AttendanceFilterBottomSheet extends StatefulWidget {
});
@override
_AttendanceFilterBottomSheetState createState() =>
State<AttendanceFilterBottomSheet> createState() =>
_AttendanceFilterBottomSheetState();
}
@ -54,22 +54,20 @@ class _AttendanceFilterBottomSheetState
{'label': 'Regularization Requests', 'value': 'regularizationRequests'},
];
final filteredViewOptions = viewOptions.where((item) {
if (item['value'] == 'regularizationRequests') {
return hasRegularizationPermission;
}
return true;
final filteredOptions = viewOptions.where((item) {
return item['value'] != 'regularizationRequests' ||
hasRegularizationPermission;
}).toList();
List<Widget> widgets = [
final List<Widget> widgets = [
Padding(
padding: const EdgeInsets.only(bottom: 4),
padding: EdgeInsets.only(bottom: 4),
child: Align(
alignment: Alignment.centerLeft,
child: MyText.titleSmall("View", fontWeight: 600),
),
),
...filteredViewOptions.map((item) {
...filteredOptions.map((item) {
return RadioListTile<String>(
dense: true,
contentPadding: EdgeInsets.zero,
@ -81,14 +79,14 @@ class _AttendanceFilterBottomSheetState
groupValue: tempSelectedTab,
onChanged: (value) => setState(() => tempSelectedTab = value!),
);
}).toList(),
}),
];
if (tempSelectedTab == 'attendanceLogs') {
widgets.addAll([
const Divider(),
Padding(
padding: const EdgeInsets.only(top: 12, bottom: 4),
padding: EdgeInsets.only(top: 12, bottom: 4),
child: Align(
alignment: Alignment.centerLeft,
child: MyText.titleSmall("Date Range", fontWeight: 600),

View File

@ -0,0 +1,189 @@
// lib/view/attendance/tabs/attendance_logs_tab.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:marco/controller/dashboard/attendance_screen_controller.dart';
import 'package:marco/helpers/widgets/avatar.dart';
import 'package:marco/helpers/widgets/my_card.dart';
import 'package:marco/helpers/widgets/my_container.dart';
import 'package:marco/helpers/widgets/my_spacing.dart';
import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/helpers/widgets/my_custom_skeleton.dart';
import 'package:marco/model/attendance/log_details_view.dart';
import 'package:marco/model/attendance/attendence_action_button.dart';
class AttendanceLogsTab extends StatelessWidget {
final AttendanceController controller;
const AttendanceLogsTab({super.key, required this.controller});
@override
Widget build(BuildContext context) {
return Obx(() {
final logs = List.of(controller.attendanceLogs);
logs.sort((a, b) {
final aDate = a.checkIn ?? DateTime(0);
final bDate = b.checkIn ?? DateTime(0);
return bDate.compareTo(aDate);
});
final dateFormat = DateFormat('dd MMM yyyy');
final dateRangeText = controller.startDateAttendance != null &&
controller.endDateAttendance != null
? '${dateFormat.format(controller.endDateAttendance!)} - ${dateFormat.format(controller.startDateAttendance!)}'
: 'Select date range';
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
MyText.titleMedium("Attendance Logs", fontWeight: 600),
controller.isLoading.value
? const SizedBox(
height: 20, width: 20, child: LinearProgressIndicator())
: MyText.bodySmall(
dateRangeText,
fontWeight: 600,
color: Colors.grey[700],
overflow: TextOverflow.ellipsis,
),
],
),
),
if (controller.isLoadingAttendanceLogs.value)
SkeletonLoaders.employeeListSkeletonLoader()
else if (logs.isEmpty)
const SizedBox(
height: 120,
child: Center(
child: Text("No Attendance Logs Found for this Project"),
),
)
else
MyCard.bordered(
paddingAll: 8,
child: 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,
),
),
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,
),
),
MySpacing.width(6),
Flexible(
child: MyText.bodySmall(
'(${employee.designation})',
fontWeight: 600,
color: Colors.grey[700],
overflow: TextOverflow.ellipsis,
),
),
],
),
MySpacing.height(8),
if (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),
MyText.bodySmall(
DateFormat('hh:mm a')
.format(employee.checkIn!),
fontWeight: 600,
),
MySpacing.width(16),
],
if (employee.checkOut != null) ...[
const Icon(Icons.arrow_circle_left,
size: 16, color: Colors.red),
MySpacing.width(4),
MyText.bodySmall(
DateFormat('hh:mm a')
.format(employee.checkOut!),
fontWeight: 600,
),
],
],
),
MySpacing.height(12),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
AttendanceActionButton(
employee: employee,
attendanceController: controller,
),
MySpacing.width(8),
AttendanceLogViewButton(
employee: employee,
attendanceController: controller,
),
],
),
],
),
),
],
),
),
if (index != logs.length - 1)
Divider(color: Colors.grey.withOpacity(0.3)),
],
);
}),
),
),
],
);
});
}
}

View File

@ -2,77 +2,64 @@ 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';
import 'package:marco/helpers/widgets/my_custom_skeleton.dart';
import 'package:marco/view/dashboard/Attendence/regularization_requests_tab.dart';
import 'package:marco/view/dashboard/Attendence/attendance_logs_tab.dart';
import 'package:marco/view/dashboard/Attendence/todays_attendance_tab.dart';
class AttendanceScreen extends StatefulWidget {
AttendanceScreen({super.key});
const 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());
final attendanceController = Get.put(AttendanceController());
final permissionController = Get.put(PermissionController());
final projectController = Get.find<ProjectController>();
String selectedTab = 'todaysAttendance';
@override
void initState() {
super.initState();
final projectController = Get.find<ProjectController>();
final attendanceController = Get.find<AttendanceController>();
WidgetsBinding.instance.addPostFrameCallback((_) {
// Listen for future project selection changes
ever<String>(projectController.selectedProjectId, (projectId) async {
if (projectId.isNotEmpty) await _loadData(projectId);
});
WidgetsBinding.instance.addPostFrameCallback((_) async {
// Listen for future changes in selected project
ever<String?>(projectController.selectedProjectId!, (projectId) async {
if (projectId != null && projectId.isNotEmpty) {
// Load initial data
final projectId = projectController.selectedProjectId.value;
if (projectId.isNotEmpty) _loadData(projectId);
});
}
Future<void> _loadData(String projectId) async {
try {
await attendanceController.loadAttendanceData(projectId);
attendanceController.update(['attendance_dashboard_controller']);
} catch (e) {
debugPrint("Error updating data on project change: $e");
debugPrint("Error loading data: $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(72),
child: AppBar(
Future<void> _refreshData() async {
final projectId = projectController.selectedProjectId.value;
if (projectId.isNotEmpty) await _loadData(projectId);
}
Widget _buildAppBar() {
return AppBar(
backgroundColor: const Color(0xFFF5F5F5),
elevation: 0.5,
automaticallyImplyLeading: false,
@ -83,31 +70,22 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
crossAxisAlignment: CrossAxisAlignment.center,
children: [
IconButton(
icon: const Icon(Icons.arrow_back_ios_new,
color: Colors.black, size: 20),
icon: const Icon(Icons.arrow_back_ios_new, color: Colors.black, size: 20),
onPressed: () => Get.offNamed('/dashboard'),
),
MySpacing.width(8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
MyText.titleLarge(
'Attendance',
fontWeight: 700,
color: Colors.black,
),
MyText.titleLarge('Attendance', fontWeight: 700, color: Colors.black),
MySpacing.height(2),
GetBuilder<ProjectController>(
builder: (projectController) {
final projectName =
projectController.selectedProject?.name ??
'Select Project';
final projectName = projectController.selectedProject?.name ?? 'Select Project';
return Row(
children: [
const Icon(Icons.work_outline,
size: 14, color: Colors.grey),
const Icon(Icons.work_outline, size: 14, color: Colors.grey),
MySpacing.width(4),
Expanded(
child: MyText.bodySmall(
@ -127,24 +105,11 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
],
),
),
),
),
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(
);
}
Widget _buildFilterAndRefreshRow() {
return Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
MyText.bodyMedium("Filter", fontWeight: 600),
@ -153,15 +118,12 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
child: InkWell(
borderRadius: BorderRadius.circular(24),
onTap: () async {
final result = await showModalBottomSheet<
Map<String, dynamic>>(
final result = await showModalBottomSheet<Map<String, dynamic>>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12)),
borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
),
builder: (context) => AttendanceFilterBottomSheet(
controller: attendanceController,
@ -171,49 +133,28 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
);
if (result != null) {
final selectedProjectId =
Get.find<ProjectController>()
.selectedProjectId
.value;
final selectedProjectId = projectController.selectedProjectId.value;
final selectedView = result['selectedTab'] as String?;
final selectedView =
result['selectedTab'] as String?;
if (selectedProjectId != null) {
if (selectedProjectId.isNotEmpty) {
try {
await attendanceController
.fetchEmployeesByProject(
selectedProjectId);
await attendanceController
.fetchAttendanceLogs(selectedProjectId);
await attendanceController
.fetchRegularizationLogs(
selectedProjectId);
await attendanceController
.fetchProjectData(selectedProjectId);
await attendanceController.fetchEmployeesByProject(selectedProjectId);
await attendanceController.fetchAttendanceLogs(selectedProjectId);
await attendanceController.fetchRegularizationLogs(selectedProjectId);
await attendanceController.fetchProjectData(selectedProjectId);
} catch (_) {}
attendanceController.update(
['attendance_dashboard_controller']);
attendanceController.update(['attendance_dashboard_controller']);
}
if (selectedView != null &&
selectedView != selectedTab) {
setState(() {
selectedTab = selectedView;
});
if (selectedView != null && selectedView != selectedTab) {
setState(() => selectedTab = selectedView);
}
}
},
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(
Icons.tune,
color: Colors.blueAccent,
size: 20,
),
),
child: Icon(Icons.tune, color: Colors.blueAccent, size: 20),
),
),
),
@ -223,42 +164,19 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
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,
onTap: _refreshData,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(
Icons.refresh,
color: Colors.green,
size: 22,
),
),
child: Icon(Icons.refresh, color: Colors.green, size: 22),
),
),
),
],
),
MySpacing.height(flexSpacing),
MyFlex(children: [
MyFlexItem(
sizes: 'lg-12 md-12 sm-12',
child: noProjectSelected
? Center(
);
}
Widget _buildNoProjectWidget() {
return Center(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: MyText.titleMedium(
@ -267,580 +185,57 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
color: Colors.grey[600],
),
),
)
: selectedTab == 'todaysAttendance'
? employeeListTab()
: selectedTab == 'attendanceLogs'
? employeeLog()
: regularizationScreen(),
);
}
Widget _buildSelectedTabContent() {
switch (selectedTab) {
case 'attendanceLogs':
return AttendanceLogsTab(controller: attendanceController);
case 'regularizationRequests':
return RegularizationRequestsTab(controller: attendanceController);
case 'todaysAttendance':
default:
return TodaysAttendanceTab(controller: attendanceController);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: PreferredSize(preferredSize: const Size.fromHeight(72), child: _buildAppBar()),
body: SafeArea(
child: GetBuilder<AttendanceController>(
init: attendanceController,
tag: 'attendance_dashboard_controller',
builder: (controller) {
final selectedProjectId = projectController.selectedProjectId.value;
final noProjectSelected = selectedProjectId.isEmpty;
return SingleChildScrollView(
padding: MySpacing.zero,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MySpacing.height(flexSpacing),
_buildFilterAndRefreshRow(),
MySpacing.height(flexSpacing),
MyFlex(
children: [
MyFlexItem(
sizes: 'lg-12 md-12 sm-12',
child: noProjectSelected
? _buildNoProjectWidget()
: _buildSelectedTabContent(),
),
]),
],
),
],
),
);
},
),
),
),
);
}
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)
SkeletonLoaders.employeeListSkeletonLoader()
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)
SkeletonLoaders.employeeListSkeletonLoader()
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 SkeletonLoaders.employeeListSkeletonLoader();
}
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,
),
],
);
}),
),
);
}),
],
);
}
}

View File

@ -0,0 +1,157 @@
// lib/view/attendance/tabs/regularization_requests_tab.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:marco/controller/dashboard/attendance_screen_controller.dart';
import 'package:marco/helpers/widgets/avatar.dart';
import 'package:marco/helpers/widgets/my_card.dart';
import 'package:marco/helpers/widgets/my_container.dart';
import 'package:marco/helpers/widgets/my_spacing.dart';
import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/helpers/widgets/my_custom_skeleton.dart';
import 'package:marco/model/attendance/log_details_view.dart';
import 'package:marco/model/attendance/regualrize_action_button.dart';
class RegularizationRequestsTab extends StatelessWidget {
final AttendanceController controller;
const RegularizationRequestsTab({super.key, required this.controller});
@override
Widget build(BuildContext context) {
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 = controller.regularizationLogs;
if (controller.isLoadingRegularizationLogs.value) {
return SkeletonLoaders.employeeListSkeletonLoader();
}
if (employees.isEmpty) {
return const SizedBox(
height: 120,
child: Center(
child: Text("No Regularization Requests Found for this Project"),
),
);
}
return MyCard.bordered(
paddingAll: 8,
child: Column(
children: List.generate(employees.length, (index) {
final employee = employees[index];
return Column(
children: [
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,
),
),
MySpacing.width(6),
Flexible(
child: MyText.bodySmall(
'(${employee.role})',
fontWeight: 600,
overflow: TextOverflow.ellipsis,
color: Colors.grey[700],
),
),
],
),
MySpacing.height(8),
if (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),
MyText.bodySmall(
DateFormat('hh:mm a')
.format(employee.checkIn!),
fontWeight: 600,
),
MySpacing.width(16),
],
if (employee.checkOut != null) ...[
const Icon(Icons.arrow_circle_left,
size: 16, color: Colors.red),
MySpacing.width(4),
MyText.bodySmall(
DateFormat('hh:mm a')
.format(employee.checkOut!),
fontWeight: 600,
),
],
],
),
MySpacing.height(12),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
RegularizeActionButton(
attendanceController: controller,
log: employee,
uniqueLogKey: employee.employeeId,
action: ButtonActions.approve,
),
const SizedBox(width: 8),
RegularizeActionButton(
attendanceController: controller,
log: employee,
uniqueLogKey: employee.employeeId,
action: ButtonActions.reject,
),
const SizedBox(width: 8),
if (employee.checkIn != null)
AttendanceLogViewButton(
employee: employee,
attendanceController: controller,
),
],
),
],
),
),
],
),
),
if (index != employees.length - 1)
Divider(color: Colors.grey.withOpacity(0.3)),
],
);
}),
),
);
}),
],
);
}
}

View File

@ -0,0 +1,131 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:marco/controller/dashboard/attendance_screen_controller.dart';
import 'package:marco/helpers/widgets/avatar.dart';
import 'package:marco/helpers/widgets/my_card.dart';
import 'package:marco/helpers/widgets/my_container.dart';
import 'package:marco/helpers/widgets/my_spacing.dart';
import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/helpers/widgets/my_custom_skeleton.dart';
import 'package:marco/model/attendance/log_details_view.dart';
import 'package:marco/model/attendance/attendence_action_button.dart';
class TodaysAttendanceTab extends StatelessWidget {
final AttendanceController controller;
const TodaysAttendanceTab({super.key, required this.controller});
String _formatDate(DateTime date) {
return "${date.day}/${date.month}/${date.year}";
}
@override
Widget build(BuildContext context) {
return Obx(() {
final isLoading = controller.isLoadingEmployees.value;
final employees = controller.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],
),
],
),
),
if (isLoading)
SkeletonLoaders.employeeListSkeletonLoader()
else if (employees.isEmpty)
const SizedBox(height: 120, child: Center(child: Text("No Employees Assigned")))
else
MyCard.bordered(
paddingAll: 8,
child: Column(
children: List.generate(employees.length, (index) {
final employee = employees[index];
return Column(
children: [
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(
spacing: 6,
children: [
MyText.bodyMedium(employee.name, fontWeight: 600),
MyText.bodySmall('(${employee.designation})', fontWeight: 600, color: Colors.grey[700]),
],
),
MySpacing.height(8),
if (employee.checkIn != null || employee.checkOut != null)
Row(
children: [
if (employee.checkIn != null)
Row(
children: [
const Icon(Icons.arrow_circle_right, size: 16, color: Colors.green),
MySpacing.width(4),
Text(DateFormat('hh:mm a').format(employee.checkIn!)),
],
),
if (employee.checkOut != null) ...[
MySpacing.width(16),
const Icon(Icons.arrow_circle_left, size: 16, color: Colors.red),
MySpacing.width(4),
Text(DateFormat('hh:mm a').format(employee.checkOut!)),
],
],
),
MySpacing.height(12),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
AttendanceActionButton(
employee: employee,
attendanceController: controller,
),
if (employee.checkIn != null) ...[
MySpacing.width(8),
AttendanceLogViewButton(
employee: employee,
attendanceController: controller,
),
],
],
),
],
),
),
],
),
),
if (index != employees.length - 1)
Divider(color: Colors.grey.withOpacity(0.3)),
],
);
}),
),
),
],
);
});
}
}