Merge pull request 'feat: Enhance ReportTaskController with image picking and form field management' (#34) from Vaibhav_Bug-#411 into main

Reviewed-on: #34
This commit is contained in:
vaibhav.surve 2025-05-30 09:12:43 +00:00
commit d9ad7581bf
13 changed files with 673 additions and 602 deletions

View File

@ -1,41 +1,49 @@
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:logger/logger.dart'; import 'package:logger/logger.dart';
import 'dart:io';
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/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/helpers/widgets/my_image_compressor.dart';
final Logger log = Logger(); final Logger log = Logger();
class AttendanceController extends GetxController { class AttendanceController extends GetxController {
// Data lists
List<AttendanceModel> attendances = []; List<AttendanceModel> attendances = [];
List<ProjectModel> projects = []; List<ProjectModel> projects = [];
String? selectedProjectId;
List<EmployeeModel> employees = []; List<EmployeeModel> employees = [];
String selectedTab = 'Employee List';
DateTime? startDateAttendance;
DateTime? endDateAttendance;
List<AttendanceLogModel> attendanceLogs = []; List<AttendanceLogModel> attendanceLogs = [];
List<RegularizationLogModel> regularizationLogs = []; List<RegularizationLogModel> regularizationLogs = [];
List<AttendanceLogViewModel> attendenceLogsView = []; List<AttendanceLogViewModel> attendenceLogsView = [];
RxBool isLoading = true.obs; // initially true // Selected values
String? selectedProjectId;
String selectedTab = 'Employee List';
// Date range for attendance filtering
DateTime? startDateAttendance;
DateTime? endDateAttendance;
// Loading states
RxBool isLoading = true.obs;
RxBool isLoadingProjects = true.obs; RxBool isLoadingProjects = true.obs;
RxBool isLoadingEmployees = true.obs; RxBool isLoadingEmployees = true.obs;
RxBool isLoadingAttendanceLogs = true.obs; RxBool isLoadingAttendanceLogs = true.obs;
RxBool isLoadingRegularizationLogs = true.obs; RxBool isLoadingRegularizationLogs = true.obs;
RxBool isLoadingLogView = true.obs; RxBool isLoadingLogView = true.obs;
// Uploading state per employee (keyed by employeeId)
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs; RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
@override @override
@ -46,7 +54,7 @@ class AttendanceController extends GetxController {
void _initializeDefaults() { void _initializeDefaults() {
_setDefaultDateRange(); _setDefaultDateRange();
fetchProjects(); // fetchProjects will set isLoading to false after loading fetchProjects();
} }
void _setDefaultDateRange() { void _setDefaultDateRange() {
@ -56,8 +64,10 @@ class AttendanceController extends GetxController {
log.i("Default date range set: $startDateAttendance to $endDateAttendance"); log.i("Default date range set: $startDateAttendance to $endDateAttendance");
} }
/// Checks and requests location permission, returns true if granted.
Future<bool> _handleLocationPermission() async { Future<bool> _handleLocationPermission() async {
LocationPermission permission = await Geolocator.checkPermission(); LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) { if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission(); permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) { if (permission == LocationPermission.denied) {
@ -65,13 +75,16 @@ class AttendanceController extends GetxController {
return false; return false;
} }
} }
if (permission == LocationPermission.deniedForever) { if (permission == LocationPermission.deniedForever) {
log.e('Location permissions are permanently denied'); log.e('Location permissions are permanently denied');
return false; return false;
} }
return true; return true;
} }
/// Fetches projects and initializes selected project.
Future<void> fetchProjects() async { Future<void> fetchProjects() async {
isLoadingProjects.value = true; isLoadingProjects.value = true;
isLoading.value = true; isLoading.value = true;
@ -81,11 +94,11 @@ class AttendanceController extends GetxController {
if (response != null && response.isNotEmpty) { if (response != null && response.isNotEmpty) {
projects = response.map((json) => ProjectModel.fromJson(json)).toList(); projects = response.map((json) => ProjectModel.fromJson(json)).toList();
selectedProjectId = projects.first.id.toString(); selectedProjectId = projects.first.id.toString();
log.i("Projects fetched: ${projects.length} projects loaded."); log.i("Projects fetched: ${projects.length}");
await fetchProjectData(selectedProjectId); await fetchProjectData(selectedProjectId);
} else { } else {
log.w("No project data found or API call failed."); log.w("No projects found or API call failed.");
} }
isLoadingProjects.value = false; isLoadingProjects.value = false;
@ -94,6 +107,7 @@ class AttendanceController extends GetxController {
update(['attendance_dashboard_controller']); update(['attendance_dashboard_controller']);
} }
/// Fetches employees, attendance logs and regularization logs for a project.
Future<void> fetchProjectData(String? projectId) async { Future<void> fetchProjectData(String? projectId) async {
if (projectId == null) return; if (projectId == null) return;
@ -107,9 +121,11 @@ class AttendanceController extends GetxController {
]); ]);
isLoading.value = false; isLoading.value = false;
log.i("Project data fetched for project ID: $projectId"); log.i("Project data fetched for project ID: $projectId");
} }
/// Fetches employees for the given project.
Future<void> fetchEmployeesByProject(String? projectId) async { Future<void> fetchEmployeesByProject(String? projectId) async {
if (projectId == null) return; if (projectId == null) return;
@ -120,18 +136,22 @@ class AttendanceController extends GetxController {
if (response != null) { if (response != null) {
employees = response.map((json) => EmployeeModel.fromJson(json)).toList(); employees = response.map((json) => EmployeeModel.fromJson(json)).toList();
// Initialize uploading states for employees
for (var emp in employees) { for (var emp in employees) {
uploadingStates[emp.id] = false.obs; uploadingStates[emp.id] = false.obs;
} }
log.i(
"Employees fetched: ${employees.length} employees for project $projectId"); log.i("Employees fetched: ${employees.length} for project $projectId");
update(); update();
} else { } else {
log.e("Failed to fetch employees for project $projectId"); log.e("Failed to fetch employees for project $projectId");
} }
isLoadingEmployees.value = false; isLoadingEmployees.value = false;
} }
/// Captures image, gets location, and uploads attendance data.
/// Returns true on success.
Future<bool> captureAndUploadAttendance( Future<bool> captureAndUploadAttendance(
String id, String id,
String employeeId, String employeeId,
@ -155,8 +175,8 @@ class AttendanceController extends GetxController {
uploadingStates[employeeId]?.value = false; uploadingStates[employeeId]?.value = false;
return false; return false;
} }
final compressedBytes =
await compressImageToUnder100KB(File(image.path)); final compressedBytes = await compressImageToUnder100KB(File(image.path));
if (compressedBytes == null) { if (compressedBytes == null) {
log.e("Image compression failed."); log.e("Image compression failed.");
uploadingStates[employeeId]?.value = false; uploadingStates[employeeId]?.value = false;
@ -205,6 +225,7 @@ class AttendanceController extends GetxController {
} }
} }
/// Opens a date range picker for attendance filtering and fetches logs on selection.
Future<void> selectDateRangeForAttendance( Future<void> selectDateRangeForAttendance(
BuildContext context, BuildContext context,
AttendanceController controller, AttendanceController controller,
@ -217,18 +238,10 @@ class AttendanceController extends GetxController {
firstDate: DateTime(2022), firstDate: DateTime(2022),
lastDate: todayDateOnly.subtract(const Duration(days: 1)), lastDate: todayDateOnly.subtract(const Duration(days: 1)),
initialDateRange: DateTimeRange( initialDateRange: DateTimeRange(
start: startDateAttendance ?? start: startDateAttendance ?? today.subtract(const Duration(days: 7)),
DateTime.now().subtract(const Duration(days: 7)), end: endDateAttendance ?? todayDateOnly.subtract(const Duration(days: 1)),
end: endDateAttendance ??
todayDateOnly.subtract(const Duration(days: 1)),
), ),
selectableDayPredicate: (DateTime day, DateTime? start, DateTime? end) {
final dayDateOnly = DateTime(day.year, day.month, day.day);
if (dayDateOnly == todayDateOnly) {
return false;
}
return true;
},
builder: (BuildContext context, Widget? child) { builder: (BuildContext context, Widget? child) {
return Center( return Center(
child: SizedBox( child: SizedBox(
@ -242,13 +255,9 @@ class AttendanceController extends GetxController {
onSurface: Colors.teal.shade800, onSurface: Colors.teal.shade800,
), ),
textButtonTheme: TextButtonThemeData( textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom( style: TextButton.styleFrom(foregroundColor: Colors.teal),
foregroundColor: Colors.teal,
),
),
dialogTheme: DialogTheme(
backgroundColor: Colors.white,
), ),
dialogTheme: const DialogTheme(backgroundColor: Colors.white),
), ),
child: child!, child: child!,
), ),
@ -256,6 +265,7 @@ class AttendanceController extends GetxController {
); );
}, },
); );
if (picked != null) { if (picked != null) {
startDateAttendance = picked.start; startDateAttendance = picked.start;
endDateAttendance = picked.end; endDateAttendance = picked.end;
@ -270,6 +280,7 @@ class AttendanceController extends GetxController {
} }
} }
/// Fetches attendance logs filtered by project and date range.
Future<void> fetchAttendanceLogs( Future<void> fetchAttendanceLogs(
String? projectId, { String? projectId, {
DateTime? dateFrom, DateTime? dateFrom,
@ -294,10 +305,12 @@ class AttendanceController extends GetxController {
} else { } else {
log.e("Failed to fetch attendance logs for project $projectId"); log.e("Failed to fetch attendance logs for project $projectId");
} }
isLoadingAttendanceLogs.value = false; isLoadingAttendanceLogs.value = false;
isLoading.value = false; isLoading.value = false;
} }
/// Groups attendance logs by check-in date formatted as 'dd MMM yyyy'.
Map<String, List<AttendanceLogModel>> groupLogsByCheckInDate() { Map<String, List<AttendanceLogModel>> groupLogsByCheckInDate() {
final groupedLogs = <String, List<AttendanceLogModel>>{}; final groupedLogs = <String, List<AttendanceLogModel>>{};
@ -309,6 +322,7 @@ class AttendanceController extends GetxController {
groupedLogs.putIfAbsent(checkInDate, () => []); groupedLogs.putIfAbsent(checkInDate, () => []);
groupedLogs[checkInDate]!.add(logItem); groupedLogs[checkInDate]!.add(logItem);
} }
final sortedEntries = groupedLogs.entries.toList() final sortedEntries = groupedLogs.entries.toList()
..sort((a, b) { ..sort((a, b) {
if (a.key == 'Unknown') return 1; if (a.key == 'Unknown') return 1;
@ -318,13 +332,13 @@ class AttendanceController extends GetxController {
return dateB.compareTo(dateA); return dateB.compareTo(dateA);
}); });
final sortedMap = final sortedMap = Map<String, List<AttendanceLogModel>>.fromEntries(sortedEntries);
Map<String, List<AttendanceLogModel>>.fromEntries(sortedEntries);
log.i("Logs grouped and sorted by check-in date."); log.i("Logs grouped and sorted by check-in date.");
return sortedMap; return sortedMap;
} }
/// Fetches regularization logs for a project.
Future<void> fetchRegularizationLogs( Future<void> fetchRegularizationLogs(
String? projectId, { String? projectId, {
DateTime? dateFrom, DateTime? dateFrom,
@ -338,18 +352,19 @@ class AttendanceController extends GetxController {
final response = await ApiService.getRegularizationLogs(projectId); final response = await ApiService.getRegularizationLogs(projectId);
if (response != null) { if (response != null) {
regularizationLogs = response regularizationLogs =
.map((json) => RegularizationLogModel.fromJson(json)) response.map((json) => RegularizationLogModel.fromJson(json)).toList();
.toList();
log.i("Regularization logs fetched: ${regularizationLogs.length}"); log.i("Regularization logs fetched: ${regularizationLogs.length}");
update(); update();
} else { } else {
log.e("Failed to fetch regularization logs for project $projectId"); log.e("Failed to fetch regularization logs for project $projectId");
} }
isLoadingRegularizationLogs.value = false; isLoadingRegularizationLogs.value = false;
isLoading.value = false; isLoading.value = false;
} }
/// Fetches detailed attendance log view for a specific ID.
Future<void> fetchLogsView(String? id) async { Future<void> fetchLogsView(String? id) async {
if (id == null) return; if (id == null) return;
@ -359,9 +374,8 @@ class AttendanceController extends GetxController {
final response = await ApiService.getAttendanceLogView(id); final response = await ApiService.getAttendanceLogView(id);
if (response != null) { if (response != null) {
attendenceLogsView = response attendenceLogsView =
.map((json) => AttendanceLogViewModel.fromJson(json)) response.map((json) => AttendanceLogViewModel.fromJson(json)).toList();
.toList();
attendenceLogsView.sort((a, b) { attendenceLogsView.sort((a, b) {
if (a.activityTime == null || b.activityTime == null) return 0; if (a.activityTime == null || b.activityTime == null) return 0;

View File

@ -7,11 +7,17 @@ import 'package:get/get.dart';
import 'package:logger/logger.dart'; import 'package:logger/logger.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/controller/task_planing/daily_task_planing_controller.dart'; import 'package:marco/controller/task_planing/daily_task_planing_controller.dart';
import 'package:image_picker/image_picker.dart';
import 'dart:io';
final Logger logger = Logger(); final Logger logger = Logger();
enum ApiStatus { idle, loading, success, failure } enum ApiStatus { idle, loading, success, failure }
final DailyTaskPlaningController taskController = Get.put(DailyTaskPlaningController());
final DailyTaskPlaningController taskController =
Get.put(DailyTaskPlaningController());
final ImagePicker _picker = ImagePicker();
class ReportTaskController extends MyController { class ReportTaskController extends MyController {
List<PlatformFile> files = []; List<PlatformFile> files = [];
MyFormValidator basicValidator = MyFormValidator(); MyFormValidator basicValidator = MyFormValidator();
@ -19,74 +25,71 @@ class ReportTaskController extends MyController {
Rx<ApiStatus> reportStatus = ApiStatus.idle.obs; Rx<ApiStatus> reportStatus = ApiStatus.idle.obs;
Rx<ApiStatus> commentStatus = ApiStatus.idle.obs; Rx<ApiStatus> commentStatus = ApiStatus.idle.obs;
RxList<File> selectedImages = <File>[].obs;
// Controllers for each form field
final assignedDateController = TextEditingController();
final workAreaController = TextEditingController();
final activityController = TextEditingController();
final teamSizeController = TextEditingController();
final taskIdController = TextEditingController();
final assignedController = TextEditingController();
final completedWorkController = TextEditingController();
final commentController = TextEditingController();
final assignedByController = TextEditingController();
final teamMembersController = TextEditingController();
final plannedWorkController = TextEditingController();
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
logger.i("Initializing ReportTaskController..."); logger.i("Initializing ReportTaskController...");
// Add form fields to the validator basicValidator.addField('assigned_date',
basicValidator.addField( label: "Assigned Date", controller: assignedDateController);
'assigned_date', basicValidator.addField('work_area',
label: "Assigned Date", label: "Work Area", controller: workAreaController);
controller: TextEditingController(), basicValidator.addField('activity',
); label: "Activity", controller: activityController);
basicValidator.addField( basicValidator.addField('team_size',
'work_area', label: "Team Size", controller: teamSizeController);
label: "Work Area", basicValidator.addField('task_id',
controller: TextEditingController(), label: "Task Id", controller: taskIdController);
); basicValidator.addField('assigned',
basicValidator.addField( label: "Assigned", controller: assignedController);
'activity', basicValidator.addField('completed_work',
label: "Activity", label: "Completed Work",
controller: TextEditingController(), required: true,
); controller: completedWorkController);
basicValidator.addField( basicValidator.addField('comment',
'team_size', label: "Comment", required: true, controller: commentController);
label: "Team Size", basicValidator.addField('assigned_by',
controller: TextEditingController(), label: "Assigned By", controller: assignedByController);
); basicValidator.addField('team_members',
basicValidator.addField( label: "Team Members", controller: teamMembersController);
'task_id', basicValidator.addField('planned_work',
label: "Task Id", label: "Planned Work", controller: plannedWorkController);
controller: TextEditingController(),
);
basicValidator.addField(
'assigned',
label: "Assigned",
controller: TextEditingController(),
);
basicValidator.addField(
'completed_work',
label: "Completed Work",
required: true,
controller: TextEditingController(),
);
basicValidator.addField(
'comment',
label: "Comment",
required: true,
controller: TextEditingController(),
);
basicValidator.addField(
'assigned_by',
label: "Assigned By",
controller: TextEditingController(),
);
basicValidator.addField(
'team_members',
label: "Team Members",
controller: TextEditingController(),
);
basicValidator.addField(
'planned_work',
label: "Planned Work",
controller: TextEditingController(),
);
logger.i( logger.i(
"Fields initialized for assigned_date, work_area, activity, team_size, assigned, completed_work, and comment."); "Fields initialized for assigned_date, work_area, activity, team_size, assigned, completed_work, and comment.");
} }
@override
void onClose() {
assignedDateController.dispose();
workAreaController.dispose();
activityController.dispose();
teamSizeController.dispose();
taskIdController.dispose();
assignedController.dispose();
completedWorkController.dispose();
commentController.dispose();
assignedByController.dispose();
teamMembersController.dispose();
plannedWorkController.dispose();
super.onClose();
}
Future<void> reportTask({ Future<void> reportTask({
required String projectId, required String projectId,
required String comment, required String comment,
@ -96,10 +99,9 @@ class ReportTaskController extends MyController {
}) async { }) async {
logger.i("Starting task report..."); logger.i("Starting task report...");
final completedWork = final completedWork = completedWorkController.text.trim();
basicValidator.getController('completed_work')?.text.trim();
if (completedWork == null || completedWork.isEmpty) { if (completedWork.isEmpty) {
showAppSnackbar( showAppSnackbar(
title: "Error", title: "Error",
message: "Completed work is required.", message: "Completed work is required.",
@ -107,6 +109,7 @@ class ReportTaskController extends MyController {
); );
return; return;
} }
final completedWorkInt = int.tryParse(completedWork); final completedWorkInt = int.tryParse(completedWork);
if (completedWorkInt == null || completedWorkInt <= 0) { if (completedWorkInt == null || completedWorkInt <= 0) {
showAppSnackbar( showAppSnackbar(
@ -116,9 +119,9 @@ class ReportTaskController extends MyController {
); );
return; return;
} }
final commentField = basicValidator.getController('comment')?.text.trim();
if (commentField == null || commentField.isEmpty) { final commentField = commentController.text.trim();
if (commentField.isEmpty) {
showAppSnackbar( showAppSnackbar(
title: "Error", title: "Error",
message: "Comment is required.", message: "Comment is required.",
@ -143,6 +146,7 @@ class ReportTaskController extends MyController {
message: "Task reported successfully!", message: "Task reported successfully!",
type: SnackbarType.success, type: SnackbarType.success,
); );
await taskController.fetchTaskData(projectId);
} else { } else {
showAppSnackbar( showAppSnackbar(
title: "Error", title: "Error",
@ -168,9 +172,8 @@ class ReportTaskController extends MyController {
}) async { }) async {
logger.i("Starting task comment..."); logger.i("Starting task comment...");
final commentField = basicValidator.getController('comment')?.text.trim(); final commentField = commentController.text.trim();
if (commentField.isEmpty) {
if (commentField == null || commentField.isEmpty) {
showAppSnackbar( showAppSnackbar(
title: "Error", title: "Error",
message: "Comment is required.", message: "Comment is required.",
@ -213,4 +216,27 @@ class ReportTaskController extends MyController {
isLoading.value = false; isLoading.value = false;
} }
} }
Future<void> pickImages({required bool fromCamera}) async {
if (fromCamera) {
final pickedFile = await _picker.pickImage(
source: ImageSource.camera,
imageQuality: 75,
);
if (pickedFile != null) {
selectedImages.add(File(pickedFile.path));
}
} else {
final pickedFiles = await _picker.pickMultiImage(imageQuality: 75);
if (pickedFiles != null && pickedFiles.isNotEmpty) {
selectedImages.addAll(pickedFiles.map((xfile) => File(xfile.path)));
}
}
}
void removeImageAt(int index) {
if (index >= 0 && index < selectedImages.length) {
selectedImages.removeAt(index);
}
}
} }

View File

@ -13,7 +13,8 @@ final Logger logger = Logger();
class PermissionService { class PermissionService {
static final Map<String, Map<String, dynamic>> _userDataCache = {}; static final Map<String, Map<String, dynamic>> _userDataCache = {};
static Future<Map<String, dynamic>> fetchAllUserData(String token, {bool hasRetried = false}) async { static Future<Map<String, dynamic>> fetchAllUserData(String token,
{bool hasRetried = false}) async {
// Return from cache if available // Return from cache if available
if (_userDataCache.containsKey(token)) { if (_userDataCache.containsKey(token)) {
return _userDataCache[token]!; return _userDataCache[token]!;
@ -51,10 +52,11 @@ class PermissionService {
throw Exception('Unauthorized. Token refresh failed.'); throw Exception('Unauthorized. Token refresh failed.');
} }
final errorMessage = json.decode(response.body)['message'] ?? 'Unknown error'; final errorMessage =
json.decode(response.body)['message'] ?? 'Unknown error';
throw Exception('Failed to load data: $errorMessage'); throw Exception('Failed to load data: $errorMessage');
} catch (e) { } catch (e) {
logger.e('Error fetching user data: $e'); // <-- Use logger here logger.e('Error fetching user data: $e');
rethrow; rethrow;
} }
} }
@ -66,8 +68,11 @@ class PermissionService {
Get.offAllNamed('/auth/login'); Get.offAllNamed('/auth/login');
} }
static List<UserPermission> _parsePermissions(List<dynamic> featurePermissions) => static List<UserPermission> _parsePermissions(
featurePermissions.map((id) => UserPermission.fromJson({'id': id})).toList(); List<dynamic> featurePermissions) =>
featurePermissions
.map((id) => UserPermission.fromJson({'id': id}))
.toList();
static EmployeeInfo _parseEmployeeInfo(Map<String, dynamic> employeeData) => static EmployeeInfo _parseEmployeeInfo(Map<String, dynamic> employeeData) =>
EmployeeInfo.fromJson(employeeData); EmployeeInfo.fromJson(employeeData);

View File

@ -5,6 +5,8 @@ import 'package:shared_preferences/shared_preferences.dart';
import 'package:marco/model/user_permission.dart'; import 'package:marco/model/user_permission.dart';
import 'package:marco/model/employee_info.dart'; import 'package:marco/model/employee_info.dart';
import 'dart:convert'; import 'dart:convert';
import 'package:get/route_manager.dart';
class LocalStorage { class LocalStorage {
static const String _loggedInUserKey = "user"; static const String _loggedInUserKey = "user";
static const String _themeCustomizerKey = "theme_customizer"; static const String _themeCustomizerKey = "theme_customizer";
@ -43,7 +45,7 @@ class LocalStorage {
.toList(); .toList();
} }
return []; // Return empty list if no permissions are found return [];
} }
static Future<bool> removeUserPermissions() async { static Future<bool> removeUserPermissions() async {
@ -62,7 +64,7 @@ class LocalStorage {
final Map<String, dynamic> json = jsonDecode(storedJson); final Map<String, dynamic> json = jsonDecode(storedJson);
return EmployeeInfo.fromJson(json); return EmployeeInfo.fromJson(json);
} }
return null; // Return null if no employee info is found return null;
} }
static Future<bool> removeEmployeeInfo() async { static Future<bool> removeEmployeeInfo() async {
@ -130,4 +132,14 @@ class LocalStorage {
static Future<bool> setRefreshToken(String refreshToken) { static Future<bool> setRefreshToken(String refreshToken) {
return setToken(_refreshTokenKey, refreshToken); return setToken(_refreshTokenKey, refreshToken);
} }
static Future<void> logout() async {
await removeLoggedInUser();
await removeToken(_jwtTokenKey);
await removeToken(_refreshTokenKey);
await removeUserPermissions();
await removeEmployeeInfo();
Get.offAllNamed('/auth/login');
await preferences.remove(_languageKey);
await preferences.remove(_themeCustomizerKey);
}
} }

View File

@ -11,6 +11,7 @@ import 'package:marco/helpers/theme/theme_customizer.dart';
import 'package:marco/routes.dart'; import 'package:marco/routes.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:url_strategy/url_strategy.dart'; import 'package:url_strategy/url_strategy.dart';
import 'package:marco/helpers/services/auth_service.dart';
Future<void> main() async { Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
@ -33,32 +34,47 @@ Future<void> main() async {
class MyApp extends StatelessWidget { class MyApp extends StatelessWidget {
const MyApp({super.key}); const MyApp({super.key});
Future<String> _getInitialRoute() async {
return AuthService.isLoggedIn ? "/dashboard" : "/auth/login";
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Consumer<AppNotifier>( return Consumer<AppNotifier>(
builder: (_, notifier, ___) { builder: (_, notifier, ___) {
return GetMaterialApp( return FutureBuilder<String>(
debugShowCheckedModeBanner: false, future: _getInitialRoute(),
theme: AppTheme.lightTheme, builder: (context, snapshot) {
darkTheme: AppTheme.darkTheme, if (!snapshot.hasData) {
themeMode: ThemeCustomizer.instance.theme, return MaterialApp(
navigatorKey: NavigationService.navigatorKey, home: Center(child: CircularProgressIndicator()),
initialRoute: "/dashboard", );
getPages: getPageRoute(), }
builder: (context, child) {
NavigationService.registerContext(context); return GetMaterialApp(
return Directionality( debugShowCheckedModeBanner: false,
textDirection: AppTheme.textDirection, theme: AppTheme.lightTheme,
child: child ?? Container()); darkTheme: AppTheme.darkTheme,
themeMode: ThemeCustomizer.instance.theme,
navigatorKey: NavigationService.navigatorKey,
initialRoute: snapshot.data!,
getPages: getPageRoute(),
builder: (context, child) {
NavigationService.registerContext(context);
return Directionality(
textDirection: AppTheme.textDirection,
child: child ?? Container(),
);
},
localizationsDelegates: [
AppLocalizationsDelegate(context),
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: Language.getLocales(),
);
}, },
localizationsDelegates: [
AppLocalizationsDelegate(context),
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: Language.getLocales(),
); );
}, },
); );

View File

@ -3,6 +3,7 @@ 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: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';
class AttendanceFilterBottomSheet extends StatelessWidget { class AttendanceFilterBottomSheet extends StatelessWidget {
final AttendanceController controller; final AttendanceController controller;
@ -38,6 +39,20 @@ class AttendanceFilterBottomSheet extends StatelessWidget {
permissionController.isUserAssignedToProject(project.id.toString())) permissionController.isUserAssignedToProject(project.id.toString()))
.toList(); .toList();
bool hasRegularizationPermission =
permissionController.hasPermission(Permissions.regularizeAttendance);
final viewOptions = [
{'label': 'Today\'s Attendance', 'value': 'todaysAttendance'},
{'label': 'Attendance Logs', 'value': 'attendanceLogs'},
{'label': 'Regularization Requests', 'value': 'regularizationRequests'},
];
final filteredViewOptions = viewOptions.where((item) {
if (item['value'] == 'regularizationRequests') {
return hasRegularizationPermission;
}
return true;
}).toList();
return StatefulBuilder(builder: (context, setState) { return StatefulBuilder(builder: (context, setState) {
List<Widget> filterWidgets; List<Widget> filterWidgets;
@ -104,14 +119,7 @@ class AttendanceFilterBottomSheet extends StatelessWidget {
), ),
), ),
), ),
...[ ...filteredViewOptions.map((item) {
{'label': 'Today\'s Attendance', 'value': 'todaysAttendance'},
{'label': 'Attendance Logs', 'value': 'attendanceLogs'},
{
'label': 'Regularization Requests',
'value': 'regularizationRequests'
},
].map((item) {
return RadioListTile<String>( return RadioListTile<String>(
dense: true, dense: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 12), contentPadding: const EdgeInsets.symmetric(horizontal: 12),
@ -209,7 +217,7 @@ class AttendanceFilterBottomSheet extends StatelessWidget {
width: double.infinity, width: double.infinity,
child: ElevatedButton( child: ElevatedButton(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Color.fromARGB(255, 95, 132, 255), backgroundColor: const Color.fromARGB(255, 95, 132, 255),
padding: const EdgeInsets.symmetric(vertical: 12), padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),

View File

@ -58,6 +58,7 @@ class _CommentTaskBottomSheetState extends State<CommentTaskBottomSheet>
data['assigned'] ?? ''; data['assigned'] ?? '';
controller.basicValidator.getController('task_id')?.text = controller.basicValidator.getController('task_id')?.text =
data['taskId'] ?? ''; data['taskId'] ?? '';
controller.basicValidator.getController('comment')?.clear();
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (_scrollController.hasClients) { if (_scrollController.hasClients) {
_scrollController.jumpTo(_scrollController.position.maxScrollExtent); _scrollController.jumpTo(_scrollController.position.maxScrollExtent);

View File

@ -10,8 +10,8 @@ import 'package:marco/helpers/widgets/my_text_style.dart';
class ReportTaskBottomSheet extends StatefulWidget { class ReportTaskBottomSheet extends StatefulWidget {
final Map<String, dynamic> taskData; final Map<String, dynamic> taskData;
final VoidCallback? onReportSuccess;
const ReportTaskBottomSheet({super.key, required this.taskData}); const ReportTaskBottomSheet({super.key, required this.taskData,this.onReportSuccess,});
@override @override
State<ReportTaskBottomSheet> createState() => _ReportTaskBottomSheetState(); State<ReportTaskBottomSheet> createState() => _ReportTaskBottomSheetState();
@ -43,6 +43,8 @@ class _ReportTaskBottomSheetState extends State<ReportTaskBottomSheet>
taskData['assigned'] ?? ''; taskData['assigned'] ?? '';
controller.basicValidator.getController('task_id')?.text = controller.basicValidator.getController('task_id')?.text =
taskData['taskId'] ?? ''; taskData['taskId'] ?? '';
controller.basicValidator.getController('completed_work')?.clear();
controller.basicValidator.getController('comment')?.clear();
} }
@override @override
@ -222,8 +224,10 @@ class _ReportTaskBottomSheetState extends State<ReportTaskBottomSheet>
0, 0,
checklist: [], checklist: [],
reportedDate: DateTime.now(), reportedDate: DateTime.now(),
); );
if (widget.onReportSuccess != null) {
widget.onReportSuccess!();
}
} }
}, },
elevation: 0, elevation: 0,

View File

@ -5,15 +5,11 @@ import 'package:marco/view/auth/forgot_password_screen.dart';
import 'package:marco/view/auth/login_screen.dart'; import 'package:marco/view/auth/login_screen.dart';
import 'package:marco/view/auth/register_account_screen.dart'; import 'package:marco/view/auth/register_account_screen.dart';
import 'package:marco/view/auth/reset_password_screen.dart'; import 'package:marco/view/auth/reset_password_screen.dart';
// import 'package:marco/view/dashboard/ecommerce_screen.dart';
import 'package:marco/view/error_pages/coming_soon_screen.dart'; import 'package:marco/view/error_pages/coming_soon_screen.dart';
import 'package:marco/view/error_pages/error_404_screen.dart'; import 'package:marco/view/error_pages/error_404_screen.dart';
import 'package:marco/view/error_pages/error_500_screen.dart'; import 'package:marco/view/error_pages/error_500_screen.dart';
// import 'package:marco/view/dashboard/attendance_screen.dart';
// import 'package:marco/view/dashboard/attendanceScreen.dart';
import 'package:marco/view/dashboard/dashboard_screen.dart'; import 'package:marco/view/dashboard/dashboard_screen.dart';
import 'package:marco/view/dashboard/add_employee_screen.dart'; import 'package:marco/view/dashboard/add_employee_screen.dart';
import 'package:marco/view/dashboard/employee_screen.dart';
import 'package:marco/view/dashboard/daily_task_screen.dart'; import 'package:marco/view/dashboard/daily_task_screen.dart';
import 'package:marco/view/taskPlaning/report_task_screen.dart'; import 'package:marco/view/taskPlaning/report_task_screen.dart';
import 'package:marco/view/taskPlaning/comment_task_screen.dart'; import 'package:marco/view/taskPlaning/comment_task_screen.dart';
@ -33,7 +29,7 @@ getPageRoute() {
var routes = [ var routes = [
GetPage( GetPage(
name: '/', name: '/',
page: () => AttendanceScreen(), page: () => DashboardScreen(),
middlewares: [AuthMiddleware()]), middlewares: [AuthMiddleware()]),
// Dashboard // Dashboard
GetPage( GetPage(

View File

@ -70,7 +70,6 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
"Filter", "Filter",
fontWeight: 600, fontWeight: 600,
), ),
// Wrap with Tooltip and InkWell for interactive feedback
Tooltip( Tooltip(
message: 'Filter Project', message: 'Filter Project',
child: InkWell( child: InkWell(
@ -232,158 +231,148 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
], ],
), ),
), ),
MyCard.bordered( if (isLoading)
borderRadiusAll: 4, const SizedBox(
border: Border.all(color: Colors.grey.withOpacity(0.2)), height: 120,
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom), child: Center(child: CircularProgressIndicator()),
paddingAll: 8, )
child: isLoading else if (employees.isEmpty)
? Center(child: CircularProgressIndicator()) SizedBox(
: employees.isEmpty height: 120,
? Center( child: Center(
child: MyText.bodySmall( child: MyText.bodySmall(
"No Employees Assigned to This Project", "No Employees Assigned to This Project",
fontWeight: 600, fontWeight: 600,
), ),
) ),
: Column( )
children: List.generate(employees.length, (index) { else
final employee = employees[index]; MyCard.bordered(
return Column( 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: [ children: [
Padding( Avatar(
padding: EdgeInsets.only(bottom: 8), firstName: employee.firstName,
child: MyContainer( lastName: employee.lastName,
paddingAll: 5, size: 31,
child: Row( ),
crossAxisAlignment: MySpacing.width(16),
CrossAxisAlignment.start, Expanded(
children: [ child: Column(
Avatar( crossAxisAlignment: CrossAxisAlignment.start,
firstName: employee.firstName, children: [
lastName: employee.lastName, Row(
size: 31, children: [
), MyText.bodyMedium(
MySpacing.width(16), employee.name,
Expanded( fontWeight: 600,
child: Column( overflow: TextOverflow.ellipsis,
crossAxisAlignment: maxLines: 1,
CrossAxisAlignment.start,
children: [
Row(
children: [
MyText.bodyMedium(
employee.name,
fontWeight: 600,
overflow:
TextOverflow.ellipsis,
maxLines: 1,
),
MySpacing.width(6),
MyText.bodySmall(
'(${employee.designation})',
fontWeight: 600,
overflow:
TextOverflow.ellipsis,
maxLines: 1,
color: Colors.grey[
700], // optional styling
),
],
),
MySpacing.height(8),
(employee.checkIn != null ||
employee.checkOut != null)
? Row(
children: [
if (employee.checkIn !=
null) ...[
Icon(
Icons
.arrow_circle_right,
size: 16,
color:
Colors.green),
MySpacing.width(4),
Expanded(
child:
MyText.bodySmall(
DateFormat(
'hh:mm a')
.format(employee
.checkIn!),
fontWeight: 600,
overflow:
TextOverflow
.ellipsis,
),
),
MySpacing.width(16),
],
if (employee.checkOut !=
null) ...[
Icon(
Icons
.arrow_circle_left,
size: 16,
color: Colors.red),
MySpacing.width(4),
Expanded(
child:
MyText.bodySmall(
DateFormat(
'hh:mm a')
.format(employee
.checkOut!),
fontWeight: 600,
overflow:
TextOverflow
.ellipsis,
),
),
],
],
)
: SizedBox.shrink(),
MySpacing.height(12),
Row(
mainAxisAlignment:
MainAxisAlignment.end,
children: [
AttendanceActionButton(
employee: employee,
attendanceController:
attendanceController,
),
if (employee.checkIn !=
null) ...[
MySpacing.width(8),
AttendanceLogViewButton(
employee: employee,
attendanceController:
attendanceController,
),
],
],
),
],
), ),
), MySpacing.width(6),
], 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: [
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,
),
], ],
); ),
}), ),
), ),
), if (index != employees.length - 1)
Divider(
color: Colors.grey.withOpacity(0.3),
thickness: 1,
height: 1,
),
],
);
}),
),
),
], ],
); );
}); });
@ -397,6 +386,7 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
final bDate = b.checkIn ?? DateTime(0); final bDate = b.checkIn ?? DateTime(0);
return bDate.compareTo(aDate); return bDate.compareTo(aDate);
}); });
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -437,179 +427,176 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
], ],
), ),
), ),
MyCard.bordered( if (attendanceController.isLoadingAttendanceLogs.value)
borderRadiusAll: 4, const SizedBox(
border: Border.all(color: Colors.grey.withOpacity(0.2)), height: 120,
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom), child: Center(child: CircularProgressIndicator()),
paddingAll: 8, )
child: Column( else if (logs.isEmpty)
crossAxisAlignment: CrossAxisAlignment.start, SizedBox(
children: [ height: 120,
if (attendanceController.isLoadingAttendanceLogs.value) child: Center(
const Padding( child: MyText.bodySmall(
padding: EdgeInsets.symmetric(vertical: 32), "No Attendance Logs Found for this Project",
child: Center(child: CircularProgressIndicator()), fontWeight: 600,
) ),
else if (logs.isEmpty) ),
MyText.bodySmall( )
"No Attendance Logs Found for this Project", else
fontWeight: 600, MyCard.bordered(
) borderRadiusAll: 4,
else border: Border.all(color: Colors.grey.withOpacity(0.2)),
Column( shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
children: List.generate(logs.length, (index) { paddingAll: 8,
final employee = logs[index]; child: Column(
final currentDate = employee.checkIn != null crossAxisAlignment: CrossAxisAlignment.start,
? DateFormat('dd MMM yyyy').format(employee.checkIn!) 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 previousDate =
index > 0 && logs[index - 1].checkIn != null
? DateFormat('dd MMM yyyy')
.format(logs[index - 1].checkIn!)
: '';
final showDateHeader = final showDateHeader =
index == 0 || currentDate != previousDate; index == 0 || currentDate != previousDate;
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (showDateHeader) if (showDateHeader)
Padding( Padding(
padding: const EdgeInsets.symmetric(vertical: 8), padding: const EdgeInsets.symmetric(vertical: 8),
child: MyText.bodyMedium( child: MyText.bodyMedium(
currentDate, currentDate,
fontWeight: 700, 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),
Padding( Expanded(
padding: EdgeInsets.only(bottom: 8), child: Column(
child: MyContainer( crossAxisAlignment: CrossAxisAlignment.start,
paddingAll: 8, children: [
child: Row( Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Avatar(
firstName: employee.firstName,
lastName: employee.lastName,
size: 31,
),
MySpacing.width(16),
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [ children: [
Row( Flexible(
children: [ child: MyText.bodyMedium(
Flexible( employee.name,
child: MyText.bodyMedium( fontWeight: 600,
employee.name, overflow: TextOverflow.ellipsis,
fontWeight: 600, maxLines: 1,
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), MySpacing.width(6),
(employee.checkIn != null || Flexible(
employee.checkOut != null) child: MyText.bodySmall(
? Row( '(${employee.designation})',
children: [ fontWeight: 600,
if (employee.checkIn != overflow: TextOverflow.ellipsis,
null) ...[ maxLines: 1,
Icon( color: Colors.grey[700],
Icons ),
.arrow_circle_right,
size: 16,
color: Colors.green),
MySpacing.width(4),
Expanded(
child: MyText.bodySmall(
DateFormat('hh:mm a')
.format(employee
.checkIn!),
fontWeight: 600,
overflow: TextOverflow
.ellipsis,
),
),
MySpacing.width(16),
],
if (employee.checkOut !=
null) ...[
Icon(
Icons.arrow_circle_left,
size: 16,
color: Colors.red),
MySpacing.width(4),
Expanded(
child: MyText.bodySmall(
DateFormat('hh:mm a')
.format(employee
.checkOut!),
fontWeight: 600,
overflow: TextOverflow
.ellipsis,
),
),
],
],
)
: SizedBox.shrink(),
MySpacing.height(12),
Row(
mainAxisAlignment:
MainAxisAlignment.end,
children: [
Flexible(
child: AttendanceActionButton(
employee: employee,
attendanceController:
attendanceController,
),
),
MySpacing.width(8),
Flexible(
child: AttendanceLogViewButton(
employee: employee,
attendanceController:
attendanceController,
),
),
],
), ),
], ],
), ),
), 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), if (index != logs.length - 1)
thickness: 1, Divider(
height: 1, color: Colors.grey.withOpacity(0.3),
), thickness: 1,
], height: 1,
); ),
}), ],
), );
], }),
),
), ),
),
], ],
); );
}); });
@ -628,151 +615,143 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
), ),
Obx(() { Obx(() {
final employees = attendanceController.regularizationLogs; final employees = attendanceController.regularizationLogs;
if (attendanceController.isLoadingRegularizationLogs.value) {
return SizedBox(
height: 120,
child: const Center(child: CircularProgressIndicator()),
);
}
if (employees.isEmpty) {
return SizedBox(
height: 120,
child: Center(
child: MyText.bodySmall(
"No Regularization Requests Found for this Project",
fontWeight: 600,
),
),
);
}
return MyCard.bordered( return MyCard.bordered(
borderRadiusAll: 4, borderRadiusAll: 4,
border: Border.all(color: Colors.grey.withOpacity(0.2)), border: Border.all(color: Colors.grey.withOpacity(0.2)),
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom), shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
paddingAll: 8, paddingAll: 8,
child: attendanceController.isLoadingRegularizationLogs.value child: Column(
? const Padding( children: List.generate(employees.length, (index) {
padding: EdgeInsets.symmetric(vertical: 32.0), final employee = employees[index];
child: Center(child: CircularProgressIndicator()), return Column(
) children: [
: employees.isEmpty Padding(
? MyText.bodySmall( padding: const EdgeInsets.only(bottom: 8),
"No Regularization Requests Found for this Project", child: MyContainer(
fontWeight: 600, paddingAll: 8,
) child: Row(
: Column( crossAxisAlignment: CrossAxisAlignment.start,
children: List.generate(employees.length, (index) { children: [
final employee = employees[index]; Avatar(
return Column( firstName: employee.firstName,
children: [ lastName: employee.lastName,
Padding( size: 31,
padding: EdgeInsets.only(bottom: 8), ),
child: MyContainer( MySpacing.width(16),
paddingAll: 8, Expanded(
child: Row( child: Column(
crossAxisAlignment: crossAxisAlignment: CrossAxisAlignment.start,
CrossAxisAlignment.start, children: [
Row(
children: [ children: [
Avatar( Flexible(
firstName: employee.firstName, child: MyText.bodyMedium(
lastName: employee.lastName, employee.name,
size: 31, fontWeight: 600,
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
), ),
MySpacing.width(16), MySpacing.width(6),
Expanded( Flexible(
child: Column( child: MyText.bodySmall(
crossAxisAlignment: '(${employee.role})',
CrossAxisAlignment.start, fontWeight: 600,
children: [ overflow: TextOverflow.ellipsis,
Row( maxLines: 1,
children: [ color: Colors.grey[700],
Flexible(
child: MyText.bodyMedium(
employee.name,
fontWeight: 600,
overflow:
TextOverflow.ellipsis,
maxLines: 1,
),
),
MySpacing.width(6),
Flexible(
child: MyText.bodySmall(
'(${employee.role})',
fontWeight: 600,
overflow:
TextOverflow.ellipsis,
maxLines: 1,
color: Colors.grey[700],
),
),
],
),
MySpacing.height(8),
Row(
children: [
if (employee.checkIn !=
null) ...[
Icon(Icons.arrow_circle_right,
size: 16,
color: Colors.green),
MySpacing.width(4),
Expanded(
child: MyText.bodySmall(
DateFormat('hh:mm a')
.format(employee
.checkIn!),
fontWeight: 600,
overflow:
TextOverflow.ellipsis,
),
),
MySpacing.width(16),
],
if (employee.checkOut !=
null) ...[
Icon(Icons.arrow_circle_left,
size: 16,
color: Colors.red),
MySpacing.width(4),
Expanded(
child: MyText.bodySmall(
DateFormat('hh:mm a')
.format(employee
.checkOut!),
fontWeight: 600,
overflow:
TextOverflow.ellipsis,
),
),
],
],
),
MySpacing.height(12),
Row(
mainAxisAlignment:
MainAxisAlignment.end,
children: [
RegularizeActionButton(
attendanceController:
attendanceController,
log: employee,
uniqueLogKey:
employee.employeeId,
action: ButtonActions.approve,
),
const SizedBox(width: 8),
RegularizeActionButton(
attendanceController:
attendanceController,
log: employee,
uniqueLogKey:
employee.employeeId,
action: ButtonActions.reject,
),
],
)
],
), ),
), ),
], ],
), ),
), 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,
),
],
)
],
), ),
if (index != employees.length - 1) ),
Divider( ],
color: Colors.grey.withOpacity(0.3), ),
thickness: 1,
height: 1,
),
],
);
}),
), ),
),
if (index != employees.length - 1)
Divider(
color: Colors.grey.withOpacity(0.3),
thickness: 1,
height: 1,
),
],
);
}),
),
); );
}), }),
], ],

