-- Enhance layout with floating action button and navigation improvements

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 327 KiB

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 43 KiB

View File

@ -3,8 +3,8 @@ import 'package:flutter/material.dart';
import 'package:marco/controller/my_controller.dart';
import 'package:marco/helpers/widgets/my_form_validator.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:get/get.dart';
import 'package:logger/logger.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
enum Gender {
male,
@ -77,16 +77,16 @@ class AddEmployeeController extends MyController {
update();
}
Future<void> createEmployees() async {
Future<bool> createEmployees() async {
logger.i("Starting employee creation...");
if (selectedGender == null || selectedRoleId == null) {
logger.w("Missing gender or role.");
Get.snackbar(
"Missing Fields",
"Please select both Gender and Role.",
snackPosition: SnackPosition.BOTTOM,
showAppSnackbar(
title: "Missing Fields",
message: "Please select both Gender and Role.",
type: SnackbarType.warning,
);
return;
return false;
}
final firstName = basicValidator.getController("first_name")?.text.trim();
@ -107,13 +107,20 @@ class AddEmployeeController extends MyController {
if (response == true) {
logger.i("Employee created successfully.");
Get.back(); // Or navigate as needed
Get.snackbar("Success", "Employee created successfully!",
snackPosition: SnackPosition.BOTTOM);
showAppSnackbar(
title: "Success",
message: "Employee created successfully!",
type: SnackbarType.success,
);
return true;
} else {
logger.e("Failed to create employee.");
Get.snackbar("Error", "Failed to create employee.",
snackPosition: SnackPosition.BOTTOM);
showAppSnackbar(
title: "Error",
message: "Failed to create employee.",
type: SnackbarType.error,
);
return false;
}
}
}

View File

@ -4,7 +4,7 @@ import 'package:image_picker/image_picker.dart';
import 'package:geolocator/geolocator.dart';
import 'package:intl/intl.dart';
import 'package:logger/logger.dart';
import 'dart:io';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/model/attendance_model.dart';
import 'package:marco/model/project_model.dart';
@ -12,6 +12,7 @@ import 'package:marco/model/employee_model.dart';
import 'package:marco/model/attendance_log_model.dart';
import 'package:marco/model/regularization_log_model.dart';
import 'package:marco/model/attendance_log_view_model.dart';
import 'package:marco/helpers/widgets/my_image_compressor.dart';
final Logger log = Logger();
@ -28,9 +29,7 @@ class AttendanceController extends GetxController {
List<RegularizationLogModel> regularizationLogs = [];
List<AttendanceLogViewModel> attendenceLogsView = [];
RxBool isLoading = true.obs;
// New separate loading states per feature
RxBool isLoading = true.obs; // initially true
RxBool isLoadingProjects = true.obs;
RxBool isLoadingEmployees = true.obs;
RxBool isLoadingAttendanceLogs = true.obs;
@ -47,7 +46,7 @@ class AttendanceController extends GetxController {
void _initializeDefaults() {
_setDefaultDateRange();
fetchProjects();
fetchProjects(); // fetchProjects will set isLoading to false after loading
}
void _setDefaultDateRange() {
@ -58,9 +57,7 @@ class AttendanceController extends GetxController {
}
Future<bool> _handleLocationPermission() async {
LocationPermission permission;
permission = await Geolocator.checkPermission();
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
@ -68,34 +65,33 @@ class AttendanceController extends GetxController {
return false;
}
}
if (permission == LocationPermission.deniedForever) {
log.e('Location permissions are permanently denied');
return false;
}
return true;
}
Future<void> fetchProjects() async {
// Both old and new loading state set for safety
isLoading.value = true;
isLoadingProjects.value = true;
isLoading.value = true;
final response = await ApiService.getProjects();
isLoadingProjects.value = false;
isLoading.value = false;
if (response != null && response.isNotEmpty) {
projects = response.map((json) => ProjectModel.fromJson(json)).toList();
selectedProjectId = projects.first.id.toString();
log.i("Projects fetched: ${projects.length} projects loaded.");
await fetchProjectData(selectedProjectId);
update(['attendance_dashboard_controller']);
} else {
log.w("No project data found or API call failed.");
}
isLoadingProjects.value = false;
isLoading.value = false;
update(['attendance_dashboard_controller']);
}
Future<void> fetchProjectData(String? projectId) async {
@ -117,28 +113,23 @@ class AttendanceController extends GetxController {
Future<void> fetchEmployeesByProject(String? projectId) async {
if (projectId == null) return;
isLoading.value = true;
isLoadingEmployees.value = true;
final response = await ApiService.getEmployeesByProject(projectId);
isLoadingEmployees.value = false;
isLoading.value = false;
if (response != null) {
employees = response.map((json) => EmployeeModel.fromJson(json)).toList();
// Initialize per-employee uploading state
for (var emp in employees) {
uploadingStates[emp.id] = false.obs;
}
log.i(
"Employees fetched: ${employees.length} employees for project $projectId");
update();
} else {
log.e("Failed to fetch employees for project $projectId");
}
isLoadingEmployees.value = false;
}
Future<bool> captureAndUploadAttendance(
@ -164,6 +155,16 @@ class AttendanceController extends GetxController {
uploadingStates[employeeId]?.value = false;
return false;
}
final compressedBytes =
await compressImageToUnder100KB(File(image.path));
if (compressedBytes == null) {
log.e("Image compression failed.");
uploadingStates[employeeId]?.value = false;
return false;
}
final compressedFile = await saveCompressedImageToFile(compressedBytes);
image = XFile(compressedFile.path);
}
final hasLocationPermission = await _handleLocationPermission();
@ -228,8 +229,33 @@ class AttendanceController extends GetxController {
}
return true;
},
builder: (BuildContext context, Widget? child) {
return Center(
child: SizedBox(
width: 400,
height: 500,
child: Theme(
data: Theme.of(context).copyWith(
colorScheme: ColorScheme.light(
primary: const Color.fromARGB(255, 95, 132, 255),
onPrimary: Colors.white,
onSurface: Colors.teal.shade800,
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: Colors.teal,
),
),
dialogTheme: DialogTheme(
backgroundColor: Colors.white,
),
),
child: child!,
),
),
);
},
);
if (picked != null) {
startDateAttendance = picked.start;
endDateAttendance = picked.end;
@ -251,8 +277,8 @@ class AttendanceController extends GetxController {
}) async {
if (projectId == null) return;
isLoading.value = true;
isLoadingAttendanceLogs.value = true;
isLoading.value = true;
final response = await ApiService.getAttendanceLogs(
projectId,
@ -260,9 +286,6 @@ class AttendanceController extends GetxController {
dateTo: dateTo,
);
isLoadingAttendanceLogs.value = false;
isLoading.value = false;
if (response != null) {
attendanceLogs =
response.map((json) => AttendanceLogModel.fromJson(json)).toList();
@ -271,6 +294,8 @@ class AttendanceController extends GetxController {
} else {
log.e("Failed to fetch attendance logs for project $projectId");
}
isLoadingAttendanceLogs.value = false;
isLoading.value = false;
}
Map<String, List<AttendanceLogModel>> groupLogsByCheckInDate() {
@ -284,8 +309,6 @@ class AttendanceController extends GetxController {
groupedLogs.putIfAbsent(checkInDate, () => []);
groupedLogs[checkInDate]!.add(logItem);
}
// Sort by date descending
final sortedEntries = groupedLogs.entries.toList()
..sort((a, b) {
if (a.key == 'Unknown') return 1;
@ -309,14 +332,11 @@ class AttendanceController extends GetxController {
}) async {
if (projectId == null) return;
isLoading.value = true;
isLoadingRegularizationLogs.value = true;
isLoading.value = true;
final response = await ApiService.getRegularizationLogs(projectId);
isLoadingRegularizationLogs.value = false;
isLoading.value = false;
if (response != null) {
regularizationLogs = response
.map((json) => RegularizationLogModel.fromJson(json))
@ -326,25 +346,23 @@ class AttendanceController extends GetxController {
} else {
log.e("Failed to fetch regularization logs for project $projectId");
}
isLoadingRegularizationLogs.value = false;
isLoading.value = false;
}
Future<void> fetchLogsView(String? id) async {
if (id == null) return;
isLoading.value = true;
isLoadingLogView.value = true;
isLoading.value = true;
final response = await ApiService.getAttendanceLogView(id);
isLoadingLogView.value = false;
isLoading.value = false;
if (response != null) {
attendenceLogsView = response
.map((json) => AttendanceLogViewModel.fromJson(json))
.toList();
// Sort by activityTime field (latest first)
attendenceLogsView.sort((a, b) {
if (a.activityTime == null || b.activityTime == null) return 0;
return b.activityTime!.compareTo(a.activityTime!);
@ -355,5 +373,8 @@ class AttendanceController extends GetxController {
} else {
log.e("Failed to fetch attendance log view for ID $id");
}
isLoadingLogView.value = false;
isLoading.value = false;
}
}

View File

@ -15,9 +15,18 @@ class DailyTaskController extends GetxController {
DateTime? endDateTask;
List<TaskModel> dailyTasks = [];
final RxSet<String> expandedDates = <String>{}.obs;
void toggleDate(String dateKey) {
if (expandedDates.contains(dateKey)) {
expandedDates.remove(dateKey);
} else {
expandedDates.add(dateKey);
}
}
RxBool isLoading = false.obs;
Map<String, List<TaskModel>> groupedDailyTasks = {};
@override
void onInit() {
super.onInit();
@ -66,22 +75,22 @@ Future<void> fetchTaskData(String? projectId) async {
isLoading.value = false;
if (response != null) {
Map<String, List<TaskModel>> groupedTasks = {};
groupedDailyTasks.clear();
for (var taskJson in response) {
TaskModel task = TaskModel.fromJson(taskJson);
String assignmentDateKey = task.assignmentDate;
String assignmentDateKey =
task.assignmentDate.toIso8601String().split('T')[0];
if (groupedTasks.containsKey(assignmentDateKey)) {
groupedTasks[assignmentDateKey]?.add(task);
if (groupedDailyTasks.containsKey(assignmentDateKey)) {
groupedDailyTasks[assignmentDateKey]?.add(task);
} else {
groupedTasks[assignmentDateKey] = [task];
groupedDailyTasks[assignmentDateKey] = [task];
}
}
dailyTasks = groupedTasks.entries
.map((entry) => entry.value)
.expand((taskList) => taskList)
.toList();
// Flatten the grouped tasks into the existing dailyTasks list
dailyTasks = groupedDailyTasks.values.expand((list) => list).toList();
log.i("Daily tasks fetched and grouped: ${dailyTasks.length}");
@ -91,7 +100,6 @@ Future<void> fetchTaskData(String? projectId) async {
}
}
Future<void> selectDateRangeForTaskData(
BuildContext context,
DailyTaskController controller,
@ -101,7 +109,8 @@ Future<void> fetchTaskData(String? projectId) async {
firstDate: DateTime(2022),
lastDate: DateTime.now(),
initialDateRange: DateTimeRange(
start: startDateTask ?? DateTime.now().subtract(const Duration(days: 7)),
start:
startDateTask ?? DateTime.now().subtract(const Duration(days: 7)),
end: endDateTask ?? DateTime.now(),
),
);

View File

@ -4,6 +4,7 @@ import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/model/attendance_model.dart';
import 'package:marco/model/project_model.dart';
import 'package:marco/model/employee_model.dart';
import 'package:marco/model/employees/employee_details_model.dart';
final Logger log = Logger();
@ -12,14 +13,18 @@ class EmployeesScreenController extends GetxController {
List<ProjectModel> projects = [];
String? selectedProjectId;
List<EmployeeModel> employees = [];
List<EmployeeDetailsModel> employeeDetails = [];
RxBool isLoading = false.obs;
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
Rxn<EmployeeDetailsModel> selectedEmployeeDetails =
Rxn<EmployeeDetailsModel>();
RxBool isLoadingEmployeeDetails = false.obs;
@override
void onInit() {
super.onInit();
fetchAllProjects();
fetchAllEmployees();
}
Future<void> fetchAllProjects() async {
@ -99,4 +104,47 @@ class EmployeesScreenController extends GetxController {
}
}
}
Future<void> fetchEmployeeDetails(String? employeeId) async {
if (employeeId == null || employeeId.isEmpty) return;
isLoadingEmployeeDetails.value = true;
await _handleSingleApiCall(
() => ApiService.getEmployeeDetails(employeeId),
onSuccess: (data) {
selectedEmployeeDetails.value = EmployeeDetailsModel.fromJson(data);
},
onEmpty: () {
selectedEmployeeDetails.value = null;
},
onError: (e) {
selectedEmployeeDetails.value = null;
},
);
isLoadingEmployeeDetails.value = false;
}
Future<void> _handleSingleApiCall(
Future<Map<String, dynamic>?> Function() apiCall, {
required Function(Map<String, dynamic>) onSuccess,
required Function() onEmpty,
Function(dynamic error)? onError,
}) async {
try {
final response = await apiCall();
if (response != null && response.isNotEmpty) {
onSuccess(response);
} else {
onEmpty();
}
} catch (e) {
if (onError != null) {
onError(e);
} else {
log.e("API call error: $e");
}
}
}
}

View File

@ -0,0 +1,190 @@
import 'package:get/get.dart';
import 'package:logger/logger.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/model/project_model.dart';
import 'package:marco/model/dailyTaskPlaning/daily_task_planing_model.dart';
import 'package:marco/model/employee_model.dart';
import 'package:marco/helpers/widgets/my_form_validator.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
final Logger log = Logger();
class DailyTaskPlaningController extends GetxController {
List<ProjectModel> projects = [];
String? selectedProjectId;
List<EmployeeModel> employees = [];
List<TaskPlanningDetailsModel> dailyTasks = [];
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
MyFormValidator basicValidator = MyFormValidator();
List<Map<String, dynamic>> roles = [];
RxnString selectedRoleId = RxnString();
RxList<EmployeeModel> selectedEmployees = <EmployeeModel>[].obs;
void updateSelectedEmployees() {
final selected =
employees.where((e) => uploadingStates[e.id]?.value == true).toList();
selectedEmployees.value = selected;
}
RxBool isLoading = false.obs;
@override
void onInit() {
super.onInit();
fetchRoles();
_initializeDefaults();
}
void _initializeDefaults() {
fetchProjects();
}
String? formFieldValidator(String? value, {required String fieldType}) {
if (value == null || value.trim().isEmpty) {
return 'This field is required';
}
if (fieldType == "target") {
if (int.tryParse(value.trim()) == null) {
return 'Please enter a valid number';
}
}
if (fieldType == "description") {
if (value.trim().length < 5) {
return 'Description must be at least 5 characters';
}
}
return null;
}
Future<void> fetchRoles() async {
logger.i("Fetching roles...");
final result = await ApiService.getRoles();
if (result != null) {
roles = List<Map<String, dynamic>>.from(result);
logger.i("Roles fetched successfully.");
update();
} else {
logger.e("Failed to fetch roles.");
}
}
void onRoleSelected(String? roleId) {
selectedRoleId.value = roleId;
logger.i("Role selected: $roleId");
}
Future<bool> assignDailyTask({
required String workItemId,
required int plannedTask,
required String description,
required List<String> taskTeam,
DateTime? assignmentDate,
}) async {
logger.i("Starting assign task...");
final response = await ApiService.assignDailyTask(
workItemId: workItemId,
plannedTask: plannedTask,
description: description,
taskTeam: taskTeam,
assignmentDate: assignmentDate,
);
if (response == true) {
logger.i("Task assigned successfully.");
showAppSnackbar(
title: "Success",
message: "Task assigned successfully!",
type: SnackbarType.success,
);
return true;
} else {
logger.e("Failed to assign task.");
showAppSnackbar(
title: "Error",
message: "Failed to assign task.",
type: SnackbarType.error,
);
return false;
}
}
Future<void> fetchProjects() async {
try {
isLoading.value = true;
final response = await ApiService.getProjects();
if (response?.isEmpty ?? true) {
log.w("No project data found or API call failed.");
return;
}
projects = response!.map((json) => ProjectModel.fromJson(json)).toList();
selectedProjectId = projects.first.id.toString();
log.i("Projects fetched: ${projects.length} projects loaded.");
update();
await fetchTaskData(selectedProjectId);
} catch (e, stack) {
log.e("Error fetching projects", error: e, stackTrace: stack);
} finally {
isLoading.value = false;
}
}
Future<void> fetchTaskData(String? projectId) async {
if (projectId == null) return;
try {
isLoading.value = true;
final response = await ApiService.getDailyTasksDetails(projectId);
if (response != null) {
final data = response['data'];
if (data != null) {
dailyTasks = [TaskPlanningDetailsModel.fromJson(data)];
log.i("Daily task Planning Details fetched.");
} else {
log.e("Data field is null");
}
} else {
log.e(
"Failed to fetch daily task planning Details for project $projectId");
}
} catch (e, stack) {
log.e("Error fetching daily task data", error: e, stackTrace: stack);
} finally {
isLoading.value = false;
update();
}
}
Future<void> fetchEmployeesByProject(String? projectId) async {
if (projectId == null || projectId.isEmpty) {
log.e("Project ID is required but was null or empty.");
return;
}
isLoading.value = true;
try {
final response = await ApiService.getAllEmployeesByProject(projectId);
if (response != null && response.isNotEmpty) {
employees =
response.map((json) => EmployeeModel.fromJson(json)).toList();
for (var emp in employees) {
uploadingStates[emp.id] = false.obs;
}
log.i("Employees fetched: ${employees.length} for project $projectId");
} else {
log.w("No employees found for project $projectId.");
employees = [];
}
} catch (e) {
log.e("Error fetching employees for project $projectId: $e");
}
update();
isLoading.value = false;
}
}

View File

@ -5,9 +5,13 @@ import 'package:marco/helpers/widgets/my_form_validator.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:get/get.dart';
import 'package:logger/logger.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/controller/task_planing/daily_task_planing_controller.dart';
final Logger logger = Logger();
enum ApiStatus { idle, loading, success, failure }
final DailyTaskPlaningController taskController = Get.put(DailyTaskPlaningController());
class ReportTaskController extends MyController {
List<PlatformFile> files = [];
MyFormValidator basicValidator = MyFormValidator();
@ -96,18 +100,30 @@ class ReportTaskController extends MyController {
basicValidator.getController('completed_work')?.text.trim();
if (completedWork == null || completedWork.isEmpty) {
Get.snackbar("Error", "Completed work is required.");
showAppSnackbar(
title: "Error",
message: "Completed work is required.",
type: SnackbarType.error,
);
return;
}
final completedWorkInt = int.tryParse(completedWork);
if (completedWorkInt == null || completedWorkInt <= 0) {
Get.snackbar("Error", "Completed work must be a positive integer.");
showAppSnackbar(
title: "Error",
message: "Completed work must be a positive integer.",
type: SnackbarType.error,
);
return;
}
final commentField = basicValidator.getController('comment')?.text.trim();
if (commentField == null || commentField.isEmpty) {
Get.snackbar("Error", "Comment is required.");
showAppSnackbar(
title: "Error",
message: "Comment is required.",
type: SnackbarType.error,
);
return;
}
@ -122,13 +138,25 @@ class ReportTaskController extends MyController {
);
if (success) {
Get.snackbar("Success", "Task reported successfully!");
showAppSnackbar(
title: "Success",
message: "Task reported successfully!",
type: SnackbarType.success,
);
} else {
Get.snackbar("Error", "Failed to report task.");
showAppSnackbar(
title: "Error",
message: "Failed to report task.",
type: SnackbarType.error,
);
}
} catch (e) {
logger.e("Error reporting task: $e");
Get.snackbar("Error", "An error occurred while reporting the task.");
showAppSnackbar(
title: "Error",
message: "An error occurred while reporting the task.",
type: SnackbarType.error,
);
} finally {
isLoading.value = false;
}
@ -137,23 +165,17 @@ class ReportTaskController extends MyController {
Future<void> commentTask({
required String projectId,
required String comment,
required int completedTask,
required List<Map<String, dynamic>> checklist,
required DateTime reportedDate,
}) async {
logger.i("Starting task report...");
logger.i("Starting task comment...");
final completedWork =
basicValidator.getController('completed_work')?.text.trim();
final commentField = basicValidator.getController('comment')?.text.trim();
if (completedWork == null || completedWork.isEmpty) {
Get.snackbar("Error", "Completed work is required.");
return;
}
if (commentField == null || commentField.isEmpty) {
Get.snackbar("Error", "Comment is required.");
showAppSnackbar(
title: "Error",
message: "Comment is required.",
type: SnackbarType.error,
);
return;
}
@ -166,13 +188,27 @@ class ReportTaskController extends MyController {
);
if (success) {
Get.snackbar("Success", "Task commented successfully!");
showAppSnackbar(
title: "Success",
message: "Task commented successfully!",
type: SnackbarType.success,
);
await taskController.fetchTaskData(projectId);
} else {
Get.snackbar("Error", "Failed to comment task.");
showAppSnackbar(
title: "Error",
message: "Failed to comment task.",
type: SnackbarType.error,
);
}
} catch (e) {
logger.e("Error commenting task: $e");
Get.snackbar("Error", "An error occurred while commenting the task.");
showAppSnackbar(
title: "Error",
message: "An error occurred while commenting the task.",
type: SnackbarType.error,
);
} finally {
isLoading.value = false;
}

View File

@ -14,9 +14,12 @@ class ApiEndpoints {
static const String getAllEmployees = "/employee/list";
static const String getRoles = "/roles/jobrole";
static const String createEmployee = "/employee/manage";
static const String getEmployeeInfo = "/employee/profile/get";
// Daily Task Screen API Endpoints
static const String getDailyTask = "/task/list";
static const String reportTask = "/task/report";
static const String commentTask = "task/comment";
static const String commentTask = "/task/comment";
static const String dailyTaskDetails = "/project/details";
static const String assignDailyTask = "/task/assign";
}

View File

@ -7,6 +7,7 @@ import 'package:marco/helpers/services/storage/local_storage.dart';
import 'package:marco/helpers/services/auth_service.dart';
import 'package:marco/helpers/services/api_endpoints.dart';
import 'package:get/get.dart';
final Logger logger = Logger();
class ApiService {
@ -46,6 +47,21 @@ class ApiService {
return null;
}
static dynamic _parseResponseForAllData(http.Response response,
{String label = ''}) {
_log("$label Response: ${response.body}");
try {
final json = jsonDecode(response.body);
if (response.statusCode == 200 && json['success'] == true) {
return json; // 👈 Return full response, not just json['data']
}
_log("API Error [$label]: ${json['message'] ?? 'Unknown error'}");
} catch (e) {
_log("Response parsing error [$label]: $e");
}
return null;
}
static Future<http.Response?> _getRequest(String endpoint,
{Map<String, String>? queryParams, bool hasRetried = false}) async {
String? token = await _getToken();
@ -291,6 +307,20 @@ class ApiService {
}
}
static Future<Map<String, dynamic>?> getEmployeeDetails(
String employeeId) async {
final url = "${ApiEndpoints.getEmployeeInfo}/$employeeId";
final response = await _getRequest(url);
final data = response != null
? _parseResponse(response, label: 'Employee Details')
: null;
if (data is Map<String, dynamic>) {
return data;
}
return null;
}
// ===== Daily Tasks API Calls =====
static Future<List<dynamic>?> getDailyTasks(String projectId,
{DateTime? dateFrom, DateTime? dateTo}) async {
@ -339,6 +369,7 @@ class ApiService {
return false;
}
}
static Future<bool> commentTask({
required String id,
required String comment,
@ -359,11 +390,58 @@ class ApiService {
final json = jsonDecode(response.body);
if (response.statusCode == 200 && json['success'] == true) {
Get.back();
return true;
} else {
_log("Failed to comment task: ${json['message'] ?? 'Unknown error'}");
return false;
}
}
// Daily Task Planing //
static Future<Map<String, dynamic>?> getDailyTasksDetails(
String projectId) async {
final url = "${ApiEndpoints.dailyTaskDetails}/$projectId";
final response = await _getRequest(url);
return response != null
? _parseResponseForAllData(response, label: 'Daily Task Details')
as Map<String, dynamic>?
: null;
}
static Future<bool> assignDailyTask({
required String workItemId,
required int plannedTask,
required String description,
required List<String> taskTeam,
DateTime? assignmentDate,
}) async {
final body = {
"workItemId": workItemId,
"plannedTask": plannedTask,
"description": description,
"taskTeam": taskTeam,
"assignmentDate":
(assignmentDate ?? DateTime.now()).toUtc().toIso8601String(),
};
final response = await _postRequest(ApiEndpoints.assignDailyTask, body);
if (response == null) {
_log("Error: No response from server.");
return false;
}
final json = jsonDecode(response.body);
if (response.statusCode == 200 && json['success'] == true) {
Get.back();
return true;
} else {
_log(
"Failed to assign daily task: ${json['message'] ?? 'Unknown error'}");
return false;
}
}
}

View File

@ -3,8 +3,7 @@ import 'package:http/http.dart' as http;
import 'package:get/get.dart';
import 'package:marco/helpers/services/storage/local_storage.dart';
import 'package:marco/controller/permission_controller.dart';
import 'package:logger/logger.dart'; // <-- Make sure this import is present
import 'package:logger/logger.dart';
final Logger logger = Logger();
class AuthService {
@ -13,8 +12,6 @@ class AuthService {
'Content-Type': 'application/json',
};
static bool isLoggedIn = false;
/// Logs in the user and stores tokens if successful.
static Future<Map<String, String>?> loginUser(
Map<String, dynamic> data) async {
try {
@ -44,7 +41,7 @@ class AuthService {
Get.put(PermissionController());
return null; // Success
return null;
} else if (response.statusCode == 401) {
return {"password": "Invalid email or password"};
} else {

View File

@ -0,0 +1,47 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as path;
import 'package:logger/logger.dart';
final logger = Logger();
Future<Uint8List?> compressImageToUnder100KB(File file) async {
int quality = 40;
Uint8List? result;
const int maxWidth = 800;
const int maxHeight = 800;
while (quality >= 10) {
result = await FlutterImageCompress.compressWithFile(
file.absolute.path,
quality: quality,
minWidth: maxWidth,
minHeight: maxHeight,
format: CompressFormat.jpeg,
);
if (result != null) {
logger.i('Quality: $quality, Size: ${(result.lengthInBytes / 1024).toStringAsFixed(2)} KB');
if (result.lengthInBytes <= 100 * 1024) {
return result;
}
}
quality -= 10;
}
return result;
}
Future<File> saveCompressedImageToFile(Uint8List bytes) async {
final tempDir = await getTemporaryDirectory();
final filePath = path.join(
tempDir.path,
'compressed_${DateTime.now().millisecondsSinceEpoch}.jpg',
);
final file = File(filePath);
return await file.writeAsBytes(bytes);
}

View File

@ -0,0 +1,47 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
enum SnackbarType { success, error, warning, info }
void showAppSnackbar({
required String title,
required String message,
SnackbarType type = SnackbarType.info,
}) {
Color backgroundColor;
IconData iconData;
switch (type) {
case SnackbarType.success:
backgroundColor = Colors.green;
iconData = Icons.check_circle;
break;
case SnackbarType.error:
backgroundColor = Colors.red;
iconData = Icons.error;
break;
case SnackbarType.warning:
backgroundColor = Colors.orange;
iconData = Icons.warning;
break;
case SnackbarType.info:
backgroundColor = Colors.blue;
iconData = Icons.info;
break;
}
Get.snackbar(
title,
message,
backgroundColor: backgroundColor,
colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM,
margin: const EdgeInsets.all(16),
borderRadius: 8,
duration: const Duration(seconds: 3),
icon: Icon(
iconData,
color: Colors.white,
),
);
}

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/controller/dashboard/attendance_screen_controller.dart';
import 'package:marco/helpers/utils/attendance_actions.dart';
@ -19,6 +19,98 @@ class AttendanceActionButton extends StatefulWidget {
State<AttendanceActionButton> createState() => _AttendanceActionButtonState();
}
Future<String?> _showCommentBottomSheet(BuildContext context, String actionText) async {
final TextEditingController commentController = TextEditingController();
String? errorText;
return showModalBottomSheet<String>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.white,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (context) {
return StatefulBuilder(
builder: (context, setModalState) {
return Padding(
padding: EdgeInsets.only(
left: 16,
right: 16,
top: 24,
bottom: MediaQuery.of(context).viewInsets.bottom + 24,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Add Comment for ${capitalizeFirstLetter(actionText)}',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
const SizedBox(height: 16),
TextField(
controller: commentController,
maxLines: 4,
decoration: InputDecoration(
hintText: 'Type your comment here...',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
filled: true,
fillColor: Colors.grey.shade100,
errorText: errorText,
),
onChanged: (_) {
if (errorText != null) {
setModalState(() => errorText = null);
}
},
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel'),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton(
onPressed: () {
final comment = commentController.text.trim();
if (comment.isEmpty) {
setModalState(() {
errorText = 'Comment cannot be empty.';
});
return;
}
Navigator.of(context).pop(comment);
},
child: const Text('Submit'),
),
),
],
),
],
),
);
},
);
},
);
}
String capitalizeFirstLetter(String text) {
if (text.isEmpty) return text;
return text[0].toUpperCase() + text.substring(1);
}
class _AttendanceActionButtonState extends State<AttendanceActionButton> {
late final String uniqueLogKey;
@ -28,7 +120,6 @@ class _AttendanceActionButtonState extends State<AttendanceActionButton> {
uniqueLogKey = AttendanceButtonHelper.getUniqueKey(
widget.employee.employeeId, widget.employee.id);
// Defer the Rx initialization after first frame to avoid setState during build
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!widget.attendanceController.uploadingStates
.containsKey(uniqueLogKey)) {
@ -58,9 +149,10 @@ class _AttendanceActionButtonState extends State<AttendanceActionButton> {
if (selectedDateTime.isAfter(checkInTime)) {
return selectedDateTime;
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("Please select a time after check-in time.")),
showAppSnackbar(
title: "Invalid Time",
message: "Please select a time after check-in time.",
type: SnackbarType.warning,
);
return null;
}
@ -69,14 +161,13 @@ class _AttendanceActionButtonState extends State<AttendanceActionButton> {
}
void _handleButtonPressed(BuildContext context) async {
// Set uploading state true safely
widget.attendanceController.uploadingStates[uniqueLogKey]?.value = true;
if (widget.attendanceController.selectedProjectId == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("Please select a project first"),
),
showAppSnackbar(
title: "Project Required",
message: "Please select a project first",
type: SnackbarType.error,
);
widget.attendanceController.uploadingStates[uniqueLogKey]?.value = false;
return;
@ -122,6 +213,12 @@ class _AttendanceActionButtonState extends State<AttendanceActionButton> {
break;
}
final userComment = await _showCommentBottomSheet(context, actionText);
if (userComment == null || userComment.isEmpty) {
widget.attendanceController.uploadingStates[uniqueLogKey]?.value = false;
return;
}
bool success = false;
if (actionText == ButtonActions.requestRegularize) {
final selectedTime = await showTimePickerForRegularization(
@ -135,37 +232,31 @@ class _AttendanceActionButtonState extends State<AttendanceActionButton> {
widget.employee.id,
widget.employee.employeeId,
widget.attendanceController.selectedProjectId!,
comment: actionText,
comment: userComment,
action: updatedAction,
imageCapture: imageCapture,
markTime: formattedSelectedTime,
);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(success
? '${actionText.toLowerCase()} marked successfully!'
: 'Failed to ${actionText.toLowerCase()}'),
),
);
}
} else {
success = await widget.attendanceController.captureAndUploadAttendance(
widget.employee.id,
widget.employee.employeeId,
widget.attendanceController.selectedProjectId!,
comment: actionText,
comment: userComment,
action: updatedAction,
imageCapture: imageCapture,
);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(success
? '${actionText.toLowerCase()} marked successfully!'
: 'Failed to ${actionText.toLowerCase()}'),
),
);
}
showAppSnackbar(
title: success ? '${capitalizeFirstLetter(actionText)} Success' : 'Error',
message: success
? '${capitalizeFirstLetter(actionText)} marked successfully!'
: 'Failed to ${actionText.toLowerCase()}',
type: success ? SnackbarType.success : SnackbarType.error,
);
widget.attendanceController.uploadingStates[uniqueLogKey]?.value = false;
if (success) {

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:marco/controller/permission_controller.dart';
import 'package:marco/controller/dashboard/attendance_screen_controller.dart';
import 'package:intl/intl.dart';
import 'package:marco/helpers/widgets/my_text.dart';
class AttendanceFilterBottomSheet extends StatelessWidget {
final AttendanceController controller;
@ -23,7 +24,7 @@ class AttendanceFilterBottomSheet extends StatelessWidget {
final end = DateFormat('dd MM yyyy').format(endDate);
return "$start - $end";
}
return "Select Date Range";
return "Date Range";
}
@override
@ -72,16 +73,17 @@ class AttendanceFilterBottomSheet extends StatelessWidget {
)
: null;
final selectedProjectName =
selectedProject?.name ?? "Select Project";
final selectedProjectName = selectedProject?.name ?? "Select Project";
filterWidgets = [
const Padding(
padding: EdgeInsets.fromLTRB(16, 12, 16, 4),
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
child: Align(
alignment: Alignment.centerLeft,
child: Text('Select Project',
style: TextStyle(fontWeight: FontWeight.bold)),
child: MyText.titleSmall(
"Project",
fontWeight: 600,
),
),
),
ListTile(
@ -92,18 +94,23 @@ class AttendanceFilterBottomSheet extends StatelessWidget {
onTap: () => setState(() => showProjectList = true),
),
const Divider(),
const Padding(
padding: EdgeInsets.fromLTRB(16, 12, 16, 4),
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
child: Align(
alignment: Alignment.centerLeft,
child: Text('Select View',
style: TextStyle(fontWeight: FontWeight.bold)),
child: MyText.titleSmall(
"View",
fontWeight: 600,
),
),
),
...[
{'label': 'Today\'s Attendance', 'value': 'todaysAttendance'},
{'label': 'Attendance Logs', 'value': 'attendanceLogs'},
{'label': 'Regularization Requests', 'value': 'regularizationRequests'},
{
'label': 'Regularization Requests',
'value': 'regularizationRequests'
},
].map((item) {
return RadioListTile<String>(
dense: true,
@ -119,13 +126,13 @@ class AttendanceFilterBottomSheet extends StatelessWidget {
if (tempSelectedTab == 'attendanceLogs') {
filterWidgets.addAll([
const Divider(),
const Padding(
padding: EdgeInsets.fromLTRB(16, 12, 16, 4),
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
child: Align(
alignment: Alignment.centerLeft,
child: Text(
"Select Date Range",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
child: MyText.titleSmall(
"Date Range",
fontWeight: 600,
),
),
),
@ -139,7 +146,7 @@ class AttendanceFilterBottomSheet extends StatelessWidget {
),
child: Ink(
decoration: BoxDecoration(
color: Colors.grey.shade100,
color: const Color.fromARGB(255, 255, 255, 255),
border: Border.all(color: Colors.grey.shade400),
borderRadius: BorderRadius.circular(10),
),
@ -147,7 +154,8 @@ class AttendanceFilterBottomSheet extends StatelessWidget {
const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
child: Row(
children: [
Icon(Icons.date_range, color: Colors.blue.shade600),
Icon(Icons.date_range,
color: const Color.fromARGB(255, 9, 9, 9)),
const SizedBox(width: 12),
Expanded(
child: Text(
@ -160,7 +168,8 @@ class AttendanceFilterBottomSheet extends StatelessWidget {
overflow: TextOverflow.ellipsis,
),
),
const Icon(Icons.arrow_drop_down, color: Colors.grey),
const Icon(Icons.arrow_drop_down,
color: Color.fromARGB(255, 0, 0, 0)),
],
),
),
@ -178,14 +187,29 @@ class AttendanceFilterBottomSheet extends StatelessWidget {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.only(top: 12, bottom: 8),
child: Center(
child: Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: Colors.grey[400],
borderRadius: BorderRadius.circular(4),
),
),
),
),
...filterWidgets,
const Divider(),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Color.fromARGB(255, 95, 132, 255),
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),

View File

@ -1,11 +1,12 @@
import 'package:flutter/material.dart';
import 'package:marco/helpers/utils/attendance_actions.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
enum ButtonActions { approve, reject }
class RegularizeActionButton extends StatefulWidget {
final dynamic attendanceController; // Replace dynamic with your controller's type
final dynamic log; // Replace dynamic with your log model type
final dynamic
attendanceController;
final dynamic log;
final String uniqueLogKey;
final ButtonActions action;
@ -21,6 +22,11 @@ class RegularizeActionButton extends StatefulWidget {
State<RegularizeActionButton> createState() => _RegularizeActionButtonState();
}
String capitalizeFirstLetter(String text) {
if (text.isEmpty) return text;
return text[0].toUpperCase() + text.substring(1);
}
class _RegularizeActionButtonState extends State<RegularizeActionButton> {
bool isUploading = false;
@ -41,13 +47,16 @@ class _RegularizeActionButtonState extends State<RegularizeActionButton> {
Color get backgroundColor {
// Use string keys for correct color lookup
return AttendanceActionColors.colors[_buttonTexts[widget.action]!] ?? Colors.grey;
return AttendanceActionColors.colors[_buttonTexts[widget.action]!] ??
Colors.grey;
}
Future<void> _handlePress() async {
if (widget.attendanceController.selectedProjectId == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Please select a project first")),
showAppSnackbar(
title: 'Warning',
message: 'Please select a project first',
type: SnackbarType.warning,
);
return;
}
@ -56,9 +65,11 @@ class _RegularizeActionButtonState extends State<RegularizeActionButton> {
isUploading = true;
});
widget.attendanceController.uploadingStates[widget.uniqueLogKey]?.value = true;
widget.attendanceController.uploadingStates[widget.uniqueLogKey]?.value =
true;
final success = await widget.attendanceController.captureAndUploadAttendance(
final success =
await widget.attendanceController.captureAndUploadAttendance(
widget.log.id,
widget.log.employeeId,
widget.attendanceController.selectedProjectId!,
@ -67,22 +78,27 @@ class _RegularizeActionButtonState extends State<RegularizeActionButton> {
imageCapture: false,
);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(success
? '${_buttonTexts[widget.action]} marked successfully!'
: 'Failed to mark ${_buttonTexts[widget.action]}.'),
),
showAppSnackbar(
title: success ? 'Success' : 'Error',
message: success
? '${capitalizeFirstLetter(_buttonTexts[widget.action]!)} marked successfully!'
: 'Failed to mark ${capitalizeFirstLetter(_buttonTexts[widget.action]!)}.',
type: success ? SnackbarType.success : SnackbarType.error,
);
if (success) {
widget.attendanceController.fetchEmployeesByProject(widget.attendanceController.selectedProjectId!);
widget.attendanceController.fetchAttendanceLogs(widget.attendanceController.selectedProjectId!);
await widget.attendanceController.fetchRegularizationLogs(widget.attendanceController.selectedProjectId!);
await widget.attendanceController.fetchProjectData(widget.attendanceController.selectedProjectId!);
widget.attendanceController.fetchEmployeesByProject(
widget.attendanceController.selectedProjectId!);
widget.attendanceController
.fetchAttendanceLogs(widget.attendanceController.selectedProjectId!);
await widget.attendanceController.fetchRegularizationLogs(
widget.attendanceController.selectedProjectId!);
await widget.attendanceController
.fetchProjectData(widget.attendanceController.selectedProjectId!);
}
widget.attendanceController.uploadingStates[widget.uniqueLogKey]?.value = false;
widget.attendanceController.uploadingStates[widget.uniqueLogKey]?.value =
false;
setState(() {
isUploading = false;
@ -101,7 +117,8 @@ class _RegularizeActionButtonState extends State<RegularizeActionButton> {
onPressed: isUploading ? null : _handlePress,
style: ElevatedButton.styleFrom(
backgroundColor: backgroundColor,
foregroundColor: Colors.white, // Ensures visibility on all backgrounds
foregroundColor:
Colors.white, // Ensures visibility on all backgrounds
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 6),
minimumSize: const Size(60, 20),
textStyle: const TextStyle(fontSize: 12),

View File

@ -0,0 +1,402 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:marco/controller/task_planing/daily_task_planing_controller.dart';
import 'package:marco/helpers/widgets/my_button.dart';
import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/helpers/widgets/my_spacing.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
class AssignTaskBottomSheet extends StatefulWidget {
final String workLocation;
final String activityName;
final int pendingTask;
final String workItemId;
final DateTime assignmentDate;
final String buildingName;
final String floorName;
final String workAreaName;
const AssignTaskBottomSheet({
super.key,
required this.buildingName,
required this.workLocation,
required this.floorName,
required this.workAreaName,
required this.activityName,
required this.pendingTask,
required this.workItemId,
required this.assignmentDate,
});
@override
State<AssignTaskBottomSheet> createState() => _AssignTaskBottomSheetState();
}
class _AssignTaskBottomSheetState extends State<AssignTaskBottomSheet> {
final DailyTaskPlaningController controller = Get.find();
final TextEditingController targetController = TextEditingController();
final TextEditingController descriptionController = TextEditingController();
String? selectedProjectId;
final ScrollController _employeeListScrollController = ScrollController();
@override
void dispose() {
_employeeListScrollController.dispose();
targetController.dispose();
descriptionController.dispose();
super.dispose();
}
@override
void initState() {
super.initState();
selectedProjectId = controller.selectedProjectId;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (selectedProjectId != null) {
controller.fetchEmployeesByProject(selectedProjectId!);
}
});
}
@override
Widget build(BuildContext context) {
return SafeArea(
child: Container(
padding: MediaQuery.of(context).viewInsets.add(MySpacing.all(16)),
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Icon(Icons.assignment, color: Colors.black54),
SizedBox(width: 8),
MyText.titleMedium("Assign Task",
fontSize: 18, fontWeight: 600),
],
),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Get.back(),
),
],
),
Divider(),
_infoRow(Icons.location_on, "Work Location",
"${widget.buildingName} > ${widget.floorName} > ${widget.workAreaName} > ${widget.activityName}"),
Divider(),
_infoRow(Icons.pending_actions, "Pending Task of Activity",
"${widget.pendingTask}"),
Divider(),
GestureDetector(
onTap: () {
final RenderBox overlay = Overlay.of(context)
.context
.findRenderObject() as RenderBox;
final Size screenSize = overlay.size;
showMenu(
context: context,
position: RelativeRect.fromLTRB(
screenSize.width / 2 - 100,
screenSize.height / 2 - 20,
screenSize.width / 2 - 100,
screenSize.height / 2 - 20,
),
items: [
const PopupMenuItem(
value: 'all',
child: Text("All Roles"),
),
...controller.roles.map((role) {
return PopupMenuItem(
value: role['id'].toString(),
child: Text(role['name'] ?? 'Unknown Role'),
);
}),
],
).then((value) {
if (value != null) {
controller.onRoleSelected(value == 'all' ? null : value);
}
});
},
child: Row(
children: [
MyText.titleMedium("Select Team :", fontWeight: 600),
const SizedBox(width: 4),
Icon(Icons.filter_alt,
color: const Color.fromARGB(255, 95, 132, 255)),
],
),
),
MySpacing.height(8),
Container(
constraints: BoxConstraints(
maxHeight: 150,
),
child: _buildEmployeeList(),
),
MySpacing.height(8),
Obx(() {
if (controller.selectedEmployees.isEmpty) {
return Container();
}
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Wrap(
spacing: 4,
runSpacing: 4,
children: controller.selectedEmployees.map((e) {
return Obx(() {
final isSelected =
controller.uploadingStates[e.id]?.value ?? false;
if (!isSelected) return Container();
return Chip(
label: Text(e.name,
style: const TextStyle(color: Colors.white)),
backgroundColor:
const Color.fromARGB(255, 95, 132, 255),
deleteIcon:
const Icon(Icons.close, color: Colors.white),
onDeleted: () {
controller.uploadingStates[e.id]?.value = false;
controller.updateSelectedEmployees();
});
});
}).toList(),
),
);
}),
_buildTextField(
icon: Icons.track_changes,
label: "Target for Today :",
controller: targetController,
hintText: "Enter target",
keyboardType: TextInputType.number,
validatorType: "target",
),
MySpacing.height(24),
_buildTextField(
icon: Icons.description,
label: "Description :",
controller: descriptionController,
hintText: "Enter task description",
maxLines: 3,
validatorType: "description",
),
MySpacing.height(24),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
MyButton(
onPressed: _onAssignTaskPressed,
backgroundColor: const Color.fromARGB(255, 95, 132, 255),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
MyText.bodyMedium("Assign Task", color: Colors.white),
],
),
),
],
),
],
),
),
),
);
}
Widget _buildEmployeeList() {
return Obx(() {
if (controller.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
final selectedRoleId = controller.selectedRoleId.value;
final filteredEmployees = selectedRoleId == null
? controller.employees
: controller.employees
.where((e) => e.jobRoleID.toString() == selectedRoleId)
.toList();
if (filteredEmployees.isEmpty) {
return const Text("No employees found for selected role.");
}
return Scrollbar(
controller: _employeeListScrollController,
thumbVisibility: true,
interactive: true,
child: ListView.builder(
controller: _employeeListScrollController,
shrinkWrap: true,
physics: const AlwaysScrollableScrollPhysics(),
itemCount: filteredEmployees.length,
itemBuilder: (context, index) {
final employee = filteredEmployees[index];
final rxBool = controller.uploadingStates[employee.id];
return Obx(() => Padding(
padding: const EdgeInsets.symmetric(vertical: 0),
child: Row(
children: [
Theme(
data: Theme.of(context)
.copyWith(unselectedWidgetColor: Colors.black),
child: Checkbox(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
side: const BorderSide(color: Colors.black),
),
value: rxBool?.value ?? false,
onChanged: (bool? selected) {
if (rxBool != null) {
rxBool.value = selected ?? false;
controller.updateSelectedEmployees();
}
},
fillColor:
WidgetStateProperty.resolveWith<Color>((states) {
if (states.contains(WidgetState.selected)) {
return const Color.fromARGB(255, 95, 132, 255);
}
return Colors.transparent;
}),
checkColor: Colors.white,
side: const BorderSide(color: Colors.black),
),
),
const SizedBox(width: 8),
Expanded(
child: Text(employee.name,
style: TextStyle(fontSize: 14))),
],
),
));
},
),
);
});
}
Widget _buildTextField({
required IconData icon,
required String label,
required TextEditingController controller,
required String hintText,
TextInputType keyboardType = TextInputType.text,
int maxLines = 1,
required String validatorType,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(icon, size: 18, color: Colors.black54),
const SizedBox(width: 6),
MyText.titleMedium(label, fontWeight: 600),
],
),
MySpacing.height(6),
TextFormField(
controller: controller,
keyboardType: keyboardType,
maxLines: maxLines,
decoration: InputDecoration(
hintText: hintText,
border: const OutlineInputBorder(),
),
validator: (value) => this
.controller
.formFieldValidator(value, fieldType: validatorType),
),
],
);
}
Widget _infoRow(IconData icon, String title, String value) {
return Padding(
padding: MySpacing.y(6),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, size: 20, color: Colors.grey[700]),
const SizedBox(width: 8),
Expanded(
child: RichText(
text: TextSpan(
children: [
WidgetSpan(
child: MyText.titleMedium("$title: ",
fontWeight: 600, color: Colors.black),
),
TextSpan(
text: value,
style: const TextStyle(color: Colors.black),
),
],
),
),
),
],
),
);
}
void _onAssignTaskPressed() {
final selectedTeam = controller.uploadingStates.entries
.where((e) => e.value.value)
.map((e) => e.key)
.toList();
if (selectedTeam.isEmpty) {
showAppSnackbar(
title: "Team Required",
message: "Please select at least one team member",
type: SnackbarType.error,
);
return;
}
final target = int.tryParse(targetController.text.trim());
if (target == null || target <= 0) {
showAppSnackbar(
title: "Invalid Input",
message: "Please enter a valid target number",
type: SnackbarType.error,
);
return;
}
final description = descriptionController.text.trim();
if (description.isEmpty) {
showAppSnackbar(
title: "Description Required",
message: "Please enter a description",
type: SnackbarType.error,
);
return;
}
controller.assignDailyTask(
workItemId: widget.workItemId,
plannedTask: target,
description: description,
taskTeam: selectedTeam,
assignmentDate: widget.assignmentDate,
);
}
}

View File

@ -0,0 +1,482 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:marco/controller/task_planing/report_task_controller.dart';
import 'package:marco/helpers/theme/app_theme.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
import 'package:marco/helpers/widgets/my_button.dart';
import 'package:marco/helpers/widgets/my_spacing.dart';
import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/helpers/widgets/my_text_style.dart';
import 'package:marco/helpers/widgets/avatar.dart';
import 'package:marco/helpers/widgets/my_team_model_sheet.dart';
import 'package:intl/intl.dart';
class CommentTaskBottomSheet extends StatefulWidget {
final Map<String, dynamic> taskData;
final VoidCallback? onCommentSuccess;
const CommentTaskBottomSheet({
super.key,
required this.taskData,
this.onCommentSuccess,
});
@override
State<CommentTaskBottomSheet> createState() => _CommentTaskBottomSheetState();
}
class _Member {
final String firstName;
_Member(this.firstName);
}
class _CommentTaskBottomSheetState extends State<CommentTaskBottomSheet>
with UIMixin {
late ReportTaskController controller;
final ScrollController _scrollController = ScrollController();
@override
void initState() {
super.initState();
controller = Get.put(ReportTaskController(),
tag: widget.taskData['taskId'] ?? UniqueKey().toString());
final data = widget.taskData;
controller.basicValidator.getController('assigned_date')?.text =
data['assignedOn'] ?? '';
controller.basicValidator.getController('assigned_by')?.text =
data['assignedBy'] ?? '';
controller.basicValidator.getController('work_area')?.text =
data['location'] ?? '';
controller.basicValidator.getController('activity')?.text =
data['activity'] ?? '';
controller.basicValidator.getController('planned_work')?.text =
data['plannedWork'] ?? '';
controller.basicValidator.getController('completed_work')?.text =
data['completedWork'] ?? '';
controller.basicValidator.getController('team_members')?.text =
(data['teamMembers'] as List<dynamic>).join(', ');
controller.basicValidator.getController('assigned')?.text =
data['assigned'] ?? '';
controller.basicValidator.getController('task_id')?.text =
data['taskId'] ?? '';
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_scrollController.hasClients) {
_scrollController.jumpTo(_scrollController.position.maxScrollExtent);
}
});
}
String timeAgo(String dateString) {
try {
DateTime date = DateTime.parse(dateString + "Z").toLocal();
final now = DateTime.now();
final difference = now.difference(date);
if (difference.inDays > 8) {
return DateFormat('dd-MM-yyyy').format(date);
} else if (difference.inDays >= 1) {
return '${difference.inDays} day${difference.inDays > 1 ? 's' : ''} ago';
} else if (difference.inHours >= 1) {
return '${difference.inHours} hr${difference.inHours > 1 ? 's' : ''} ago';
} else if (difference.inMinutes >= 1) {
return '${difference.inMinutes} min${difference.inMinutes > 1 ? 's' : ''} ago';
} else {
return 'just now';
}
} catch (e) {
print('Error parsing date: $e');
return '';
}
}
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
),
child: SingleChildScrollView(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom + 24,
left: 24,
right: 24,
top: 12,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Drag handle
Container(
width: 40,
height: 4,
margin: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
color: Colors.grey.shade400,
borderRadius: BorderRadius.circular(2),
),
),
GetBuilder<ReportTaskController>(
tag: widget.taskData['taskId'] ?? '',
builder: (controller) {
return Form(
key: controller.basicValidator.formKey,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
MyText.titleMedium(
"Comment Task",
fontWeight: 600,
fontSize: 18,
),
],
),
MySpacing.height(24),
buildRow(
"Assigned By",
controller.basicValidator
.getController('assigned_by')
?.text
.trim(),
icon: Icons.person_outline,
),
buildRow(
"Work Area",
controller.basicValidator
.getController('work_area')
?.text
.trim(),
icon: Icons.place_outlined,
),
buildRow(
"Activity",
controller.basicValidator
.getController('activity')
?.text
.trim(),
icon: Icons.assignment_outlined,
),
buildRow(
"Planned Work",
controller.basicValidator
.getController('planned_work')
?.text
.trim(),
icon: Icons.schedule_outlined,
),
buildRow(
"Completed Work",
controller.basicValidator
.getController('completed_work')
?.text
.trim(),
icon: Icons.done_all_outlined,
),
buildTeamMembers(),
Row(
children: [
Icon(Icons.comment_outlined,
size: 18, color: Colors.grey[700]),
MySpacing.width(8),
MyText.titleSmall(
"Comment:",
fontWeight: 600,
),
],
),
MySpacing.height(8),
TextFormField(
validator: controller.basicValidator
.getValidation('comment'),
controller: controller.basicValidator
.getController('comment'),
keyboardType: TextInputType.text,
decoration: InputDecoration(
hintText: "eg: Work done successfully",
hintStyle: MyTextStyle.bodySmall(xMuted: true),
border: outlineInputBorder,
enabledBorder: outlineInputBorder,
focusedBorder: focusedInputBorder,
contentPadding: MySpacing.all(16),
isCollapsed: true,
floatingLabelBehavior: FloatingLabelBehavior.never,
),
),
MySpacing.height(24),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
MyButton.text(
onPressed: () => Navigator.of(context).pop(),
padding: MySpacing.xy(20, 16),
splashColor: contentTheme.secondary.withAlpha(25),
child: MyText.bodySmall('Cancel'),
),
MySpacing.width(12),
Obx(() {
return MyButton(
onPressed: controller.isLoading.value
? null
: () async {
if (controller.basicValidator
.validateForm()) {
await controller.commentTask(
projectId: controller.basicValidator
.getController('task_id')
?.text ??
'',
comment: controller.basicValidator
.getController('comment')
?.text ??
'',
);
if (widget.onCommentSuccess != null) {
widget.onCommentSuccess!();
}
}
},
elevation: 0,
padding: MySpacing.xy(20, 16),
backgroundColor: Colors.blueAccent,
borderRadiusAll: AppStyle.buttonRadius.medium,
child: controller.isLoading.value
? SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor:
AlwaysStoppedAnimation<Color>(
contentTheme.onPrimary),
),
)
: MyText.bodySmall(
'Comment',
color: contentTheme.onPrimary,
),
);
}),
],
),
MySpacing.height(24),
if ((widget.taskData['taskComments'] as List<dynamic>?)
?.isNotEmpty ==
true) ...[
Row(
children: [
Icon(Icons.chat_bubble_outline,
size: 18, color: Colors.grey[700]),
MySpacing.width(8),
MyText.titleSmall(
"Comments",
fontWeight: 600,
),
],
),
MySpacing.height(12),
Builder(
builder: (context) {
final comments = List<Map<String, dynamic>>.from(
widget.taskData['taskComments'] as List,
);
comments.sort((a, b) {
final aDate =
DateTime.tryParse(a['date'] ?? '') ??
DateTime.fromMillisecondsSinceEpoch(0);
final bDate =
DateTime.tryParse(b['date'] ?? '') ??
DateTime.fromMillisecondsSinceEpoch(0);
return bDate.compareTo(
aDate); // descending: newest first
});
return SizedBox(
height: 300,
child: ListView.builder(
itemCount: comments.length,
itemBuilder: (context, index) {
final comment = comments[index];
final commentText = comment['text'] ?? '-';
final commentedBy =
comment['commentedBy'] ?? 'Unknown';
final relativeTime =
timeAgo(comment['date'] ?? '');
return Container(
margin: EdgeInsets.symmetric(
vertical: 6, horizontal: 8),
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey.shade200,
borderRadius: BorderRadius.circular(12),
),
child: Row(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
// Avatar for commenter
Avatar(
firstName:
commentedBy.split(' ').first,
lastName: commentedBy
.split(' ')
.length >
1
? commentedBy.split(' ').last
: '',
size: 32,
),
SizedBox(width: 12),
// Comment text and meta
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment:
MainAxisAlignment
.spaceBetween,
children: [
Text(
commentedBy,
style: TextStyle(
fontWeight:
FontWeight.bold,
color: Colors.black87,
),
),
Text(
relativeTime,
style: TextStyle(
fontSize: 12,
color: Colors.black54,
),
)
],
),
SizedBox(height: 6),
Text(
commentText,
style: TextStyle(
fontWeight: FontWeight.w500,
color: Colors.black87,
),
),
],
),
),
],
),
);
},
),
);
},
),
],
],
),
),
);
},
),
],
),
),
);
}
Widget buildTeamMembers() {
final teamMembersText =
controller.basicValidator.getController('team_members')?.text ?? '';
final members = teamMembersText
.split(',')
.map((e) => e.trim())
.where((e) => e.isNotEmpty)
.toList();
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
MyText.titleSmall(
"Team Members:",
fontWeight: 600,
),
MySpacing.width(12),
GestureDetector(
onTap: () {
TeamBottomSheet.show(
context: context,
teamMembers: members.map((name) => _Member(name)).toList(),
);
},
child: SizedBox(
height: 32,
width: 100,
child: Stack(
children: [
for (int i = 0; i < members.length.clamp(0, 3); i++)
Positioned(
left: i * 24.0,
child: Tooltip(
message: members[i],
child: Avatar(
firstName: members[i],
lastName: '',
size: 32,
),
),
),
if (members.length > 3)
Positioned(
left: 2 * 24.0,
child: CircleAvatar(
radius: 16,
backgroundColor: Colors.grey.shade300,
child: MyText.bodyMedium(
'+${members.length - 3}',
style: const TextStyle(
fontSize: 12, color: Colors.black87),
),
),
),
],
),
),
),
],
),
);
}
Widget buildRow(String label, String? value, {IconData? icon}) {
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (icon != null)
Padding(
padding: const EdgeInsets.only(right: 8.0, top: 2),
child: Icon(icon, size: 18, color: Colors.grey[700]),
),
MyText.titleSmall(
"$label:",
fontWeight: 600,
),
MySpacing.width(12),
Expanded(
child: MyText.bodyMedium(value?.isNotEmpty == true ? value! : "-"),
),
],
),
);
}
}

