Refactor attendance management: Split attendance screen into tabs, add attendance logs and regularization requests tabs, and improve filter functionality. Update attendance filter sheet and enhance UI components for better user experience.
This commit is contained in:
parent
70443d8e24
commit
fe66f35be7
@ -1,22 +1,25 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:geolocator/geolocator.dart';
|
import 'package:geolocator/geolocator.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
import 'package:marco/helpers/services/api_service.dart';
|
import 'package:marco/helpers/services/api_service.dart';
|
||||||
import 'package:marco/helpers/widgets/my_image_compressor.dart';
|
import 'package:marco/helpers/widgets/my_image_compressor.dart';
|
||||||
|
|
||||||
import 'package:marco/model/attendance_model.dart';
|
import 'package:marco/model/attendance_model.dart';
|
||||||
import 'package:marco/model/project_model.dart';
|
import 'package:marco/model/project_model.dart';
|
||||||
import 'package:marco/model/employee_model.dart';
|
import 'package:marco/model/employee_model.dart';
|
||||||
import 'package:marco/model/attendance_log_model.dart';
|
import 'package:marco/model/attendance_log_model.dart';
|
||||||
import 'package:marco/model/regularization_log_model.dart';
|
import 'package:marco/model/regularization_log_model.dart';
|
||||||
import 'package:marco/model/attendance_log_view_model.dart';
|
import 'package:marco/model/attendance_log_view_model.dart';
|
||||||
|
|
||||||
import 'package:marco/controller/project_controller.dart';
|
import 'package:marco/controller/project_controller.dart';
|
||||||
|
|
||||||
class AttendanceController extends GetxController {
|
class AttendanceController extends GetxController {
|
||||||
|
// Data models
|
||||||
List<AttendanceModel> attendances = [];
|
List<AttendanceModel> attendances = [];
|
||||||
List<ProjectModel> projects = [];
|
List<ProjectModel> projects = [];
|
||||||
List<EmployeeModel> employees = [];
|
List<EmployeeModel> employees = [];
|
||||||
@ -24,19 +27,18 @@ class AttendanceController extends GetxController {
|
|||||||
List<RegularizationLogModel> regularizationLogs = [];
|
List<RegularizationLogModel> regularizationLogs = [];
|
||||||
List<AttendanceLogViewModel> attendenceLogsView = [];
|
List<AttendanceLogViewModel> attendenceLogsView = [];
|
||||||
|
|
||||||
|
// States
|
||||||
String selectedTab = 'Employee List';
|
String selectedTab = 'Employee List';
|
||||||
|
|
||||||
DateTime? startDateAttendance;
|
DateTime? startDateAttendance;
|
||||||
DateTime? endDateAttendance;
|
DateTime? endDateAttendance;
|
||||||
|
|
||||||
RxBool isLoading = true.obs;
|
final isLoading = true.obs;
|
||||||
RxBool isLoadingProjects = true.obs;
|
final isLoadingProjects = true.obs;
|
||||||
RxBool isLoadingEmployees = true.obs;
|
final isLoadingEmployees = true.obs;
|
||||||
RxBool isLoadingAttendanceLogs = true.obs;
|
final isLoadingAttendanceLogs = true.obs;
|
||||||
RxBool isLoadingRegularizationLogs = true.obs;
|
final isLoadingRegularizationLogs = true.obs;
|
||||||
RxBool isLoadingLogView = true.obs;
|
final isLoadingLogView = true.obs;
|
||||||
|
final uploadingStates = <String, RxBool>{}.obs;
|
||||||
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
@ -56,76 +58,46 @@ class AttendanceController extends GetxController {
|
|||||||
logSafe("Default date range set: $startDateAttendance to $endDateAttendance");
|
logSafe("Default date range set: $startDateAttendance to $endDateAttendance");
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> _handleLocationPermission() async {
|
// ------------------ Project & Employee ------------------
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> fetchProjects() async {
|
Future<void> fetchProjects() async {
|
||||||
isLoadingProjects.value = true;
|
isLoadingProjects.value = true;
|
||||||
isLoading.value = true;
|
|
||||||
|
|
||||||
final response = await ApiService.getProjects();
|
final response = await ApiService.getProjects();
|
||||||
if (response != null && response.isNotEmpty) {
|
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}");
|
logSafe("Projects fetched: ${projects.length}");
|
||||||
} else {
|
} else {
|
||||||
logSafe("Failed to fetch projects or no projects available.", level: LogLevel.error);
|
|
||||||
projects = [];
|
projects = [];
|
||||||
|
logSafe("Failed to fetch projects or no projects available.", level: LogLevel.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoadingProjects.value = false;
|
isLoadingProjects.value = false;
|
||||||
isLoading.value = false;
|
|
||||||
update(['attendance_dashboard_controller']);
|
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 {
|
Future<void> fetchEmployeesByProject(String? projectId) async {
|
||||||
if (projectId == null) return;
|
if (projectId == null) return;
|
||||||
|
|
||||||
isLoadingEmployees.value = true;
|
isLoadingEmployees.value = true;
|
||||||
|
|
||||||
final response = await ApiService.getEmployeesByProject(projectId);
|
final response = await ApiService.getEmployeesByProject(projectId);
|
||||||
if (response != null) {
|
if (response != null) {
|
||||||
employees = response.map((json) => EmployeeModel.fromJson(json)).toList();
|
employees = response.map((e) => EmployeeModel.fromJson(e)).toList();
|
||||||
for (var emp in employees) {
|
for (var emp in employees) {
|
||||||
uploadingStates[emp.id] = false.obs;
|
uploadingStates[emp.id] = false.obs;
|
||||||
}
|
}
|
||||||
logSafe("Employees fetched: ${employees.length} for project $projectId");
|
logSafe("Employees fetched: ${employees.length} for project $projectId");
|
||||||
update();
|
|
||||||
} else {
|
} else {
|
||||||
logSafe("Failed to fetch employees for project $projectId", level: LogLevel.error);
|
logSafe("Failed to fetch employees for project $projectId", level: LogLevel.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoadingEmployees.value = false;
|
isLoadingEmployees.value = false;
|
||||||
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ------------------ Attendance Capture ------------------
|
||||||
|
|
||||||
Future<bool> captureAndUploadAttendance(
|
Future<bool> captureAndUploadAttendance(
|
||||||
String id,
|
String id,
|
||||||
String employeeId,
|
String employeeId,
|
||||||
@ -137,6 +109,7 @@ class AttendanceController extends GetxController {
|
|||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
uploadingStates[employeeId]?.value = true;
|
uploadingStates[employeeId]?.value = true;
|
||||||
|
|
||||||
XFile? image;
|
XFile? image;
|
||||||
if (imageCapture) {
|
if (imageCapture) {
|
||||||
image = await ImagePicker().pickImage(source: ImageSource.camera, imageQuality: 80);
|
image = await ImagePicker().pickImage(source: ImageSource.camera, imageQuality: 80);
|
||||||
@ -144,24 +117,39 @@ class AttendanceController extends GetxController {
|
|||||||
logSafe("Image capture cancelled.", level: LogLevel.warning);
|
logSafe("Image capture cancelled.", level: LogLevel.warning);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
final compressedBytes = await compressImageToUnder100KB(File(image.path));
|
final compressedBytes = await compressImageToUnder100KB(File(image.path));
|
||||||
if (compressedBytes == null) {
|
if (compressedBytes == null) {
|
||||||
logSafe("Image compression failed.", level: LogLevel.error);
|
logSafe("Image compression failed.", level: LogLevel.error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
final compressedFile = await saveCompressedImageToFile(compressedBytes);
|
final compressedFile = await saveCompressedImageToFile(compressedBytes);
|
||||||
image = XFile(compressedFile.path);
|
image = XFile(compressedFile.path);
|
||||||
}
|
}
|
||||||
final hasLocationPermission = await _handleLocationPermission();
|
|
||||||
if (!hasLocationPermission) return false;
|
if (!await _handleLocationPermission()) return false;
|
||||||
final position = await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high);
|
final position = await Geolocator.getCurrentPosition(
|
||||||
final imageName = imageCapture ? ApiService.generateImageName(employeeId, employees.length + 1) : "";
|
desiredAccuracy: LocationAccuracy.high);
|
||||||
|
|
||||||
|
final imageName = imageCapture
|
||||||
|
? ApiService.generateImageName(employeeId, employees.length + 1)
|
||||||
|
: "";
|
||||||
|
|
||||||
final result = await ApiService.uploadAttendanceImage(
|
final result = await ApiService.uploadAttendanceImage(
|
||||||
id, employeeId, image, position.latitude, position.longitude,
|
id,
|
||||||
imageName: imageName, projectId: projectId, comment: comment,
|
employeeId,
|
||||||
action: action, imageCapture: imageCapture, markTime: markTime,
|
image,
|
||||||
|
position.latitude,
|
||||||
|
position.longitude,
|
||||||
|
imageName: imageName,
|
||||||
|
projectId: projectId,
|
||||||
|
comment: comment,
|
||||||
|
action: action,
|
||||||
|
imageCapture: imageCapture,
|
||||||
|
markTime: markTime,
|
||||||
);
|
);
|
||||||
|
|
||||||
logSafe("Attendance uploaded for $employeeId, action: $action");
|
logSafe("Attendance uploaded for $employeeId, action: $action");
|
||||||
return result;
|
return result;
|
||||||
} catch (e, stacktrace) {
|
} 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 today = DateTime.now();
|
||||||
|
|
||||||
final picked = await showDateRangePicker(
|
final picked = await showDateRangePicker(
|
||||||
context: context,
|
context: context,
|
||||||
firstDate: DateTime(2022),
|
firstDate: DateTime(2022),
|
||||||
@ -190,14 +303,13 @@ class AttendanceController extends GetxController {
|
|||||||
child: Theme(
|
child: Theme(
|
||||||
data: Theme.of(context).copyWith(
|
data: Theme.of(context).copyWith(
|
||||||
colorScheme: ColorScheme.light(
|
colorScheme: ColorScheme.light(
|
||||||
primary: const Color.fromARGB(255, 95, 132, 255),
|
primary: const Color(0xFF5F84FF),
|
||||||
onPrimary: Colors.white,
|
onPrimary: Colors.white,
|
||||||
onSurface: Colors.teal.shade800,
|
onSurface: Colors.teal.shade800,
|
||||||
),
|
),
|
||||||
textButtonTheme: TextButtonThemeData(
|
textButtonTheme: TextButtonThemeData(
|
||||||
style: TextButton.styleFrom(foregroundColor: Colors.teal),
|
style: TextButton.styleFrom(foregroundColor: Colors.teal),
|
||||||
),
|
),
|
||||||
dialogTheme: DialogThemeData(backgroundColor: Colors.white),
|
|
||||||
),
|
),
|
||||||
child: child!,
|
child: child!,
|
||||||
),
|
),
|
||||||
@ -210,6 +322,7 @@ class AttendanceController extends GetxController {
|
|||||||
startDateAttendance = picked.start;
|
startDateAttendance = picked.start;
|
||||||
endDateAttendance = picked.end;
|
endDateAttendance = picked.end;
|
||||||
logSafe("Date range selected: $startDateAttendance to $endDateAttendance");
|
logSafe("Date range selected: $startDateAttendance to $endDateAttendance");
|
||||||
|
|
||||||
await controller.fetchAttendanceLogs(
|
await controller.fetchAttendanceLogs(
|
||||||
Get.find<ProjectController>().selectedProject?.id,
|
Get.find<ProjectController>().selectedProject?.id,
|
||||||
dateFrom: picked.start,
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
import 'package:marco/controller/permission_controller.dart';
|
import 'package:marco/controller/permission_controller.dart';
|
||||||
import 'package:marco/controller/dashboard/attendance_screen_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/widgets/my_text.dart';
|
||||||
import 'package:marco/helpers/utils/permission_constants.dart';
|
import 'package:marco/helpers/utils/permission_constants.dart';
|
||||||
import 'package:marco/helpers/utils/base_bottom_sheet.dart';
|
import 'package:marco/helpers/utils/base_bottom_sheet.dart';
|
||||||
@ -19,7 +19,7 @@ class AttendanceFilterBottomSheet extends StatefulWidget {
|
|||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_AttendanceFilterBottomSheetState createState() =>
|
State<AttendanceFilterBottomSheet> createState() =>
|
||||||
_AttendanceFilterBottomSheetState();
|
_AttendanceFilterBottomSheetState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,22 +54,20 @@ class _AttendanceFilterBottomSheetState
|
|||||||
{'label': 'Regularization Requests', 'value': 'regularizationRequests'},
|
{'label': 'Regularization Requests', 'value': 'regularizationRequests'},
|
||||||
];
|
];
|
||||||
|
|
||||||
final filteredViewOptions = viewOptions.where((item) {
|
final filteredOptions = viewOptions.where((item) {
|
||||||
if (item['value'] == 'regularizationRequests') {
|
return item['value'] != 'regularizationRequests' ||
|
||||||
return hasRegularizationPermission;
|
hasRegularizationPermission;
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
List<Widget> widgets = [
|
final List<Widget> widgets = [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 4),
|
padding: EdgeInsets.only(bottom: 4),
|
||||||
child: Align(
|
child: Align(
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
child: MyText.titleSmall("View", fontWeight: 600),
|
child: MyText.titleSmall("View", fontWeight: 600),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
...filteredViewOptions.map((item) {
|
...filteredOptions.map((item) {
|
||||||
return RadioListTile<String>(
|
return RadioListTile<String>(
|
||||||
dense: true,
|
dense: true,
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
@ -81,14 +79,14 @@ class _AttendanceFilterBottomSheetState
|
|||||||
groupValue: tempSelectedTab,
|
groupValue: tempSelectedTab,
|
||||||
onChanged: (value) => setState(() => tempSelectedTab = value!),
|
onChanged: (value) => setState(() => tempSelectedTab = value!),
|
||||||
);
|
);
|
||||||
}).toList(),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
if (tempSelectedTab == 'attendanceLogs') {
|
if (tempSelectedTab == 'attendanceLogs') {
|
||||||
widgets.addAll([
|
widgets.addAll([
|
||||||
const Divider(),
|
const Divider(),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 12, bottom: 4),
|
padding: EdgeInsets.only(top: 12, bottom: 4),
|
||||||
child: Align(
|
child: Align(
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
child: MyText.titleSmall("Date Range", fontWeight: 600),
|
child: MyText.titleSmall("Date Range", fontWeight: 600),
|
||||||
|
|||||||
189
lib/view/dashboard/Attendence/attendance_logs_tab.dart
Normal file
189
lib/view/dashboard/Attendence/attendance_logs_tab.dart
Normal 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)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,77 +2,64 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/helpers/theme/app_theme.dart';
|
import 'package:marco/helpers/theme/app_theme.dart';
|
||||||
import 'package:marco/helpers/utils/mixins/ui_mixin.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.dart';
|
||||||
import 'package:marco/helpers/widgets/my_flex_item.dart';
|
import 'package:marco/helpers/widgets/my_flex_item.dart';
|
||||||
import 'package:marco/helpers/widgets/my_spacing.dart';
|
import 'package:marco/helpers/widgets/my_spacing.dart';
|
||||||
import 'package:marco/helpers/widgets/my_text.dart';
|
import 'package:marco/helpers/widgets/my_text.dart';
|
||||||
import 'package:marco/controller/dashboard/attendance_screen_controller.dart';
|
import 'package:marco/controller/dashboard/attendance_screen_controller.dart';
|
||||||
import 'package:marco/controller/permission_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/model/attendance/attendence_filter_sheet.dart';
|
||||||
import 'package:marco/controller/project_controller.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 {
|
class AttendanceScreen extends StatefulWidget {
|
||||||
AttendanceScreen({super.key});
|
const AttendanceScreen({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<AttendanceScreen> createState() => _AttendanceScreenState();
|
State<AttendanceScreen> createState() => _AttendanceScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
||||||
final AttendanceController attendanceController =
|
final attendanceController = Get.put(AttendanceController());
|
||||||
Get.put(AttendanceController());
|
final permissionController = Get.put(PermissionController());
|
||||||
final PermissionController permissionController =
|
final projectController = Get.find<ProjectController>();
|
||||||
Get.put(PermissionController());
|
|
||||||
|
|
||||||
String selectedTab = 'todaysAttendance';
|
String selectedTab = 'todaysAttendance';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
final projectController = Get.find<ProjectController>();
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
final attendanceController = Get.find<AttendanceController>();
|
// Listen for future project selection changes
|
||||||
|
ever<String>(projectController.selectedProjectId, (projectId) async {
|
||||||
|
if (projectId.isNotEmpty) await _loadData(projectId);
|
||||||
|
});
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
// Load initial data
|
||||||
// Listen for future changes in selected project
|
final projectId = projectController.selectedProjectId.value;
|
||||||
ever<String?>(projectController.selectedProjectId!, (projectId) async {
|
if (projectId.isNotEmpty) _loadData(projectId);
|
||||||
if (projectId != null && projectId.isNotEmpty) {
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadData(String projectId) async {
|
||||||
try {
|
try {
|
||||||
await attendanceController.loadAttendanceData(projectId);
|
await attendanceController.loadAttendanceData(projectId);
|
||||||
attendanceController.update(['attendance_dashboard_controller']);
|
attendanceController.update(['attendance_dashboard_controller']);
|
||||||
} catch (e) {
|
} 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
|
Future<void> _refreshData() async {
|
||||||
Widget build(BuildContext context) {
|
final projectId = projectController.selectedProjectId.value;
|
||||||
return Scaffold(
|
if (projectId.isNotEmpty) await _loadData(projectId);
|
||||||
appBar: PreferredSize(
|
}
|
||||||
preferredSize: const Size.fromHeight(72),
|
|
||||||
child: AppBar(
|
Widget _buildAppBar() {
|
||||||
|
return AppBar(
|
||||||
backgroundColor: const Color(0xFFF5F5F5),
|
backgroundColor: const Color(0xFFF5F5F5),
|
||||||
elevation: 0.5,
|
elevation: 0.5,
|
||||||
automaticallyImplyLeading: false,
|
automaticallyImplyLeading: false,
|
||||||
@ -83,31 +70,22 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.arrow_back_ios_new,
|
icon: const Icon(Icons.arrow_back_ios_new, color: Colors.black, size: 20),
|
||||||
color: Colors.black, size: 20),
|
|
||||||
onPressed: () => Get.offNamed('/dashboard'),
|
onPressed: () => Get.offNamed('/dashboard'),
|
||||||
),
|
),
|
||||||
MySpacing.width(8),
|
MySpacing.width(8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
children: [
|
||||||
MyText.titleLarge(
|
MyText.titleLarge('Attendance', fontWeight: 700, color: Colors.black),
|
||||||
'Attendance',
|
|
||||||
fontWeight: 700,
|
|
||||||
color: Colors.black,
|
|
||||||
),
|
|
||||||
MySpacing.height(2),
|
MySpacing.height(2),
|
||||||
GetBuilder<ProjectController>(
|
GetBuilder<ProjectController>(
|
||||||
builder: (projectController) {
|
builder: (projectController) {
|
||||||
final projectName =
|
final projectName = projectController.selectedProject?.name ?? 'Select Project';
|
||||||
projectController.selectedProject?.name ??
|
|
||||||
'Select Project';
|
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.work_outline,
|
const Icon(Icons.work_outline, size: 14, color: Colors.grey),
|
||||||
size: 14, color: Colors.grey),
|
|
||||||
MySpacing.width(4),
|
MySpacing.width(4),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: MyText.bodySmall(
|
child: MyText.bodySmall(
|
||||||
@ -127,24 +105,11 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
),
|
}
|
||||||
body: SafeArea(
|
|
||||||
child: SingleChildScrollView(
|
Widget _buildFilterAndRefreshRow() {
|
||||||
padding: MySpacing.x(0),
|
return Row(
|
||||||
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,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
MyText.bodyMedium("Filter", fontWeight: 600),
|
MyText.bodyMedium("Filter", fontWeight: 600),
|
||||||
@ -153,15 +118,12 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
|||||||
child: InkWell(
|
child: InkWell(
|
||||||
borderRadius: BorderRadius.circular(24),
|
borderRadius: BorderRadius.circular(24),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final result = await showModalBottomSheet<
|
final result = await showModalBottomSheet<Map<String, dynamic>>(
|
||||||
Map<String, dynamic>>(
|
|
||||||
context: context,
|
context: context,
|
||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
|
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
shape: const RoundedRectangleBorder(
|
shape: const RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.vertical(
|
borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
|
||||||
top: Radius.circular(12)),
|
|
||||||
),
|
),
|
||||||
builder: (context) => AttendanceFilterBottomSheet(
|
builder: (context) => AttendanceFilterBottomSheet(
|
||||||
controller: attendanceController,
|
controller: attendanceController,
|
||||||
@ -171,49 +133,28 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
final selectedProjectId =
|
final selectedProjectId = projectController.selectedProjectId.value;
|
||||||
Get.find<ProjectController>()
|
final selectedView = result['selectedTab'] as String?;
|
||||||
.selectedProjectId
|
|
||||||
.value;
|
|
||||||
|
|
||||||
final selectedView =
|
if (selectedProjectId.isNotEmpty) {
|
||||||
result['selectedTab'] as String?;
|
|
||||||
|
|
||||||
if (selectedProjectId != null) {
|
|
||||||
try {
|
try {
|
||||||
await attendanceController
|
await attendanceController.fetchEmployeesByProject(selectedProjectId);
|
||||||
.fetchEmployeesByProject(
|
await attendanceController.fetchAttendanceLogs(selectedProjectId);
|
||||||
selectedProjectId);
|
await attendanceController.fetchRegularizationLogs(selectedProjectId);
|
||||||
await attendanceController
|
await attendanceController.fetchProjectData(selectedProjectId);
|
||||||
.fetchAttendanceLogs(selectedProjectId);
|
|
||||||
await attendanceController
|
|
||||||
.fetchRegularizationLogs(
|
|
||||||
selectedProjectId);
|
|
||||||
await attendanceController
|
|
||||||
.fetchProjectData(selectedProjectId);
|
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
attendanceController.update(
|
|
||||||
['attendance_dashboard_controller']);
|
attendanceController.update(['attendance_dashboard_controller']);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedView != null &&
|
if (selectedView != null && selectedView != selectedTab) {
|
||||||
selectedView != selectedTab) {
|
setState(() => selectedTab = selectedView);
|
||||||
setState(() {
|
|
||||||
selectedTab = selectedView;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: MouseRegion(
|
|
||||||
cursor: SystemMouseCursors.click,
|
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: Icon(
|
child: Icon(Icons.tune, color: Colors.blueAccent, size: 20),
|
||||||
Icons.tune,
|
|
||||||
color: Colors.blueAccent,
|
|
||||||
size: 20,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -223,42 +164,19 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
|||||||
message: 'Refresh Data',
|
message: 'Refresh Data',
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
borderRadius: BorderRadius.circular(24),
|
borderRadius: BorderRadius.circular(24),
|
||||||
onTap: () async {
|
onTap: _refreshData,
|
||||||
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(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: Icon(
|
child: Icon(Icons.refresh, color: Colors.green, size: 22),
|
||||||
Icons.refresh,
|
|
||||||
color: Colors.green,
|
|
||||||
size: 22,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
);
|
||||||
MySpacing.height(flexSpacing),
|
}
|
||||||
MyFlex(children: [
|
|
||||||
MyFlexItem(
|
Widget _buildNoProjectWidget() {
|
||||||
sizes: 'lg-12 md-12 sm-12',
|
return Center(
|
||||||
child: noProjectSelected
|
|
||||||
? Center(
|
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(24.0),
|
padding: const EdgeInsets.all(24.0),
|
||||||
child: MyText.titleMedium(
|
child: MyText.titleMedium(
|
||||||
@ -267,580 +185,57 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
|||||||
color: Colors.grey[600],
|
color: Colors.grey[600],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
);
|
||||||
: selectedTab == 'todaysAttendance'
|
}
|
||||||
? employeeListTab()
|
|
||||||
: selectedTab == 'attendanceLogs'
|
Widget _buildSelectedTabContent() {
|
||||||
? employeeLog()
|
switch (selectedTab) {
|
||||||
: regularizationScreen(),
|
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,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
157
lib/view/dashboard/Attendence/regularization_requests_tab.dart
Normal file
157
lib/view/dashboard/Attendence/regularization_requests_tab.dart
Normal 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)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
131
lib/view/dashboard/Attendence/todays_attendance_tab.dart
Normal file
131
lib/view/dashboard/Attendence/todays_attendance_tab.dart
Normal 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)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user