View File

@ -272,7 +272,9 @@ class Layout extends StatelessWidget {
), ),
MyButton( MyButton(
tapTargetSize: MaterialTapTargetSize.shrinkWrap, tapTargetSize: MaterialTapTargetSize.shrinkWrap,
onPressed: () => {Get.offNamed('/auth/login')}, onPressed: () async {
await LocalStorage.logout();
},
borderRadiusAll: AppStyle.buttonRadius.medium, borderRadiusAll: AppStyle.buttonRadius.medium,
padding: MySpacing.xy(8, 4), padding: MySpacing.xy(8, 4),
splashColor: contentTheme.onBackground.withAlpha(20), splashColor: contentTheme.onBackground.withAlpha(20),

View File

@ -172,13 +172,16 @@ class _LeftBarState extends State<LeftBar>
), ),
), ),
MyContainer( MyContainer(
onTap: () { onTap: () async {
Get.offNamed('/auth/login'); await LocalStorage.logout();
}, },
color: leftBarTheme.activeItemBackground, color: leftBarTheme.activeItemBackground,
paddingAll: 8, paddingAll: 8,
child: Icon(LucideIcons.log_out, child: Icon(
size: 16, color: leftBarTheme.activeItemColor), LucideIcons.log_out,
size: 16,
color: leftBarTheme.activeItemColor,
),
) )
], ],
), ),

View File

@ -468,11 +468,16 @@ class _DailyProgressReportScreenState extends State<DailyProgressReportScreen>
top: Radius.circular( top: Radius.circular(
16)), 16)),
), ),
builder: (_) => Padding( builder: (BuildContext ctx) =>
padding: MediaQuery.of(context) Padding(
padding: MediaQuery.of(ctx)
.viewInsets, .viewInsets,
child: ReportTaskBottomSheet( child: ReportTaskBottomSheet(
taskData: taskData), taskData: taskData,
onReportSuccess: () {
_refreshData();
},
),
), ),
); );
}, },