Vaibhav_Feature-#768 #59
@ -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