View File

@ -0,0 +1,206 @@
import 'package:flutter/material.dart';
import 'package:marco/controller/permission_controller.dart';
import 'package:marco/controller/dashboard/daily_task_controller.dart';
import 'package:intl/intl.dart';
import 'package:marco/helpers/widgets/my_text.dart';
class DailyProgressReportFilter extends StatefulWidget {
final DailyTaskController controller;
final PermissionController permissionController;
const DailyProgressReportFilter({
super.key,
required this.controller,
required this.permissionController,
});
@override
State<DailyProgressReportFilter> createState() =>
_DailyProgressReportFilterState();
}
class _DailyProgressReportFilterState extends State<DailyProgressReportFilter> {
late String? tempSelectedProjectId;
bool showProjectList = false;
@override
void initState() {
super.initState();
tempSelectedProjectId = widget.controller.selectedProjectId;
}
String getLabelText() {
final startDate = widget.controller.startDateTask;
final endDate = widget.controller.endDateTask;
if (startDate != null && endDate != null) {
final start = DateFormat('dd MM yyyy').format(startDate);
final end = DateFormat('dd MM yyyy').format(endDate);
return "$start - $end";
}
return "Select Date Range";
}
@override
Widget build(BuildContext context) {
final accessibleProjects = widget.controller.projects
.where((project) => widget.permissionController
.isUserAssignedToProject(project.id.toString()))
.toList();
List<Widget> filterWidgets;
if (showProjectList) {
filterWidgets = accessibleProjects.isEmpty
? [
const Padding(
padding: EdgeInsets.all(12.0),
child: Center(child: Text('No Projects Assigned')),
),
]
: accessibleProjects.map((project) {
final isSelected = tempSelectedProjectId == project.id.toString();
return ListTile(
dense: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
title: Text(project.name),
trailing: isSelected ? const Icon(Icons.check) : null,
onTap: () {
setState(() {
tempSelectedProjectId = project.id.toString();
showProjectList = false;
});
},
);
}).toList();
} else {
final selectedProject = accessibleProjects.isNotEmpty
? accessibleProjects.firstWhere(
(p) => p.id.toString() == tempSelectedProjectId,
orElse: () => accessibleProjects[0],
)
: null;
final selectedProjectName = selectedProject?.name ?? "Select Project";
filterWidgets = [
Padding(
padding: EdgeInsets.fromLTRB(16, 12, 16, 4),
child: Align(
alignment: Alignment.centerLeft,
child: MyText.titleSmall(
'Select Project',
fontWeight: 600,
),
),
),
ListTile(
dense: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
title: Text(selectedProjectName),
trailing: const Icon(Icons.arrow_drop_down),
onTap: () => setState(() => showProjectList = true),
),
];
filterWidgets.addAll([
const Divider(),
Padding(
padding: EdgeInsets.fromLTRB(16, 12, 16, 4),
child: Align(
alignment: Alignment.centerLeft,
child: MyText.titleSmall(
"Select Date Range",
fontWeight: 600,
)),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: InkWell(
borderRadius: BorderRadius.circular(10),
onTap: () => widget.controller.selectDateRangeForTaskData(
context,
widget.controller,
),
child: Ink(
decoration: BoxDecoration(
color: Colors.grey.shade100,
border: Border.all(color: Colors.grey.shade400),
borderRadius: BorderRadius.circular(10),
),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
child: Row(
children: [
Icon(Icons.date_range, color: Colors.blue.shade600),
const SizedBox(width: 12),
Expanded(
child: Text(
getLabelText(),
style: const TextStyle(
fontSize: 16,
color: Colors.black87,
fontWeight: FontWeight.w500,
),
overflow: TextOverflow.ellipsis,
),
),
const Icon(Icons.arrow_drop_down, color: Colors.grey),
],
),
),
),
),
]);
}
return SafeArea(
child: Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.only(top: 12, bottom: 8),
child: Center(
child: Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: Colors.grey[400],
borderRadius: BorderRadius.circular(4),
),
),
),
),
...filterWidgets,
const Divider(),
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: const Text('Apply Filter'),
onPressed: () {
Navigator.pop(context, {
'projectId': tempSelectedProjectId,
});
},
),
),
),
],
),
),
),
);
}
}

View File

@ -0,0 +1,144 @@
import 'package:flutter/material.dart';
import 'package:marco/controller/permission_controller.dart';
import 'package:marco/controller/task_planing/daily_task_planing_controller.dart';
import 'package:marco/helpers/widgets/my_text.dart';
class DailyTaskPlaningFilter extends StatelessWidget {
final DailyTaskPlaningController controller;
final PermissionController permissionController;
const DailyTaskPlaningFilter({
super.key,
required this.controller,
required this.permissionController,
});
@override
Widget build(BuildContext context) {
String? tempSelectedProjectId = controller.selectedProjectId;
bool showProjectList = false;
final accessibleProjects = controller.projects
.where((project) =>
permissionController.isUserAssignedToProject(project.id.toString()))
.toList();
return StatefulBuilder(builder: (context, setState) {
List<Widget> filterWidgets;
if (showProjectList) {
filterWidgets = accessibleProjects.isEmpty
? [
Padding(
padding: EdgeInsets.all(12.0),
child: Center(
child: MyText.titleSmall(
'No Projects Assigned',
fontWeight: 600,
),
),
),
]
: accessibleProjects.map((project) {
final isSelected =
tempSelectedProjectId == project.id.toString();
return ListTile(
dense: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
title: MyText.titleSmall(project.name),
trailing: isSelected ? const Icon(Icons.check) : null,
onTap: () {
setState(() {
tempSelectedProjectId = project.id.toString();
showProjectList = false;
});
},
);
}).toList();
} else {
final selectedProject = accessibleProjects.isNotEmpty
? accessibleProjects.firstWhere(
(p) => p.id.toString() == tempSelectedProjectId,
orElse: () => accessibleProjects[0],
)
: null;
final selectedProjectName = selectedProject?.name ?? "Select Project";
filterWidgets = [
Padding(
padding: EdgeInsets.fromLTRB(16, 12, 16, 4),
child: Align(
alignment: Alignment.centerLeft,
child: MyText.titleSmall(
'Select Project',
fontWeight: 600,
),
),
),
ListTile(
dense: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
title: MyText.titleSmall(selectedProjectName),
trailing: const Icon(Icons.arrow_drop_down),
onTap: () => setState(() => showProjectList = true),
),
];
}
return Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.only(top: 12, bottom: 8),
child: Center(
child: Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: Colors.grey[400],
borderRadius: BorderRadius.circular(4),
),
),
),
),
...filterWidgets,
const Divider(),
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Color.fromARGB(255, 95, 132, 255),
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: MyText.titleSmall(
'Apply Filter',
fontWeight: 600,
color: Colors.white,
),
onPressed: () {
Navigator.pop(context, {
'projectId': tempSelectedProjectId,
});
},
),
),
),
],
),
),
);
});
}
}

View File

@ -0,0 +1,234 @@
class TaskPlanningDetailsModel {
final List<Building> buildings;
final String id;
final String name;
final String projectAddress;
final String contactPerson;
final DateTime startDate;
final DateTime endDate;
final String projectStatusId;
TaskPlanningDetailsModel({
required this.buildings,
required this.id,
required this.name,
required this.projectAddress,
required this.contactPerson,
required this.startDate,
required this.endDate,
required this.projectStatusId,
});
factory TaskPlanningDetailsModel.fromJson(Map<String, dynamic> json) {
return TaskPlanningDetailsModel(
buildings: (json['buildings'] as List<dynamic>?)
?.map((b) => Building.fromJson(b))
.toList() ??
[],
id: json['id'],
name: json['name'],
projectAddress: json['projectAddress'],
contactPerson: json['contactPerson'],
startDate: DateTime.parse(json['startDate']),
endDate: DateTime.parse(json['endDate']),
projectStatusId: json['projectStatusId'],
);
}
}
class Building {
final String id;
final String name;
final String description;
final List<Floor> floors;
Building({
required this.id,
required this.name,
required this.description,
required this.floors,
});
factory Building.fromJson(Map<String, dynamic> json) {
return Building(
id: json['id'],
name: json['name'],
description: json['description'],
floors: (json['floors'] as List).map((f) => Floor.fromJson(f)).toList(),
);
}
}
class Floor {
final String id;
final String floorName;
final List<WorkArea> workAreas;
Floor({
required this.id,
required this.floorName,
required this.workAreas,
});
factory Floor.fromJson(Map<String, dynamic> json) {
return Floor(
id: json['id'],
floorName: json['floorName'],
workAreas:
(json['workAreas'] as List).map((w) => WorkArea.fromJson(w)).toList(),
);
}
}
class WorkArea {
final String id;
final String areaName;
final List<WorkItemWrapper> workItems;
WorkArea({
required this.id,
required this.areaName,
required this.workItems,
});
factory WorkArea.fromJson(Map<String, dynamic> json) {
return WorkArea(
id: json['id'],
areaName: json['areaName'],
workItems: (json['workItems'] as List)
.map((w) => WorkItemWrapper.fromJson(w))
.toList(),
);
}
}
class WorkItemWrapper {
final String workItemId;
final WorkItem workItem;
WorkItemWrapper({
required this.workItemId,
required this.workItem,
});
factory WorkItemWrapper.fromJson(Map<String, dynamic> json) {
return WorkItemWrapper(
workItemId: json['workItemId'],
workItem: WorkItem.fromJson(json['workItem']),
);
}
}
class WorkItem {
final String? id;
final String? activityId;
final String? workCategoryId;
final String? workAreaId;
final WorkAreaBasic? workArea;
final ActivityMaster? activityMaster;
final WorkCategoryMaster? workCategoryMaster;
final int? plannedWork;
final int? completedWork;
final DateTime? taskDate;
final String? tenantId;
final Tenant? tenant;
WorkItem({
this.id,
this.activityId,
this.workCategoryId,
this.workAreaId,
this.workArea,
this.activityMaster,
this.workCategoryMaster,
this.plannedWork,
this.completedWork,
this.taskDate,
this.tenantId,
this.tenant,
});
factory WorkItem.fromJson(Map<String, dynamic> json) {
return WorkItem(
id: json['id'] as String?,
activityId: json['activityId'] as String?,
workCategoryId: json['workCategoryId'] as String?,
workAreaId: json['workAreaId'] as String?,
workArea: json['workArea'] != null
? WorkAreaBasic.fromJson(json['workArea'] as Map<String, dynamic>)
: null,
activityMaster: json['activityMaster'] != null
? ActivityMaster.fromJson(
json['activityMaster'] as Map<String, dynamic>)
: null,
workCategoryMaster: json['workCategoryMaster'] != null
? WorkCategoryMaster.fromJson(
json['workCategoryMaster'] as Map<String, dynamic>)
: null,
plannedWork: json['plannedWork'] as int?,
completedWork: json['completedWork'] as int?,
taskDate:
json['taskDate'] != null ? DateTime.tryParse(json['taskDate']) : null,
tenantId: json['tenantId'] as String?,
tenant: json['tenant'] != null
? Tenant.fromJson(json['tenant'] as Map<String, dynamic>)
: null,
);
}
}
class WorkAreaBasic {
final String? id;
final String? name;
WorkAreaBasic({this.id, this.name});
factory WorkAreaBasic.fromJson(Map<String, dynamic> json) {
return WorkAreaBasic(
id: json['id'] as String?,
name: json['name'] as String?,
);
}
}
class ActivityMaster {
final String? id;
final String? name;
ActivityMaster({this.id, this.name});
factory ActivityMaster.fromJson(Map<String, dynamic> json) {
return ActivityMaster(
id: json['id'] as String?,
name: json['activityName'] as String?,
);
}
}
class WorkCategoryMaster {
final String? id;
final String? name;
WorkCategoryMaster({this.id, this.name});
factory WorkCategoryMaster.fromJson(Map<String, dynamic> json) {
return WorkCategoryMaster(
id: json['id'] as String?,
name: json['name'] as String?,
);
}
}
class Tenant {
final String? id;
final String? name;
Tenant({this.id, this.name});
factory Tenant.fromJson(Map<String, dynamic> json) {
return Tenant(
id: json['id'] as String?,
name: json['name'] as String?,
);
}
}

View File

@ -0,0 +1,309 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:marco/controller/task_planing/report_task_controller.dart';
import 'package:marco/helpers/theme/app_theme.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
import 'package:marco/helpers/widgets/my_button.dart';
import 'package:marco/helpers/widgets/my_spacing.dart';
import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/helpers/widgets/my_text_style.dart';
class ReportTaskBottomSheet extends StatefulWidget {
final Map<String, dynamic> taskData;
const ReportTaskBottomSheet({super.key, required this.taskData});
@override
State<ReportTaskBottomSheet> createState() => _ReportTaskBottomSheetState();
}
class _ReportTaskBottomSheetState extends State<ReportTaskBottomSheet>
with UIMixin {
late final ReportTaskController controller;
@override
void initState() {
super.initState();
// Initialize the controller with a unique tag (optional)
controller = Get.put(ReportTaskController(),
tag: widget.taskData['taskId'] ?? UniqueKey().toString());
final taskData = widget.taskData;
controller.basicValidator.getController('assigned_date')?.text =
taskData['assignedOn'] ?? '';
controller.basicValidator.getController('assigned_by')?.text =
taskData['assignedBy'] ?? '';
controller.basicValidator.getController('work_area')?.text =
taskData['location'] ?? '';
controller.basicValidator.getController('activity')?.text =
taskData['activity'] ?? '';
controller.basicValidator.getController('team_size')?.text =
taskData['teamSize']?.toString() ?? '';
controller.basicValidator.getController('assigned')?.text =
taskData['assigned'] ?? '';
controller.basicValidator.getController('task_id')?.text =
taskData['taskId'] ?? '';
}
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
),
child: SingleChildScrollView(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom + 24,
left: 24,
right: 24,
top: 12,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Drag handle
Container(
width: 40,
height: 4,
margin: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
color: Colors.grey.shade400,
borderRadius: BorderRadius.circular(2),
),
),
GetBuilder<ReportTaskController>(
tag: widget.taskData['taskId'] ?? '',
init: controller,
builder: (_) {
return Form(
key: controller.basicValidator.formKey,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: MyText.titleMedium(
"Report Task",
fontWeight: 600,
),
),
MySpacing.height(16),
buildRow(
"Assigned Date",
controller.basicValidator
.getController('assigned_date')
?.text
.trim()),
buildRow(
"Assigned By",
controller.basicValidator
.getController('assigned_by')
?.text
.trim()),
buildRow(
"Work Area",
controller.basicValidator
.getController('work_area')
?.text
.trim()),
buildRow(
"Activity",
controller.basicValidator
.getController('activity')
?.text
.trim()),
buildRow(
"Team Size",
controller.basicValidator
.getController('team_size')
?.text
.trim()),
buildRow(
"Assigned",
controller.basicValidator
.getController('assigned')
?.text
.trim()),
Row(
children: [
Icon(Icons.work_outline,
size: 18, color: Colors.grey[700]),
MySpacing.width(8),
MyText.titleSmall(
"Completed Work:",
fontWeight: 600,
),
],
),
MySpacing.height(8),
TextFormField(
validator: controller.basicValidator
.getValidation('completed_work'),
controller: controller.basicValidator
.getController('completed_work'),
keyboardType: TextInputType.number,
decoration: InputDecoration(
hintText: "eg: 10",
hintStyle: MyTextStyle.bodySmall(xMuted: true),
border: outlineInputBorder,
enabledBorder: outlineInputBorder,
focusedBorder: focusedInputBorder,
contentPadding: MySpacing.all(16),
isCollapsed: true,
floatingLabelBehavior: FloatingLabelBehavior.never,
),
),
MySpacing.height(24),
Row(
children: [
Icon(Icons.comment_outlined,
size: 18, color: Colors.grey[700]),
MySpacing.width(8),
MyText.titleSmall(
"Comment:",
fontWeight: 600,
),
],
),
MySpacing.height(8),
TextFormField(
validator: controller.basicValidator
.getValidation('comment'),
controller: controller.basicValidator
.getController('comment'),
keyboardType: TextInputType.text,
decoration: InputDecoration(
hintText: "eg: Work done successfully",
hintStyle: MyTextStyle.bodySmall(xMuted: true),
border: outlineInputBorder,
enabledBorder: outlineInputBorder,
focusedBorder: focusedInputBorder,
contentPadding: MySpacing.all(16),
isCollapsed: true,
floatingLabelBehavior: FloatingLabelBehavior.never,
),
),
MySpacing.height(24),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
MyButton.text(
onPressed: () => Navigator.of(context).pop(),
padding: MySpacing.xy(20, 16),
splashColor: contentTheme.secondary.withAlpha(25),
child: MyText.bodySmall('Cancel'),
),
MySpacing.width(12),
Obx(() {
return MyButton(
onPressed: controller.reportStatus.value ==
ApiStatus.loading
? null
: () async {
if (controller.basicValidator
.validateForm()) {
await controller.reportTask(
projectId: controller.basicValidator
.getController('task_id')
?.text ??
'',
comment: controller.basicValidator
.getController('comment')
?.text ??
'',
completedTask: int.tryParse(
controller.basicValidator
.getController(
'completed_work')
?.text ??
'') ??
0,
checklist: [],
reportedDate: DateTime.now(),
);
}
},
elevation: 0,
padding: MySpacing.xy(20, 16),
backgroundColor: Colors.blueAccent,
borderRadiusAll: AppStyle.buttonRadius.medium,
child: controller.reportStatus.value ==
ApiStatus.loading
? SizedBox(
height: 16,
width: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor:
AlwaysStoppedAnimation<Color>(
contentTheme.onPrimary),
),
)
: MyText.bodySmall(
'Report',
color: contentTheme.onPrimary,
),
);
}),
],
),
],
),
),
);
},
),
],
),
),
);
}
Widget buildRow(String label, String? value) {
IconData icon;
switch (label) {
case "Assigned Date":
icon = Icons.calendar_today_outlined;
break;
case "Assigned By":
icon = Icons.person_outline;
break;
case "Work Area":
icon = Icons.place_outlined;
break;
case "Activity":
icon = Icons.run_circle_outlined;
break;
case "Team Size":
icon = Icons.group_outlined;
break;
case "Assigned":
icon = Icons.assignment_turned_in_outlined;
break;
default:
icon = Icons.info_outline;
}
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, size: 18, color: Colors.grey[700]),
MySpacing.width(8),
MyText.titleSmall(
"$label:",
fontWeight: 600,
),
MySpacing.width(12),
Expanded(
child: MyText.bodyMedium(value?.isNotEmpty == true ? value! : "-"),
),
],
),
);
}
}

View File

@ -1,5 +1,6 @@
class TaskModel {
final String assignmentDate;
final DateTime assignmentDate;
final DateTime? reportedDate;
final String id;
final WorkItem? workItem;
final String workItemId;
@ -9,11 +10,9 @@ class TaskModel {
final List<TeamMember> teamMembers;
final List<Comment> comments;
int get plannedWork => workItem?.plannedWork ?? 0;
int get completedWork => workItem?.completedWork ?? 0;
TaskModel({
required this.assignmentDate,
this.reportedDate,
required this.id,
required this.workItem,
required this.workItemId,
@ -25,15 +24,15 @@ class TaskModel {
});
factory TaskModel.fromJson(Map<String, dynamic> json) {
final workItemJson = json['workItem'];
final workItem =
workItemJson != null ? WorkItem.fromJson(workItemJson) : null;
return TaskModel(
assignmentDate: json['assignmentDate'],
id: json['id'] ?? '',
assignmentDate: DateTime.parse(json['assignmentDate']),
reportedDate: json['reportedDate'] != null
? DateTime.tryParse(json['reportedDate'])
: null,
id: json['id'],
workItem:
json['workItem'] != null ? WorkItem.fromJson(json['workItem']) : null,
workItemId: json['workItemId'],
workItem: workItem,
plannedTask: json['plannedTask'],
completedTask: json['completedTask'],
assignedBy: AssignedBy.fromJson(json['assignedBy']),
@ -47,12 +46,14 @@ class TaskModel {
}
class WorkItem {
final String? id;
final ActivityMaster? activityMaster;
final WorkArea? workArea;
final int? plannedWork;
final int? completedWork;
WorkItem({
this.id,
this.activityMaster,
this.workArea,
this.plannedWork,
@ -61,6 +62,7 @@ class WorkItem {
factory WorkItem.fromJson(Map<String, dynamic> json) {
return WorkItem(
id: json['id']?.toString(),
activityMaster: json['activityMaster'] != null
? ActivityMaster.fromJson(json['activityMaster'])
: null,
@ -78,7 +80,7 @@ class ActivityMaster {
ActivityMaster({required this.activityName});
factory ActivityMaster.fromJson(Map<String, dynamic> json) {
return ActivityMaster(activityName: json['activityName']);
return ActivityMaster(activityName: json['activityName'] ?? '');
}
}
@ -90,7 +92,7 @@ class WorkArea {
factory WorkArea.fromJson(Map<String, dynamic> json) {
return WorkArea(
areaName: json['areaName'],
areaName: json['areaName'] ?? '',
floor: json['floor'] != null ? Floor.fromJson(json['floor']) : null,
);
}
@ -104,7 +106,7 @@ class Floor {
factory Floor.fromJson(Map<String, dynamic> json) {
return Floor(
floorName: json['floorName'],
floorName: json['floorName'] ?? '',
building:
json['building'] != null ? Building.fromJson(json['building']) : null,
);
@ -117,34 +119,46 @@ class Building {
Building({required this.name});
factory Building.fromJson(Map<String, dynamic> json) {
return Building(name: json['name']);
return Building(name: json['name'] ?? '');
}
}
class AssignedBy {
final String id;
final String firstName;
final String? lastName;
AssignedBy({required this.firstName, this.lastName});
AssignedBy({
required this.id,
required this.firstName,
this.lastName,
});
factory AssignedBy.fromJson(Map<String, dynamic> json) {
return AssignedBy(
firstName: json['firstName'],
id: json['id']?.toString() ?? '',
firstName: json['firstName'] ?? '',
lastName: json['lastName'],
);
}
}
class TeamMember {
final String id;
final String firstName;
final String? lastName;
TeamMember({required this.firstName, this.lastName});
TeamMember({
required this.id,
required this.firstName,
this.lastName,
});
factory TeamMember.fromJson(Map<String, dynamic> json) {
return TeamMember(
firstName: json['firstName'],
lastName: json['lastName'],
id: json['id']?.toString() ?? '',
firstName: json['firstName']?.toString() ?? '',
lastName: json['lastName']?.toString(),
);
}
}
@ -152,7 +166,7 @@ class TeamMember {
class Comment {
final String comment;
final TeamMember commentedBy;
final String timestamp;
final DateTime timestamp;
Comment({
required this.comment,
@ -162,9 +176,11 @@ class Comment {
factory Comment.fromJson(Map<String, dynamic> json) {
return Comment(
comment: json['comment'],
commentedBy: TeamMember.fromJson(json['employee']),
timestamp: json['commentDate'],
comment: json['comment']?.toString() ?? '',
commentedBy: json['employee'] != null
? TeamMember.fromJson(json['employee'])
: TeamMember(id: '', firstName: '', lastName: null),
timestamp: DateTime.parse(json['commentDate'] ?? ''),
);
}
}

View File

@ -12,9 +12,11 @@ class EmployeeModel {
final String jobRole;
final String email;
final String phoneNumber;
final String jobRoleID;
EmployeeModel({
required this.id,
required this.jobRoleID,
required this.employeeId,
required this.name,
required this.designation,
@ -33,6 +35,7 @@ class EmployeeModel {
return EmployeeModel(
id: json['id']?.toString() ?? '',
employeeId: json['employeeId']?.toString() ?? '',
jobRoleID: json['jobRoleId']?.toString() ?? '',
name: '${json['firstName'] ?? ''} ${json['lastName'] ?? ''}'.trim(),
designation: json['jobRoleName'] ?? '',
checkIn: json['checkInTime'] != null
@ -57,6 +60,7 @@ class EmployeeModel {
'employeeId': employeeId,
'firstName': name.split(' ').first,
'lastName': name.split(' ').length > 1 ? name.split(' ').last : '',
'jobRoleId': jobRoleID,
'jobRoleName': designation,
'checkInTime': checkIn?.toIso8601String(),
'checkOutTime': checkOut?.toIso8601String(),

View File

@ -0,0 +1,245 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:marco/controller/dashboard/add_employee_controller.dart';
import 'package:marco/helpers/theme/app_theme.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
import 'package:marco/helpers/widgets/my_button.dart';
import 'package:marco/helpers/widgets/my_spacing.dart';
import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/helpers/widgets/my_text_style.dart';
import 'package:marco/controller/dashboard/employees_screen_controller.dart';
class AddEmployeeBottomSheet extends StatefulWidget {
@override
_AddEmployeeBottomSheetState createState() => _AddEmployeeBottomSheetState();
}
class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
with UIMixin {
final AddEmployeeController controller = Get.put(AddEmployeeController());
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return GetBuilder<AddEmployeeController>(
init: controller,
builder: (_) {
return SingleChildScrollView(
padding: MediaQuery.of(context).viewInsets,
child: Container(
padding:
const EdgeInsets.only(top: 12, left: 24, right: 24, bottom: 24),
decoration: BoxDecoration(
color: theme.cardColor,
borderRadius:
const BorderRadius.vertical(top: Radius.circular(24)),
boxShadow: const [
BoxShadow(
color: Colors.black26,
blurRadius: 10,
spreadRadius: 1,
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Drag handle bar
Center(
child: Container(
width: 40,
height: 4,
margin: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
color: Colors.grey.shade400,
borderRadius: BorderRadius.circular(2),
),
),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
MyText.titleMedium(
"Add Employee",
fontWeight: 600,
fontSize: 18,
),
],
),
MySpacing.height(24),
Form(
key: controller.basicValidator.formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildLabel("First Name"),
MySpacing.height(8),
_buildTextField(
hint: "eg: John",
controller: controller.basicValidator
.getController('first_name')!,
validator: controller.basicValidator
.getValidation('first_name'),
keyboardType: TextInputType.name,
),
MySpacing.height(24),
_buildLabel("Last Name"),
MySpacing.height(8),
_buildTextField(
hint: "eg: Doe",
controller: controller.basicValidator
.getController('last_name')!,
validator: controller.basicValidator
.getValidation('last_name'),
keyboardType: TextInputType.name,
),
MySpacing.height(24),
_buildLabel("Phone Number"),
MySpacing.height(8),
_buildTextField(
hint: "eg: +91 9876543210",
controller: controller.basicValidator
.getController('phone_number')!,
validator: controller.basicValidator
.getValidation('phone_number'),
keyboardType: TextInputType.phone,
),
MySpacing.height(24),
_buildLabel("Select Gender"),
MySpacing.height(8),
DropdownButtonFormField<Gender>(
value: controller.selectedGender,
dropdownColor: contentTheme.background,
isDense: true,
menuMaxHeight: 200,
decoration: _inputDecoration("Select Gender"),
icon: const Icon(Icons.expand_more, size: 20),
items: Gender.values
.map(
(gender) => DropdownMenuItem<Gender>(
value: gender,
child: MyText.labelMedium(
gender.name.capitalizeFirst!),
),
)
.toList(),
onChanged: controller.onGenderSelected,
),
MySpacing.height(24),
_buildLabel("Select Role"),
MySpacing.height(8),
DropdownButtonFormField<String>(
value: controller.selectedRoleId,
dropdownColor: contentTheme.background,
isDense: true,
decoration: _inputDecoration("Select Role"),
icon: const Icon(Icons.expand_more, size: 20),
items: controller.roles
.map(
(role) => DropdownMenuItem<String>(
value: role['id'],
child: Text(role['name']),
),
)
.toList(),
onChanged: controller.onRoleSelected,
),
MySpacing.height(24),
// Buttons row
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
MyButton.text(
onPressed: () => Navigator.pop(context),
padding: MySpacing.xy(20, 16),
child: MyText.bodySmall("Cancel"),
),
MySpacing.width(12),
MyButton(
onPressed: () async {
if (controller.basicValidator.validateForm()) {
final success =
await controller.createEmployees();
if (success) {
// Call refresh logic here via callback or GetX
final employeeController =
Get.find<EmployeesScreenController>();
final projectId =
employeeController.selectedProjectId;
if (projectId == null) {
await employeeController
.fetchAllEmployees();
} else {
await employeeController
.fetchEmployeesByProject(projectId);
}
employeeController
.update(['employee_screen_controller']);
// Optionally clear form
controller.basicValidator.getController("first_name")?.clear();
controller.basicValidator.getController("last_name")?.clear();
controller.basicValidator.getController("phone_number")?.clear();
controller.selectedGender = null;
controller.selectedRoleId = null;
controller.update();
}
}
},
elevation: 0,
padding: MySpacing.xy(20, 16),
backgroundColor: Colors.blueAccent,
borderRadiusAll: AppStyle.buttonRadius.medium,
child: MyText.bodySmall("Save",
color: contentTheme.onPrimary),
),
],
),
],
),
),
],
),
),
);
},
);
}
Widget _buildLabel(String text) => MyText.labelMedium(text);
Widget _buildTextField({
required TextEditingController controller,
required String? Function(String?)? validator,
required String hint,
TextInputType keyboardType = TextInputType.text,
}) {
return TextFormField(
controller: controller,
validator: validator,
keyboardType: keyboardType,
decoration: _inputDecoration(hint),
);
}
InputDecoration _inputDecoration(String hint) {
return InputDecoration(
hintText: hint,
hintStyle: MyTextStyle.bodySmall(xMuted: true),
border: outlineInputBorder,
enabledBorder: outlineInputBorder,
focusedBorder: focusedInputBorder,
contentPadding: MySpacing.all(16),
isCollapsed: true,
floatingLabelBehavior: FloatingLabelBehavior.never,
);
}
}

View File

@ -0,0 +1,215 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:marco/helpers/widgets/my_spacing.dart';
import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/helpers/widgets/avatar.dart';
import 'package:intl/intl.dart';
import 'package:marco/controller/dashboard/employees_screen_controller.dart';
class EmployeeDetailBottomSheet extends StatefulWidget {
final String employeeId;
const EmployeeDetailBottomSheet({super.key, required this.employeeId});
@override
State<EmployeeDetailBottomSheet> createState() =>
_EmployeeDetailBottomSheetState();
}
class _EmployeeDetailBottomSheetState extends State<EmployeeDetailBottomSheet> {
final EmployeesScreenController controller =
Get.put(EmployeesScreenController());
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
controller.fetchEmployeeDetails(widget.employeeId);
});
}
@override
void dispose() {
controller.selectedEmployeeDetails.value = null;
super.dispose();
}
String _getDisplayValue(dynamic value) {
if (value == null || value.toString().trim().isEmpty || value == 'null') {
return 'NA';
}
return value.toString();
}
String _formatDate(DateTime? date) {
if (date == null || date == DateTime(1)) return 'NA';
try {
return DateFormat('d/M/yyyy').format(date);
} catch (_) {
return 'NA';
}
}
Widget _buildLabelValueRow(IconData icon, String label, String value) {
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, size: 20, color: Colors.grey[700]),
MySpacing.width(8),
MyText.bodyMedium('$label:', fontWeight: 600),
MySpacing.width(8),
Expanded(child: MyText.bodyMedium(value, fontWeight: 400)),
],
),
);
}
@override
Widget build(BuildContext context) {
return DraggableScrollableSheet(
expand: false,
maxChildSize: 0.85,
minChildSize: 0.4,
initialChildSize: 0.6,
builder: (_, controllerScroll) {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
boxShadow: const [
BoxShadow(
color: Colors.black26,
blurRadius: 10,
offset: Offset(0, -3),
),
],
),
child: Obx(() {
if (controller.isLoadingEmployeeDetails.value) {
return const Center(child: CircularProgressIndicator());
}
final employee = controller.selectedEmployeeDetails.value;
if (employee == null) {
return const Center(child: Text('No employee details found.'));
}
return SingleChildScrollView(
controller: controllerScroll,
padding: MySpacing.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Drag Handle
Container(
width: 50,
height: 5,
decoration: BoxDecoration(
color: Colors.grey[400],
borderRadius: BorderRadius.circular(3),
),
),
MySpacing.height(20),
CircleAvatar(
radius: 40,
backgroundColor: Colors.blueGrey[200],
child: Avatar(
firstName: employee.firstName,
lastName: employee.lastName,
size: 60,
),
),
MySpacing.height(12),
MyText.titleLarge(
'${employee.firstName} ${employee.lastName}',
fontWeight: 700,
textAlign: TextAlign.center,
),
if (employee.jobRole.trim().isNotEmpty &&
employee.jobRole != 'null')
Padding(
padding: const EdgeInsets.only(top: 6),
child: Chip(
label: Text(
employee.jobRole,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w600,
),
),
backgroundColor: Colors.blueAccent,
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 6),
),
),
MySpacing.height(10),
// Contact Info Card
_buildInfoCard('Contact Information', [
_buildLabelValueRow(
Icons.email, 'Email', _getDisplayValue(employee.email)),
_buildLabelValueRow(Icons.phone, 'Phone Number',
_getDisplayValue(employee.phoneNumber)),
]),
MySpacing.height(10),
// Emergency Contact Info Card
_buildInfoCard('Emergency Contact', [
_buildLabelValueRow(Icons.person, 'Contact Person',
_getDisplayValue(employee.emergencyContactPerson)),
_buildLabelValueRow(Icons.phone_android, 'Contact Number',
_getDisplayValue(employee.emergencyPhoneNumber)),
]),
MySpacing.height(10),
// Personal Info Card
_buildInfoCard('Personal Information', [
_buildLabelValueRow(Icons.transgender, 'Gender',
_getDisplayValue(employee.gender)),
_buildLabelValueRow(Icons.cake, 'Birth Date',
_formatDate(employee.birthDate)),
_buildLabelValueRow(Icons.work, 'Joining Date',
_formatDate(employee.joiningDate)),
]),
MySpacing.height(10),
// Address Card
_buildInfoCard('Address', [
_buildLabelValueRow(Icons.home, 'Current Address',
_getDisplayValue(employee.currentAddress)),
_buildLabelValueRow(Icons.home_filled, 'Permanent Address',
_getDisplayValue(employee.permanentAddress)),
]),
],
),
);
}),
);
},
);
}
Widget _buildInfoCard(String title, List<Widget> children) {
return Card(
elevation: 1,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.titleMedium(title, fontWeight: 700),
MySpacing.height(12),
...children,
],
),
),
);
}
}

View File

@ -0,0 +1,106 @@
class EmployeeDetailsModel {
final String id;
final String firstName;
final String lastName;
final String? middleName;
final String? email;
final String gender;
final DateTime? birthDate;
final DateTime? joiningDate;
final String? permanentAddress;
final String? currentAddress;
final String phoneNumber;
final String? emergencyPhoneNumber;
final String? emergencyContactPerson;
final String? aadharNumber;
final bool isActive;
final String? panNumber;
final String? photo;
final String? applicationUserId;
final String jobRoleId;
final bool isSystem;
final String jobRole;
EmployeeDetailsModel({
required this.id,
required this.firstName,
required this.lastName,
this.middleName,
this.email,
required this.gender,
this.birthDate,
this.joiningDate,
this.permanentAddress,
this.currentAddress,
required this.phoneNumber,
this.emergencyPhoneNumber,
this.emergencyContactPerson,
this.aadharNumber,
required this.isActive,
this.panNumber,
this.photo,
this.applicationUserId,
required this.jobRoleId,
required this.isSystem,
required this.jobRole,
});
factory EmployeeDetailsModel.fromJson(Map<String, dynamic> json) {
return EmployeeDetailsModel(
id: json['id'],
firstName: json['firstName'],
lastName: json['lastName'],
middleName: json['middleName'],
email: json['email'],
gender: json['gender'],
birthDate: _parseDate(json['birthDate']),
joiningDate: _parseDate(json['joiningDate']),
permanentAddress: json['permanentAddress'],
currentAddress: json['currentAddress'],
phoneNumber: json['phoneNumber'],
emergencyPhoneNumber: json['emergencyPhoneNumber'],
emergencyContactPerson: json['emergencyContactPerson'],
aadharNumber: json['aadharNumber'],
isActive: json['isActive'],
panNumber: json['panNumber'],
photo: json['photo'],
applicationUserId: json['applicationUserId'],
jobRoleId: json['jobRoleId'],
isSystem: json['isSystem'],
jobRole: json['jobRole'],
);
}
static DateTime? _parseDate(String? dateStr) {
if (dateStr == null || dateStr == "0001-01-01T00:00:00") {
return null;
}
return DateTime.tryParse(dateStr);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'firstName': firstName,
'lastName': lastName,
'middleName': middleName,
'email': email,
'gender': gender,
'birthDate': birthDate?.toIso8601String(),
'joiningDate': joiningDate?.toIso8601String(),
'permanentAddress': permanentAddress,
'currentAddress': currentAddress,
'phoneNumber': phoneNumber,
'emergencyPhoneNumber': emergencyPhoneNumber,
'emergencyContactPerson': emergencyContactPerson,
'aadharNumber': aadharNumber,
'isActive': isActive,
'panNumber': panNumber,
'photo': photo,
'applicationUserId': applicationUserId,
'jobRoleId': jobRoleId,
'isSystem': isSystem,
'jobRole': jobRole,
};
}
}

View File

@ -0,0 +1,171 @@
import 'package:flutter/material.dart';
import 'package:marco/controller/permission_controller.dart';
import 'package:marco/controller/dashboard/employees_screen_controller.dart';
import 'package:marco/helpers/widgets/my_text.dart';
class EmployeesScreenFilterSheet extends StatelessWidget {
final EmployeesScreenController controller;
final PermissionController permissionController;
const EmployeesScreenFilterSheet({
super.key,
required this.controller,
required this.permissionController,
});
@override
Widget build(BuildContext context) {
String? tempSelectedProjectId;
bool showProjectList = false;
final accessibleProjects = controller.projects
.where((project) =>
permissionController.isUserAssignedToProject(project.id.toString()))
.toList();
return StatefulBuilder(builder: (context, setState) {
List<Widget> filterWidgets;
if (showProjectList) {
filterWidgets = [];
// Add "All Employees" option at the top
filterWidgets.add(
ListTile(
dense: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
title: MyText.titleSmall('All Employees'),
trailing:
tempSelectedProjectId == null ? const Icon(Icons.check) : null,
onTap: () {
setState(() {
tempSelectedProjectId = null;
showProjectList = false;
});
},
),
);
// Add all accessible projects below
if (accessibleProjects.isEmpty) {
filterWidgets.add(
Padding(
padding: const EdgeInsets.all(12.0),
child: Center(
child: MyText.titleSmall(
'No Projects Assigned',
fontWeight: 600,
),
),
),
);
} else {
filterWidgets.addAll(accessibleProjects.map((project) {
return ListTile(
dense: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
title: MyText.titleSmall(project.name),
trailing: tempSelectedProjectId == project.id.toString()
? const Icon(Icons.check)
: null,
onTap: () {
setState(() {
tempSelectedProjectId = project.id.toString();
showProjectList = false;
});
},
);
}).toList());
}
} else {
String selectedProjectName = 'All Employees';
if (tempSelectedProjectId != null) {
final selectedProject = accessibleProjects.isNotEmpty
? accessibleProjects.firstWhere(
(p) => p.id.toString() == tempSelectedProjectId,
orElse: () => accessibleProjects[0],
)
: null;
if (selectedProject != null) {
selectedProjectName = selectedProject.name;
}
}
filterWidgets = [
Padding(
padding: EdgeInsets.fromLTRB(16, 12, 16, 4),
child: Align(
alignment: Alignment.centerLeft,
child: MyText.titleSmall(
'Select Project',
fontWeight: 600,
),
),
),
ListTile(
dense: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
title: MyText.titleSmall(selectedProjectName),
trailing: const Icon(Icons.arrow_drop_down),
onTap: () => setState(() => showProjectList = true),
),
];
}
return Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.only(top: 12, bottom: 8),
child: Center(
child: Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: Colors.grey[400],
borderRadius: BorderRadius.circular(4),
),
),
),
),
...filterWidgets,
const Divider(),
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: const Color.fromARGB(255, 95, 132, 255),
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: MyText.titleSmall(
'Apply Filter',
fontWeight: 600,
color: Colors.white,
),
onPressed: () {
Navigator.pop(context, {
'projectId': tempSelectedProjectId,
});
},
),
),
),
],
),
),
);
});
}
}

View File

@ -18,6 +18,9 @@ import 'package:marco/view/dashboard/daily_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/dashboard/Attendence/attendance_screen.dart';
import 'package:marco/view/taskPlaning/daily_task_planing.dart';
import 'package:marco/view/taskPlaning/daily_progress.dart';
import 'package:marco/view/employees/employees_screen.dart';
class AuthMiddleware extends GetMiddleware {
@override
@ -32,7 +35,6 @@ getPageRoute() {
name: '/',
page: () => AttendanceScreen(),
middlewares: [AuthMiddleware()]),
// Dashboard
GetPage(
name: '/dashboard/attendance',
@ -44,7 +46,7 @@ getPageRoute() {
middlewares: [AuthMiddleware()]),
GetPage(
name: '/dashboard/employees',
page: () => EmployeeScreen(),
page: () => EmployeesScreen(),
middlewares: [AuthMiddleware()]),
// Employees Creation
GetPage(
@ -56,6 +58,14 @@ getPageRoute() {
name: '/dashboard/daily-task',
page: () => DailyTaskScreen(),
middlewares: [AuthMiddleware()]),
GetPage(
name: '/dashboard/daily-task-planing',
page: () => DailyTaskPlaningScreen(),
middlewares: [AuthMiddleware()]),
GetPage(
name: '/dashboard/daily-task-progress',
page: () => DailyProgressReportScreen(),
middlewares: [AuthMiddleware()]),
GetPage(
name: '/daily-task/report-task',
page: () => ReportTaskScreen(),

View File

@ -181,7 +181,6 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
),
],
),
MySpacing.height(flexSpacing),
Padding(
padding: MySpacing.x(flexSpacing / 2),
child: MyFlex(children: [
@ -394,6 +393,7 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: MyText.titleMedium(
@ -401,6 +401,29 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
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,
);
}),
],
),
),

View File

@ -200,7 +200,7 @@ Widget _buildDateRangeButton() {
Map<String, List<dynamic>> groupedTasks = {};
for (var task in dailyTaskController.dailyTasks) {
String dateKey =
DateFormat('dd-MM-yyyy').format(DateTime.parse(task.assignmentDate));
DateFormat('dd-MM-yyyy').format(task.assignmentDate);
groupedTasks.putIfAbsent(dateKey, () => []).add(task);
}

View File

@ -1,115 +1,111 @@
import 'package:flutter/material.dart';
import 'package:flutter_lucide/flutter_lucide.dart';
import 'package:get/get.dart';
import 'package:marco/helpers/theme/app_theme.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
import 'package:marco/helpers/utils/my_shadow.dart';
import 'package:marco/helpers/widgets/my_breadcrumb.dart';
import 'package:marco/helpers/widgets/my_breadcrumb_item.dart';
import 'package:marco/helpers/widgets/my_card.dart';
import 'package:marco/helpers/widgets/my_container.dart';
import 'package:marco/helpers/widgets/my_spacing.dart';
import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/view/layouts/layout.dart';
class DashboardScreen extends StatelessWidget with UIMixin {
DashboardScreen({super.key});
class DashboardScreen extends StatefulWidget {
const DashboardScreen({super.key});
static const String dashboardRoute = "/dashboard";
static const String employeesRoute = "/dashboard/employees";
static const String projectsRoute = "/dashboard";
static const String attendanceRoute = "/dashboard/attendance";
static const String tasksRoute = "/dashboard/daily-task";
static const String dailyTasksRoute = "/dashboard/daily-task-planing";
static const String dailyTasksProgressRoute =
"/dashboard/daily-task-progress";
@override
State<DashboardScreen> createState() => _DashboardScreenState();
}
class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
@override
Widget build(BuildContext context) {
return Layout(
child: SingleChildScrollView(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: MySpacing.x(flexSpacing),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
MyText.titleMedium("Dashboard", fontSize: 18, fontWeight: 600),
MyBreadcrumb(children: [MyBreadcrumbItem(name: 'Dashboard')]),
MyText.titleMedium("Dashboard", fontWeight: 600),
MySpacing.height(12),
_buildDashboardStats(),
],
),
),
MySpacing.height(flexSpacing),
Padding(
padding: MySpacing.x(flexSpacing / 2),
child: Column(
children: _buildDashboardStats(),
),
),
],
),
);
}
List<Widget> _buildDashboardStats() {
Widget _buildDashboardStats() {
final stats = [
_StatItem(
LucideIcons.gauge, "Dashboard", contentTheme.primary, dashboardRoute),
_StatItem(LucideIcons.scan_face, "Attendance", contentTheme.success,
attendanceRoute),
_StatItem(
LucideIcons.users, "Employees", contentTheme.warning, employeesRoute),
_StatItem(LucideIcons.logs, "Daily Progress Report", contentTheme.info,
tasksRoute),
DashboardScreen.attendanceRoute),
_StatItem(LucideIcons.users, "Employees", contentTheme.warning,
DashboardScreen.employeesRoute),
_StatItem(LucideIcons.logs, "Daily Task Planing", contentTheme.info,
tasksRoute),
_StatItem(LucideIcons.folder, "Projects", contentTheme.secondary,
projectsRoute),
DashboardScreen.dailyTasksRoute),
_StatItem(LucideIcons.list_todo, "Daily Task Progress", contentTheme.info,
DashboardScreen.dailyTasksProgressRoute),
];
return List.generate(
(stats.length / 2).ceil(),
(index) => Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildStatCard(stats[index * 2]),
if (index * 2 + 1 < stats.length)
_buildStatCard(stats[index * 2 + 1]),
],
),
return LayoutBuilder(
builder: (context, constraints) {
double maxWidth = constraints.maxWidth;
int crossAxisCount =
(maxWidth / 100).floor().clamp(2, 4);
double cardWidth =
(maxWidth - (crossAxisCount - 1) * 10) / crossAxisCount;
return Wrap(
spacing: 10,
runSpacing: 10,
children:
stats.map((stat) => _buildStatCard(stat, cardWidth)).toList(),
);
},
);
}
Widget _buildStatCard(_StatItem statItem) {
return Expanded(
child: InkWell(
Widget _buildStatCard(_StatItem statItem, double width) {
return InkWell(
onTap: () => Get.toNamed(statItem.route),
borderRadius: BorderRadius.circular(10),
child: MyCard.bordered(
width: width,
height: 100,
paddingAll: 5,
borderRadiusAll: 10,
border: Border.all(color: Colors.grey.withOpacity(0.2)),
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
paddingAll: 24,
height: 140,
border: Border.all(color: Colors.grey.withOpacity(0.15)),
shadow: MyShadow(elevation: 1.5, position: MyShadowPosition.bottom),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildStatCardIcon(statItem),
MySpacing.height(12),
MyText.labelSmall(statItem.title, maxLines: 1),
],
MySpacing.height(8),
MyText.labelSmall(
statItem.title,
maxLines: 2,
overflow: TextOverflow.visible,
textAlign: TextAlign.center,
),
],
),
),
);
}
Widget _buildStatCardIcon(_StatItem statItem) {
return MyContainer(
paddingAll: 16,
color: statItem.color.withOpacity(0.2),
child: MyContainer(
paddingAll: 8,
color: statItem.color,
child: Icon(statItem.icon, size: 16, color: contentTheme.light),
),
return MyContainer.rounded(
paddingAll: 10,
color: statItem.color.withOpacity(0.1),
child: Icon(statItem.icon, size: 18, color: statItem.color),
);
}
}
@ -118,7 +114,7 @@ class _StatItem {
final IconData icon;
final String title;
final Color color;
final String route; // New field to store the route for each stat item
final String route;
_StatItem(this.icon, this.title, this.color, this.route);
}

View File

@ -0,0 +1,327 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:marco/helpers/theme/app_theme.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
import 'package:marco/helpers/utils/my_shadow.dart';
import 'package:marco/helpers/widgets/my_breadcrumb.dart';
import 'package:marco/helpers/widgets/my_breadcrumb_item.dart';
import 'package:marco/helpers/widgets/my_card.dart';
import 'package:marco/helpers/widgets/my_spacing.dart';
import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/view/layouts/layout.dart';
import 'package:marco/controller/permission_controller.dart';
import 'package:marco/model/employees/employees_screen_filter_sheet.dart';
import 'package:marco/model/employees/add_employee_bottom_sheet.dart';
import 'package:marco/controller/dashboard/employees_screen_controller.dart';
import 'package:marco/helpers/widgets/avatar.dart';
import 'package:marco/model/employees/employee_detail_bottom_sheet.dart';
class EmployeesScreen extends StatefulWidget {
const EmployeesScreen({super.key});
@override
State<EmployeesScreen> createState() => _EmployeesScreenState();
}
class _EmployeesScreenState extends State<EmployeesScreen> with UIMixin {
final EmployeesScreenController employeeScreenController =
Get.put(EmployeesScreenController());
final PermissionController permissionController =
Get.put(PermissionController());
Future<void> _openFilterSheet() async {
final result = await showModalBottomSheet<Map<String, dynamic>>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.white,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
),
builder: (context) => EmployeesScreenFilterSheet(
controller: employeeScreenController,
permissionController: permissionController,
),
);
if (result != null) {
final selectedProjectId = result['projectId'] as String?;
if (selectedProjectId != employeeScreenController.selectedProjectId) {
employeeScreenController.selectedProjectId = selectedProjectId;
try {
if (selectedProjectId == null) {
await employeeScreenController.fetchAllEmployees();
} else {
await employeeScreenController
.fetchEmployeesByProject(selectedProjectId);
}
} catch (e) {
debugPrint('Error fetching employees: ${e.toString()}');
}
employeeScreenController.update(['employee_screen_controller']);
}
}
}
Future<void> _refreshEmployees() async {
try {
final projectId = employeeScreenController.selectedProjectId;
if (projectId == null) {
await employeeScreenController.fetchAllEmployees();
} else {
await employeeScreenController.fetchEmployeesByProject(projectId);
}
} catch (e) {
debugPrint('Error refreshing employee data: ${e.toString()}');
}
}
@override
Widget build(BuildContext context) {
return Layout(
floatingActionButton: InkWell(
onTap: () async {
final result = await showModalBottomSheet<bool>(
context: context,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
backgroundColor: Colors.transparent,
builder: (context) => AddEmployeeBottomSheet(),
);
if (result == true) {
await _refreshEmployees();
}
},
borderRadius: BorderRadius.circular(28),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: Colors.blueAccent,
borderRadius: BorderRadius.circular(28),
boxShadow: const [
BoxShadow(
color: Colors.black26,
blurRadius: 6,
offset: Offset(0, 3),
),
],
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.add, color: Colors.white),
SizedBox(width: 8),
Text('Add New Employee', style: TextStyle(color: Colors.white)),
],
),
),
),
child: GetBuilder<EmployeesScreenController>(
init: employeeScreenController,
tag: 'employee_screen_controller',
builder: (controller) {
return Stack(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: MySpacing.x(flexSpacing),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
MyText.titleMedium(
"Employees",
fontSize: 18,
fontWeight: 600,
),
MyBreadcrumb(
children: [
MyBreadcrumbItem(name: 'Dashboard'),
MyBreadcrumbItem(name: 'Employees', active: true),
],
),
],
),
),
MySpacing.height(flexSpacing),
Padding(
padding: MySpacing.x(flexSpacing),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
MyText.bodyMedium(
"Filter",
fontWeight: 600,
),
Tooltip(
message: 'Project',
child: InkWell(
borderRadius: BorderRadius.circular(24),
onTap: _openFilterSheet,
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: const Padding(
padding: EdgeInsets.all(8),
child: Icon(
Icons.filter_list_alt,
color: Colors.blueAccent,
size: 28,
),
),
),
),
),
const SizedBox(width: 8),
MyText.bodyMedium(
"Refresh",
fontWeight: 600,
),
Tooltip(
message: 'Refresh Data',
child: InkWell(
borderRadius: BorderRadius.circular(24),
onTap: _refreshEmployees,
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: const Padding(
padding: EdgeInsets.all(8),
child: Icon(
Icons.refresh,
color: Colors.green,
size: 28,
),
),
),
),
),
],
),
),
Padding(
padding: MySpacing.x(flexSpacing),
child: dailyProgressReportTab(),
),
],
),
],
);
},
),
);
}
Widget dailyProgressReportTab() {
return Obx(() {
final isLoading = employeeScreenController.isLoading.value;
final employees = employeeScreenController.employees;
if (isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (employees.isEmpty) {
return Center(
child: MyText.bodySmall(
"No Assigned Employees Found",
fontWeight: 600,
),
);
}
return SingleChildScrollView(
padding: const EdgeInsets.only(bottom: 80),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: employees.map((employee) {
return InkWell(
onTap: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) =>
EmployeeDetailBottomSheet(employeeId: employee.id),
);
},
child: MyCard.bordered(
borderRadiusAll: 12,
paddingAll: 10,
margin: MySpacing.bottom(12),
shadow: MyShadow(elevation: 3),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Avatar(
firstName: employee.firstName,
lastName: employee.lastName,
size: 41,
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
MyText.titleMedium(
employee.name,
fontWeight: 600,
),
const SizedBox(width: 6),
MyText.titleSmall(
'(${employee.jobRole})',
fontWeight: 400,
),
],
),
const SizedBox(height: 8),
if (employee.email.isNotEmpty &&
employee.email != '-') ...[
Row(
children: [
const Icon(Icons.email,
size: 16, color: Colors.red),
const SizedBox(width: 4),
Flexible(
child: MyText.titleSmall(
employee.email,
fontWeight: 400,
overflow: TextOverflow.ellipsis,
),
),
],
),
const SizedBox(height: 8),
],
Row(
children: [
const Icon(Icons.phone,
size: 16, color: Colors.blueAccent),
const SizedBox(width: 4),
MyText.titleSmall(
employee.phoneNumber,
fontWeight: 400,
),
],
),
],
),
),
],
)
],
),
));
}).toList(),
),
);
});
}
}

View File

@ -21,13 +21,14 @@ import 'package:marco/helpers/widgets/avatar.dart';
class Layout extends StatelessWidget {
final Widget? child;
final Widget? floatingActionButton;
final LayoutController controller = LayoutController();
final topBarTheme = AdminTheme.theme.topBarTheme;
final contentTheme = AdminTheme.theme.contentTheme;
final EmployeeInfo? employeeInfo = LocalStorage.getEmployeeInfo();
Layout({super.key, this.child});
Layout({super.key, this.child, this.floatingActionButton});
@override
Widget build(BuildContext context) {
@ -50,6 +51,22 @@ class Layout extends StatelessWidget {
appBar: AppBar(
elevation: 0,
actions: [
MySpacing.width(6),
InkWell(
onTap: () {
Get.toNamed('/dashboard');
},
borderRadius: BorderRadius.circular(6),
splashColor: contentTheme.primary.withAlpha(20),
child: Padding(
padding: MySpacing.xy(8, 8),
child: Icon(
LucideIcons.layout_dashboard,
size: 18,
color: Colors.blueAccent,
),
),
),
MySpacing.width(8),
CustomPopupMenu(
backdrop: true,
@ -71,9 +88,9 @@ class Layout extends StatelessWidget {
backdrop: true,
onChange: (_) {},
offsetX: -90,
offsetY: 4,
offsetY: 0,
menu: Padding(
padding: MySpacing.xy(8, 8),
padding: MySpacing.xy(0, 8),
child: MyContainer.rounded(
paddingAll: 0,
child: Avatar(
@ -88,6 +105,7 @@ class Layout extends StatelessWidget {
],
),
drawer: LeftBar(),
floatingActionButton: floatingActionButton,
body: SingleChildScrollView(
key: controller.scrollKey,
child: child,
@ -99,6 +117,7 @@ class Layout extends StatelessWidget {
return Scaffold(
key: controller.scaffoldKey,
endDrawer: RightBar(),
floatingActionButton: floatingActionButton,
body: Row(
children: [
LeftBar(isCondensed: ThemeCustomizer.instance.leftBarCondensed),
@ -111,14 +130,16 @@ class Layout extends StatelessWidget {
left: 0,
bottom: 0,
child: SingleChildScrollView(
padding: MySpacing.fromLTRB(0, 58 + flexSpacing, 0, flexSpacing),
padding:
MySpacing.fromLTRB(0, 58 + flexSpacing, 0, flexSpacing),
key: controller.scrollKey,
child: child,
),
),
Positioned(top: 0, left: 0, right: 0, child: TopBar()),
],
)),
),
),
],
),
);
@ -128,7 +149,11 @@ class Layout extends StatelessWidget {
Widget buildNotification(String title, String description) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [MyText.labelLarge(title), MySpacing.height(4), MyText.bodySmall(description)],
children: [
MyText.labelLarge(title),
MySpacing.height(4),
MyText.bodySmall(description)
],
);
}
@ -142,19 +167,23 @@ class Layout extends StatelessWidget {
padding: MySpacing.xy(16, 12),
child: MyText.titleMedium("Notification", fontWeight: 600),
),
MyDashedDivider(height: 1, color: theme.dividerColor, dashSpace: 4, dashWidth: 6),
MyDashedDivider(
height: 1, color: theme.dividerColor, dashSpace: 4, dashWidth: 6),
Padding(
padding: MySpacing.xy(16, 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
buildNotification("Welcome to Marco", "Welcome to Marco, we are glad to have you here"),
buildNotification("Welcome to Marco",
"Welcome to Marco, we are glad to have you here"),
MySpacing.height(12),
buildNotification("New update available", "There is a new update available for your app"),
buildNotification("New update available",
"There is a new update available for your app"),
],
),
),
MyDashedDivider(height: 1, color: theme.dividerColor, dashSpace: 4, dashWidth: 6),
MyDashedDivider(
height: 1, color: theme.dividerColor, dashSpace: 4, dashWidth: 6),
Padding(
padding: MySpacing.xy(16, 0),
child: Row(
@ -241,6 +270,28 @@ class Layout extends StatelessWidget {
],
),
),
MyButton(
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
onPressed: () => {Get.offNamed('/auth/login')},
borderRadiusAll: AppStyle.buttonRadius.medium,
padding: MySpacing.xy(8, 4),
splashColor: contentTheme.onBackground.withAlpha(20),
backgroundColor: Colors.transparent,
child: Row(
children: [
Icon(
LucideIcons.log_out,
size: 14,
color: contentTheme.onBackground,
),
MySpacing.width(8),
MyText.labelMedium(
"Logout",
fontWeight: 600,
)
],
),
),
],
),
),

View File

@ -1,4 +1,3 @@
// All import statements remain unchanged
import 'package:marco/helpers/theme/theme_customizer.dart';
import 'package:marco/helpers/services/url_service.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
@ -81,7 +80,7 @@ class _LeftBarState extends State<LeftBar>
children: [
Center(
child: Padding(
padding: MySpacing.y(12),
padding: MySpacing.fromLTRB(0, 24, 0, 0),
child: InkWell(
onTap: () => Get.toNamed('/home'),
child: Image.asset(
@ -115,7 +114,7 @@ class _LeftBarState extends State<LeftBar>
isCondensed: isCondensed,
route: '/dashboard'),
NavigationItem(
iconData: LucideIcons.layout_template,
iconData: LucideIcons.scan_face,
title: "Attendance",
isCondensed: isCondensed,
route: '/dashboard/attendance'),
@ -125,15 +124,15 @@ class _LeftBarState extends State<LeftBar>
isCondensed: isCondensed,
route: '/dashboard/employees'),
NavigationItem(
iconData: LucideIcons.list,
title: "Daily Progress Report",
isCondensed: isCondensed,
route: '/dashboard/daily-task'),
NavigationItem(
iconData: LucideIcons.list_todo,
iconData: LucideIcons.logs,
title: "Daily Task Planing",
isCondensed: isCondensed,
route: '/dashboard/daily-task-planing'),
NavigationItem(
iconData: LucideIcons.list_todo,
title: "Daily Progress Report",
isCondensed: isCondensed,
route: '/dashboard/daily-task-progress'),
],
),
),
@ -148,9 +147,7 @@ class _LeftBarState extends State<LeftBar>
Widget userInfoSection() {
if (employeeInfo == null) {
return Center(
child:
CircularProgressIndicator()); // Show loading indicator if employeeInfo is not yet loaded.
return Center(child: CircularProgressIndicator());
}
return Padding(
@ -245,9 +242,7 @@ class _MenuWidgetState extends State<MenuWidget>
}
void onChangeMenuActive(String key) {
if (key != widget.title) {
// onChangeExpansion(false);
}
if (key != widget.title) {}
}
void onChangeExpansion(value) {
@ -271,22 +266,16 @@ class _MenuWidgetState extends State<MenuWidget>
if (hideFn != null) {
hideFn!();
}
// popupShowing = false;
}
@override
Widget build(BuildContext context) {
// var route = Uri.base.fragment;
// isActive = widget.children.any((element) => element.route == route);
if (widget.isCondensed) {
return CustomPopupMenu(
backdrop: true,
show: popupShowing,
hideFn: (hide) => hideFn = hide,
onChange: (_) {
// popupShowing = _;
},
onChange: (_) {},
placement: CustomPopupMenuPlacement.right,
menu: MouseRegion(
cursor: SystemMouseCursors.click,
@ -407,7 +396,6 @@ class _MenuWidgetState extends State<MenuWidget>
void dispose() {
_controller.dispose();
super.dispose();
// LeftbarObserver.detachListener(widget.title);
}
}

View File

@ -0,0 +1,602 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:marco/helpers/theme/app_theme.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
import 'package:marco/helpers/utils/my_shadow.dart';
import 'package:marco/helpers/widgets/my_breadcrumb.dart';
import 'package:marco/helpers/widgets/my_breadcrumb_item.dart';
import 'package:marco/helpers/widgets/my_card.dart';
import 'package:marco/helpers/widgets/my_container.dart';
import 'package:marco/helpers/widgets/my_spacing.dart';
import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/view/layouts/layout.dart';
import 'package:marco/controller/permission_controller.dart';
import 'package:marco/controller/dashboard/daily_task_controller.dart';
import 'package:marco/model/dailyTaskPlaning/daily_progress_report_filter.dart';
import 'package:marco/helpers/widgets/avatar.dart';
import 'package:marco/model/dailyTaskPlaning/comment_task_bottom_sheet.dart';
import 'package:marco/model/dailyTaskPlaning/report_task_bottom_sheet.dart';
class DailyProgressReportScreen extends StatefulWidget {
const DailyProgressReportScreen({super.key});
@override
State<DailyProgressReportScreen> createState() =>
_DailyProgressReportScreenState();
}
class TaskChartData {
final String label;
final num value;
final Color color;
TaskChartData(this.label, this.value, this.color);
}
class _DailyProgressReportScreenState extends State<DailyProgressReportScreen>
with UIMixin {
final DailyTaskController dailyTaskController =
Get.put(DailyTaskController());
final PermissionController permissionController =
Get.put(PermissionController());
@override
Widget build(BuildContext context) {
return Layout(
child: GetBuilder<DailyTaskController>(
init: dailyTaskController,
tag: 'daily_progress_report_controller',
builder: (controller) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
MySpacing.height(flexSpacing),
_buildActionBar(),
Padding(
padding: MySpacing.x(flexSpacing),
child: _buildDailyProgressReportTab(),
),
],
);
},
),
);
}
Widget _buildHeader() {
return Padding(
padding: MySpacing.x(flexSpacing),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
MyText.titleMedium("Daily Progress Report",
fontSize: 18, fontWeight: 600),
MyBreadcrumb(
children: [
MyBreadcrumbItem(name: 'Dashboard'),
MyBreadcrumbItem(name: 'Daily Progress Report', active: true),
],
),
],
),
);
}
Widget _buildActionBar() {
return Padding(
padding: MySpacing.x(flexSpacing),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
_buildActionItem(
label: "Filter",
icon: Icons.filter_list_alt,
tooltip: 'Filter Project',
color: Colors.blueAccent,
onTap: _openFilterSheet,
),
const SizedBox(width: 8),
_buildActionItem(
label: "Refresh",
icon: Icons.refresh,
tooltip: 'Refresh Data',
color: Colors.green,
onTap: _refreshData,
),
],
),
);
}
Widget _buildActionItem({
required String label,
required IconData icon,
required String tooltip,
required VoidCallback onTap,
required Color color,
}) {
return Row(
children: [
MyText.bodyMedium(label, fontWeight: 600),
Tooltip(
message: tooltip,
child: InkWell(
borderRadius: BorderRadius.circular(24),
onTap: onTap,
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(icon, color: color, size: 28),
),
),
),
),
],
);
}
Future<void> _openFilterSheet() async {
final result = await showModalBottomSheet<Map<String, dynamic>>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.white,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
),
builder: (context) => DailyProgressReportFilter(
controller: dailyTaskController,
permissionController: permissionController,
),
);
if (result != null) {
final selectedProjectId = result['projectId'] as String?;
if (selectedProjectId != null &&
selectedProjectId != dailyTaskController.selectedProjectId) {
dailyTaskController.selectedProjectId = selectedProjectId;
try {
await dailyTaskController.fetchProjects();
} catch (e) {
debugPrint('Error fetching projects: $e');
}
dailyTaskController.update(['daily_progress_report_controller']);
}
}
}
Future<void> _refreshData() async {
final projectId = dailyTaskController.selectedProjectId;
if (projectId != null) {
try {
await dailyTaskController.fetchTaskData(projectId);
} catch (e) {
debugPrint('Error refreshing task data: $e');
}
}
}
void _showTeamMembersBottomSheet(List<dynamic> members) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
isDismissible: true,
enableDrag: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
),
builder: (context) {
return GestureDetector(
onTap: () {},
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius:
const BorderRadius.vertical(top: Radius.circular(12)),
),
padding: const EdgeInsets.fromLTRB(16, 24, 16, 24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.titleMedium(
'Team Members',
fontWeight: 600,
),
const SizedBox(height: 8),
const Divider(thickness: 1),
const SizedBox(height: 8),
...members.map((member) {
final firstName = member.firstName ?? 'Unnamed';
final lastName = member.lastName ?? 'User';
return ListTile(
contentPadding: EdgeInsets.zero,
leading: Avatar(
firstName: firstName,
lastName: lastName,
size: 31,
),
title: MyText.bodyMedium(
'$firstName $lastName',
fontWeight: 600,
),
);
}),
const SizedBox(height: 8),
],
),
),
);
},
);
}
Widget _buildDailyProgressReportTab() {
return Obx(() {
final isLoading = dailyTaskController.isLoading.value;
final groupedTasks = dailyTaskController.groupedDailyTasks;
if (isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (groupedTasks.isEmpty) {
return Center(
child: MyText.bodySmall(
"No Progress Report Found",
fontWeight: 600,
),
);
}
final sortedDates = groupedTasks.keys.toList()
..sort((a, b) => b.compareTo(a));
return MyCard.bordered(
borderRadiusAll: 4,
border: Border.all(color: Colors.grey.withOpacity(0.2)),
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
paddingAll: 8,
child: ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: sortedDates.length,
separatorBuilder: (_, __) => Column(
children: [
const SizedBox(height: 12),
Divider(color: Colors.grey.withOpacity(0.3), thickness: 1),
const SizedBox(height: 12),
],
),
itemBuilder: (context, dateIndex) {
final dateKey = sortedDates[dateIndex];
final tasksForDate = groupedTasks[dateKey]!;
final date = DateTime.tryParse(dateKey);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: () => dailyTaskController.toggleDate(dateKey),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
MyText.bodyMedium(
date != null
? DateFormat('dd MMM yyyy').format(date)
: dateKey,
fontWeight: 700,
),
Obx(() => Icon(
dailyTaskController.expandedDates.contains(dateKey)
? Icons.remove_circle
: Icons.add_circle,
color: Colors.blueAccent,
)),
],
),
),
Obx(() {
if (!dailyTaskController.expandedDates.contains(dateKey)) {
return const SizedBox.shrink();
}
return Column(
children: tasksForDate.asMap().entries.map((entry) {
final task = entry.value;
final index = entry.key;
final activityName =
task.workItem?.activityMaster?.activityName ?? 'N/A';
final location = [
task.workItem?.workArea?.floor?.building?.name,
task.workItem?.workArea?.floor?.floorName,
task.workItem?.workArea?.areaName
].where((e) => e?.isNotEmpty ?? false).join(' > ');
final planned = task.plannedTask;
final completed = task.completedTask;
final progress = (planned != 0)
? (completed / planned).clamp(0.0, 1.0)
: 0.0;
return Column(
children: [
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: MyContainer(
paddingAll: 12,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.bodyMedium(activityName,
fontWeight: 600),
const SizedBox(height: 2),
MyText.bodySmall(location,
color: Colors.grey),
const SizedBox(height: 8),
GestureDetector(
onTap: () => _showTeamMembersBottomSheet(
task.teamMembers),
child: Row(
children: [
const Icon(Icons.group,
size: 18, color: Colors.blueAccent),
const SizedBox(width: 6),
MyText.bodyMedium('Team',
color: Colors.blueAccent,
fontWeight: 600),
],
),
),
const SizedBox(height: 8),
MyText.bodySmall(
"Completed: $completed / $planned",
fontWeight: 600,
color: Colors.black87,
),
const SizedBox(height: 6),
Stack(
children: [
Container(
height: 5,
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius:
BorderRadius.circular(6),
),
),
FractionallySizedBox(
widthFactor: progress,
child: Container(
height: 5,
decoration: BoxDecoration(
color: progress >= 1.0
? Colors.green
: progress >= 0.5
? Colors.amber
: Colors.red,
borderRadius:
BorderRadius.circular(6),
),
),
),
],
),
const SizedBox(height: 4),
MyText.bodySmall(
"${(progress * 100).toStringAsFixed(1)}%",
fontWeight: 500,
color: progress >= 1.0
? Colors.green[700]
: progress >= 0.5
? Colors.amber[800]
: Colors.red[700],
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (task.reportedDate == null ||
task.reportedDate.toString().isEmpty)
OutlinedButton.icon(
icon: const Icon(Icons.report,
size: 18,
color: Colors.blueAccent),
label: const Text('Report',
style: TextStyle(
color: Colors.blueAccent)),
style: OutlinedButton.styleFrom(
side: const BorderSide(
color: Colors.blueAccent),
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 8),
textStyle:
const TextStyle(fontSize: 14),
),
onPressed: () {
final activityName = task
.workItem
?.activityMaster
?.activityName ??
'N/A';
final assigned =
'${(task.plannedTask - completed)}';
final assignedBy =
"${task.assignedBy.firstName} ${task.assignedBy.lastName ?? ''}";
final assignedOn =
DateFormat('dd-MM-yyyy').format(
task.assignmentDate);
final taskId = task.id;
final location = [
task.workItem?.workArea?.floor
?.building?.name,
task.workItem?.workArea?.floor
?.floorName,
task.workItem?.workArea?.areaName,
]
.where((e) =>
e != null && e.isNotEmpty)
.join(' > ');
final teamMembers = task.teamMembers
.map((e) => e.firstName)
.toList();
final taskData = {
'activity': activityName,
'assigned': assigned,
'taskId': taskId,
'assignedBy': assignedBy,
'completed': completed,
'assignedOn': assignedOn,
'location': location,
'teamSize':
task.teamMembers.length,
'teamMembers': teamMembers,
};
showModalBottomSheet(
context: context,
isScrollControlled: true,
shape:
const RoundedRectangleBorder(
borderRadius:
BorderRadius.vertical(
top: Radius.circular(
16)),
),
builder: (_) => Padding(
padding: MediaQuery.of(context)
.viewInsets,
child: ReportTaskBottomSheet(
taskData: taskData),
),
);
},
),
const SizedBox(width: 8),
OutlinedButton.icon(
icon: const Icon(Icons.comment,
size: 18, color: Colors.blueAccent),
label: const Text('Comment',
style: TextStyle(
color: Colors.blueAccent)),
style: OutlinedButton.styleFrom(
side: const BorderSide(
color: Colors.blueAccent),
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 8),
textStyle:
const TextStyle(fontSize: 14),
),
onPressed: () {
final activityName = task
.workItem
?.activityMaster
?.activityName ??
'N/A';
final plannedTask = task.plannedTask;
final completed = task.completedTask;
final assigned =
'${(plannedTask - completed)}';
final plannedWork =
plannedTask.toString();
final completedWork =
completed.toString();
final assignedBy =
"${task.assignedBy.firstName} ${task.assignedBy.lastName ?? ''}";
final assignedOn =
DateFormat('yyyy-MM-dd')
.format(task.assignmentDate);
final taskId = task.id;
final location = [
task.workItem?.workArea?.floor
?.building?.name,
task.workItem?.workArea?.floor
?.floorName,
task.workItem?.workArea?.areaName,
]
.where((e) =>
e != null && e.isNotEmpty)
.join(' > ');
final teamMembers = task.teamMembers
.map((e) =>
'${e.firstName} ${e.lastName}')
.toList();
final taskComments =
task.comments.map((comment) {
final isoDate = comment.timestamp
.toIso8601String();
final commenterName = comment
.commentedBy
.firstName
.isNotEmpty
? "${comment.commentedBy.firstName} ${comment.commentedBy.lastName ?? ''}"
.trim()
: "Unknown";
return {
'text': comment.comment,
'date': isoDate,
'commentedBy': commenterName,
};
}).toList();
final taskData = {
'activity': activityName,
'assigned': assigned,
'taskId': taskId,
'assignedBy': assignedBy,
'completedWork': completedWork,
'plannedWork': plannedWork,
'assignedOn': assignedOn,
'location': location,
'teamSize': task.teamMembers.length,
'teamMembers': teamMembers,
'taskComments': taskComments,
};
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (_) =>
CommentTaskBottomSheet(
taskData: taskData,
onCommentSuccess: () {
_refreshData();
Navigator.of(context).pop();
},
),
);
},
),
],
)
],
),
),
),
if (index != tasksForDate.length - 1)
Divider(
color: Colors.grey.withOpacity(0.2),
thickness: 1,
height: 1),
],
);
}).toList(),
);
})
],
);
},
),
);
});
}
}

View File

@ -0,0 +1,485 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:marco/helpers/theme/app_theme.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
import 'package:marco/helpers/utils/my_shadow.dart';
import 'package:marco/helpers/widgets/my_breadcrumb.dart';
import 'package:marco/helpers/widgets/my_breadcrumb_item.dart';
import 'package:marco/helpers/widgets/my_card.dart';
import 'package:marco/helpers/widgets/my_spacing.dart';
import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/view/layouts/layout.dart';
import 'package:marco/controller/permission_controller.dart';
import 'package:marco/model/dailyTaskPlaning/daily_task_planing_filter.dart';
import 'package:marco/controller/task_planing/daily_task_planing_controller.dart';
import 'package:marco/model/dailyTaskPlaning/assign_task_bottom_sheet .dart';
class DailyTaskPlaningScreen extends StatefulWidget {
DailyTaskPlaningScreen({super.key});
@override
State<DailyTaskPlaningScreen> createState() => _DailyTaskPlaningScreenState();
}
class _DailyTaskPlaningScreenState extends State<DailyTaskPlaningScreen>
with UIMixin {
final DailyTaskPlaningController dailyTaskPlaningController =
Get.put(DailyTaskPlaningController());
final PermissionController permissionController =
Get.put(PermissionController());
@override
Widget build(BuildContext context) {
return Layout(
child: GetBuilder<DailyTaskPlaningController>(
init: dailyTaskPlaningController,
tag: 'daily_task_planing_controller',
builder: (controller) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: MySpacing.x(flexSpacing),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
MyText.titleMedium("Daily Task Planning",
fontSize: 18, fontWeight: 600),
MyBreadcrumb(
children: [
MyBreadcrumbItem(name: 'Dashboard'),
MyBreadcrumbItem(
name: 'Daily Task Planning', active: true),
],
),
],
),
),
MySpacing.height(flexSpacing),
Padding(
padding: MySpacing.x(flexSpacing),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
MyText.bodyMedium(
"Filter",
fontWeight: 600,
),
Tooltip(
message: 'Project',
child: InkWell(
borderRadius: BorderRadius.circular(24),
onTap: () async {
final result =
await showModalBottomSheet<Map<String, dynamic>>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.white,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12)),
),
builder: (context) => DailyTaskPlaningFilter(
controller: dailyTaskPlaningController,
permissionController: permissionController,
),
);
if (result != null) {
final selectedProjectId =
result['projectId'] as String?;
if (selectedProjectId != null &&
selectedProjectId !=
dailyTaskPlaningController
.selectedProjectId) {
// Update the controller's selected project ID
dailyTaskPlaningController.selectedProjectId =
selectedProjectId;
try {
// Fetch tasks for the new project
await dailyTaskPlaningController
.fetchTaskData(selectedProjectId);
} catch (e) {
debugPrint(
'Error fetching task data: ${e.toString()}');
}
// Update the UI
dailyTaskPlaningController
.update(['daily_task_planing_controller']);
}
}
},
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(
Icons.filter_list_alt,
color: Colors.blueAccent,
size: 28,
),
),
),
),
),
const SizedBox(width: 8),
MyText.bodyMedium(
"Refresh",
fontWeight: 600,
),
Tooltip(
message: 'Refresh Data',
child: InkWell(
borderRadius: BorderRadius.circular(24),
onTap: () async {
final projectId =
dailyTaskPlaningController.selectedProjectId;
if (projectId != null) {
try {
await dailyTaskPlaningController
.fetchTaskData(projectId);
} catch (e) {
debugPrint(
'Error refreshing task data: ${e.toString()}');
}
}
},
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(
Icons.refresh,
color: Colors.green,
size: 28,
),
),
),
),
),
],
),
),
Padding(
padding: MySpacing.x(flexSpacing),
child: dailyProgressReportTab(),
),
],
);
},
),
);
}
Widget dailyProgressReportTab() {
return Obx(() {
final isLoading = dailyTaskPlaningController.isLoading.value;
final dailyTasks = dailyTaskPlaningController.dailyTasks;
if (isLoading) {
return Center(child: CircularProgressIndicator());
}
if (dailyTasks.isEmpty) {
return Center(
child: MyText.bodySmall(
"No Progress Report Found",
fontWeight: 600,
),
);
}
final buildingExpansionState = <String, bool>{};
final floorExpansionState = <String, bool>{};
Widget buildExpandIcon(bool isExpanded) {
return Container(
width: 32,
height: 32,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.grey.shade200,
),
child: Icon(
isExpanded ? Icons.remove : Icons.add,
size: 20,
color: Colors.black87,
),
);
}
return StatefulBuilder(builder: (context, setMainState) {
final filteredBuildings = dailyTasks.expand((task) {
return task.buildings.where((building) {
return building.floors.any((floor) =>
floor.workAreas.any((area) => area.workItems.isNotEmpty));
});
}).toList();
if (filteredBuildings.isEmpty) {
return Center(
child: MyText.bodySmall(
"No Progress Report Found",
fontWeight: 600,
),
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: filteredBuildings.map((building) {
final buildingKey = building.id.toString();
return MyCard.bordered(
borderRadiusAll: 12,
paddingAll: 0,
margin: MySpacing.bottom(12),
shadow: MyShadow(elevation: 3),
child: Theme(
data: Theme.of(context)
.copyWith(dividerColor: Colors.transparent),
child: ExpansionTile(
onExpansionChanged: (expanded) {
setMainState(() {
buildingExpansionState[buildingKey] = expanded;
});
},
trailing: buildExpandIcon(
buildingExpansionState[buildingKey] ?? false),
tilePadding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 0),
collapsedShape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
leading: Container(
decoration: BoxDecoration(
color: Colors.blueAccent.withOpacity(0.1),
shape: BoxShape.circle,
),
padding: const EdgeInsets.all(8),
child: Icon(
Icons.location_city_rounded,
color: Colors.blueAccent,
size: 24,
),
),
title: MyText.titleMedium(
building.name,
fontWeight: 700,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
childrenPadding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 0),
children: building.floors.expand((floor) {
final validWorkAreas = floor.workAreas
.where((area) => area.workItems.isNotEmpty);
// For each valid work area, return a Floor+WorkArea ExpansionTile
return validWorkAreas.map((area) {
final floorWorkAreaKey =
"${buildingKey}_${floor.floorName}_${area.areaName}";
final isExpanded =
floorExpansionState[floorWorkAreaKey] ?? false;
return ExpansionTile(
onExpansionChanged: (expanded) {
setMainState(() {
floorExpansionState[floorWorkAreaKey] = expanded;
});
},
trailing: Icon(
isExpanded
? Icons.keyboard_arrow_up
: Icons.keyboard_arrow_down,
size: 28,
color: Colors.black54,
),
tilePadding: const EdgeInsets.symmetric(
horizontal: 16, vertical: 0),
title: Wrap(
spacing: 16,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
MyText.titleSmall(
"Floor: ${floor.floorName}",
fontWeight: 600,
color: Colors.teal,
maxLines: null,
overflow: TextOverflow.visible,
softWrap: true,
),
MyText.titleSmall(
"Work Area: ${area.areaName}",
fontWeight: 600,
color: Colors.blueGrey,
maxLines: null,
overflow: TextOverflow.visible,
softWrap: true,
),
],
),
childrenPadding: const EdgeInsets.only(
left: 16, right: 0, bottom: 8),
children: area.workItems.map((wItem) {
final item = wItem.workItem;
final completed = item.completedWork ?? 0;
final planned = item.plannedWork ?? 0;
final progress = (planned == 0)
? 0.0
: (completed / planned).clamp(0.0, 1.0);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: MyText.bodyMedium(
item.activityMaster?.name ??
"No Activity",
fontWeight: 600,
maxLines: 2,
overflow: TextOverflow.visible,
softWrap: true,
),
),
MySpacing.width(8),
if (item.workCategoryMaster?.name != null)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: Colors.blue.shade100,
borderRadius:
BorderRadius.circular(20),
),
child: MyText.bodySmall(
item.workCategoryMaster!.name!,
fontWeight: 500,
color: Colors.blue.shade800,
),
),
],
),
MySpacing.height(4),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
flex: 3,
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
MySpacing.height(8),
MyText.bodySmall(
"Completed: $completed / $planned",
fontWeight: 600,
color: const Color.fromARGB(
221, 0, 0, 0),
),
],
),
),
MySpacing.width(16),
if (progress < 1.0)
IconButton(
icon: Icon(
Icons.person_add_alt_1_rounded,
color: const Color.fromARGB(
255, 46, 161, 233),
),
onPressed: () {
final pendingTask =
(planned - completed)
.clamp(0, planned);
showModalBottomSheet(
context: context,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius:
BorderRadius.vertical(
top: Radius.circular(16)),
),
builder: (context) =>
AssignTaskBottomSheet(
buildingName: building.name,
floorName: floor.floorName,
workAreaName: area.areaName,
workLocation: area.areaName,
activityName:
item.activityMaster?.name ??
"Unknown Activity",
pendingTask: pendingTask,
workItemId: item.id.toString(),
assignmentDate: DateTime.now(),
),
);
},
),
],
),
MySpacing.height(8),
Stack(
children: [
Container(
height: 5,
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(6),
),
),
FractionallySizedBox(
widthFactor: progress,
child: Container(
height: 5,
decoration: BoxDecoration(
color: progress >= 1.0
? Colors.green
: (progress >= 0.5
? Colors.amber
: Colors.red),
borderRadius:
BorderRadius.circular(6),
),
),
),
],
),
SizedBox(height: 4),
MyText.bodySmall(
"${(progress * 100).toStringAsFixed(1)}%",
fontWeight: 500,
color: progress >= 1.0
? Colors.green[700]
: (progress >= 0.5
? Colors.amber[800]
: Colors.red[700]),
),
],
),
);
}).toList(),
);
}).toList();
}).toList(),
),
),
);
}).toList(),
);
});
});
}
}

View File

@ -64,6 +64,9 @@ dependencies:
image: ^4.0.17
image_picker: ^1.0.7
logger: ^2.0.2
flutter_image_compress: ^2.1.0
path_provider: ^2.1.2
path: ^1.9.0
dev_dependencies:
flutter_test:
sdk: flutter
@ -89,17 +92,17 @@ flutter:
# To add assets to your application, add an assets section, like this:
assets:
- assets/
- assets/lang/
- assets/avatar/
- assets/logo/
- assets/coin/
- assets/country/
- assets/data/
- assets/dummy/
- assets/social/
- assets/country/
- assets/coin/
- assets/dummy/ecommerce/
- assets/dummy/single_product/
- assets/lang/
- assets/logo/
- assets/logo/loading_logo.png
- assets/social/
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg