-- Enhance layout with floating action button and navigation improvements #33

Merged
vikas.nale merged 3 commits from Vaibhav_Dev into main 2025-05-30 05:15:04 +00:00
50 changed files with 5500 additions and 384 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

@ -4,7 +4,7 @@ import 'package:marco/controller/my_controller.dart';
import 'package:marco/helpers/services/auth_service.dart';
import 'package:marco/helpers/widgets/my_form_validator.dart';
import 'package:marco/helpers/widgets/my_validators.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
class ForgotPasswordController extends MyController {
MyFormValidator basicValidator = MyFormValidator();
bool showPassword = false;
@ -35,6 +35,31 @@ class ForgotPasswordController extends MyController {
}
}
/// New: Forgot password function
Future<void> onForgotPassword() async {
if (basicValidator.validateForm()) {
update();
final data = basicValidator.getData();
final email = data['email']?.toString() ?? '';
final result = await AuthService.forgotPassword(email);
if (result != null) {
showAppSnackbar(
title: "Success",
message: "Your password reset link was sent.",
type: SnackbarType.success,
);
} else {
showAppSnackbar(
title: "Success",
message: "Your password reset link was sent.",
type: SnackbarType.success,
);
}
update();
}
}
void gotoLogIn() {
Get.toNamed('/auth/login');
}

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();
@ -224,12 +225,37 @@ class AttendanceController extends GetxController {
selectableDayPredicate: (DateTime day, DateTime? start, DateTime? end) {
final dayDateOnly = DateTime(day.year, day.month, day.day);
if (dayDateOnly == todayDateOnly) {
return false;
return false;
}
return true;
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();
@ -50,47 +59,46 @@ class DailyTaskController extends GetxController {
projects = response!.map((json) => ProjectModel.fromJson(json)).toList();
selectedProjectId = projects.first.id.toString();
log.i("Projects fetched: ${projects.length} projects loaded.");
update();
update();
await fetchTaskData(selectedProjectId);
}
Future<void> fetchTaskData(String? projectId) async {
if (projectId == null) return;
Future<void> fetchTaskData(String? projectId) async {
if (projectId == null) return;
isLoading.value = true;
final response = await ApiService.getDailyTasks(
projectId,
dateFrom: startDateTask,
dateTo: endDateTask,
);
isLoading.value = false;
isLoading.value = true;
final response = await ApiService.getDailyTasks(
projectId,
dateFrom: startDateTask,
dateTo: endDateTask,
);
isLoading.value = false;
if (response != null) {
Map<String, List<TaskModel>> groupedTasks = {};
if (response != null) {
groupedDailyTasks.clear();
for (var taskJson in response) {
TaskModel task = TaskModel.fromJson(taskJson);
String assignmentDateKey = task.assignmentDate;
for (var taskJson in response) {
TaskModel task = TaskModel.fromJson(taskJson);
String assignmentDateKey =
task.assignmentDate.toIso8601String().split('T')[0];
if (groupedTasks.containsKey(assignmentDateKey)) {
groupedTasks[assignmentDateKey]?.add(task);
} else {
groupedTasks[assignmentDateKey] = [task];
if (groupedDailyTasks.containsKey(assignmentDateKey)) {
groupedDailyTasks[assignmentDateKey]?.add(task);
} else {
groupedDailyTasks[assignmentDateKey] = [task];
}
}
// 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}");
update();
} else {
log.e("Failed to fetch daily tasks for project $projectId");
}
dailyTasks = groupedTasks.entries
.map((entry) => entry.value)
.expand((taskList) => taskList)
.toList();
log.i("Daily tasks fetched and grouped: ${dailyTasks.length}");
update();
} else {
log.e("Failed to fetch daily tasks for project $projectId");
}
}
Future<void> selectDateRangeForTaskData(
BuildContext context,
@ -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 {
@ -69,8 +74,8 @@ class EmployeesScreenController extends GetxController {
},
onEmpty: () {
log.w("No employees found for project $projectId.");
employees = [];
update();
employees = [];
update();
},
onError: (e) =>
log.e("Error fetching employees for project $projectId: $e"),
@ -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 reportTask = "/task/report";
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 {
@ -115,4 +112,89 @@ class AuthService {
return false;
}
}
// Forgot password API
static Future<Map<String, String>?> forgotPassword(String email) async {
final requestBody = {"email": email};
logger.i("Sending forgot password request with email: $email");
try {
final response = await http.post(
Uri.parse("$_baseUrl/auth/forgot-password"),
headers: _headers,
body: jsonEncode(requestBody),
);
logger.i(
"Forgot password API response (${response.statusCode}): ${response.body}");
final responseData = jsonDecode(response.body);
if (response.statusCode == 200 && responseData['success'] == true) {
logger.i("Forgot password request successful.");
return null;
} else {
return {
"error":
responseData['message'] ?? "Failed to send password reset link."
};
}
} catch (e) {
logger.e("Exception during forgot password request: $e");
return {"error": "Network error. Please check your connection."};
}
}
// Request demo API
static Future<Map<String, String>?> requestDemo(
Map<String, dynamic> demoData) async {
try {
final response = await http.post(
Uri.parse("$_baseUrl/market/inquiry"),
headers: _headers,
body: jsonEncode(demoData),
);
final responseData = jsonDecode(response.body);
if (response.statusCode == 200 && responseData['success'] == true) {
logger.i("Request Demo submitted successfully.");
return null;
} else {
return {
"error": responseData['message'] ?? "Failed to submit demo request."
};
}
} catch (e) {
logger.e("Exception during request demo: $e");
return {"error": "Network error. Please check your connection."};
}
}
static Future<List<Map<String, dynamic>>?> getIndustries() async {
try {
final response = await http.get(
Uri.parse("$_baseUrl/market/industries"),
headers: _headers,
);
logger.i(
"Get Industries API response (${response.statusCode}): ${response.body}");
final responseData = jsonDecode(response.body);
if (response.statusCode == 200 && responseData['success'] == true) {
// Return the list of industries as List<Map<String, dynamic>>
final List<dynamic> industriesData = responseData['data'];
return industriesData.cast<Map<String, dynamic>>();
} else {
logger.w("Failed to fetch industries: ${responseData['message']}");
return null;
}
} catch (e) {
logger.e("Exception during getIndustries: $e");
return null;
}
}
}

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
@ -30,9 +33,8 @@ getPageRoute() {
var routes = [
GetPage(
name: '/',
page: () => AttendanceScreen(),
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

@ -8,6 +8,9 @@ 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/view/layouts/auth_layout.dart';
import 'package:marco/images.dart';
import 'package:marco/helpers/theme/app_theme.dart';
class ForgotPasswordScreen extends StatefulWidget {
const ForgotPasswordScreen({super.key});
@ -26,57 +29,88 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> with UIMixi
init: controller,
builder: (controller) {
return Form(
key: controller.basicValidator.formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.titleLarge("Forgot Password", fontWeight: 600),
MySpacing.height(12),
MyText.bodyMedium(
"Enter the email address associated with your account and we'll send an email instructions on how to recover your password.",
key: controller.basicValidator.formKey,
child: SingleChildScrollView(
padding: MySpacing.xy(2, 40),
child: Container(
width: double.infinity,
padding: MySpacing.all(24),
decoration: BoxDecoration(
color: theme.colorScheme.primary.withOpacity(0.02),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: contentTheme.primary.withOpacity(0.5),
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Image.asset(
Images.logoDark,
height: 120,
fit: BoxFit.contain,
),
),
MySpacing.height(10),
MyText.titleLarge("Forgot Password", fontWeight: 600),
MySpacing.height(12),
MyText.bodyMedium(
"Enter your email and we'll send you instructions to reset your password.",
fontWeight: 600,
xMuted: true),
MySpacing.height(12),
TextFormField(
xMuted: true,
),
MySpacing.height(12),
TextFormField(
validator: controller.basicValidator.getValidation('email'),
controller: controller.basicValidator.getController('email'),
keyboardType: TextInputType.emailAddress,
style: MyTextStyle.labelMedium(),
decoration: InputDecoration(
labelText: "Email Address",
labelStyle: MyTextStyle.bodySmall(xMuted: true),
border: OutlineInputBorder(borderSide: BorderSide.none),
filled: true,
fillColor: contentTheme.secondary.withAlpha(36),
prefixIcon: Icon(LucideIcons.mail, size: 16),
contentPadding: MySpacing.all(15),
isDense: true,
isCollapsed: true,
floatingLabelBehavior: FloatingLabelBehavior.never,
keyboardType: TextInputType.emailAddress,
style: MyTextStyle.labelMedium(),
decoration: InputDecoration(
labelText: "Email Address",
labelStyle: MyTextStyle.bodySmall(xMuted: true),
border: OutlineInputBorder(borderSide: BorderSide.none),
filled: true,
fillColor: theme.cardColor,
prefixIcon: Icon(LucideIcons.mail, size: 16),
contentPadding: MySpacing.all(15),
isDense: true,
isCollapsed: true,
floatingLabelBehavior: FloatingLabelBehavior.never,
),
),
),
MySpacing.height(20),
Center(
child: MyButton.rounded(
onPressed: controller.onLogin,
elevation: 0,
padding: MySpacing.xy(20, 16),
backgroundColor: contentTheme.primary,
child: MyText.labelMedium('Forgot Password', color: contentTheme.onPrimary),
MySpacing.height(20),
Center(
child: MyButton.rounded(
onPressed: controller.onForgotPassword,
elevation: 0,
padding: MySpacing.xy(20, 16),
backgroundColor: Colors.blueAccent,
child: MyText.labelMedium(
'Send Reset Link',
color: contentTheme.onPrimary,
),
),
),
),
Center(
child: MyButton.text(
onPressed: controller.gotoLogIn,
elevation: 0,
padding: MySpacing.x(16),
splashColor: contentTheme.secondary.withValues(alpha:0.1),
child: MyText.labelMedium('Back to log in', color: contentTheme.secondary),
Center(
child: MyButton.text(
onPressed: controller.gotoLogIn,
elevation: 0,
padding: MySpacing.x(16),
splashColor:
contentTheme.secondary.withValues(alpha: 0.1),
child: MyText.labelMedium(
'Back to log in',
color: contentTheme.secondary,
),
),
),
),
],
));
],
),
),
),
);
},
),
);

View File

@ -10,6 +10,7 @@ import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/helpers/widgets/my_text_style.dart';
import 'package:marco/view/layouts/auth_layout.dart';
import 'package:marco/images.dart';
import 'package:marco/view/auth/request_demo_bottom_sheet.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@ -194,7 +195,7 @@ class _LoginScreenState extends State<LoginScreen> with UIMixin {
elevation: 2,
padding: MySpacing.xy(24, 16),
borderRadiusAll: 16,
backgroundColor: contentTheme.primary,
backgroundColor: Colors.blueAccent,
child: MyText.labelMedium(
'Login',
fontWeight: 600,
@ -207,10 +208,13 @@ class _LoginScreenState extends State<LoginScreen> with UIMixin {
/// Register Link
Center(
child: MyButton.text(
onPressed: controller.gotoRegister,
onPressed: () {
OrganizationFormBottomSheet.show(context);
},
elevation: 0,
padding: MySpacing.xy(12, 8),
splashColor: contentTheme.secondary.withAlpha(30),
splashColor:
contentTheme.secondary.withAlpha(30),
child: MyText.bodySmall(
"Request a Demo",
color: contentTheme.secondary,

View File

@ -0,0 +1,325 @@
import 'package:flutter/material.dart';
import 'package:marco/helpers/widgets/my_form_validator.dart';
import 'package:marco/helpers/services/auth_service.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
class OrganizationFormBottomSheet {
static void show(BuildContext context) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (_) => DraggableScrollableSheet(
expand: false,
initialChildSize: 0.85,
minChildSize: 0.5,
maxChildSize: 0.95,
builder: (context, scrollController) {
return _OrganizationForm(scrollController: scrollController);
},
),
);
}
}
class _OrganizationForm extends StatefulWidget {
final ScrollController scrollController;
const _OrganizationForm({required this.scrollController});
@override
State<_OrganizationForm> createState() => _OrganizationFormState();
}
class _OrganizationFormState extends State<_OrganizationForm> {
final MyFormValidator validator = MyFormValidator();
List<Map<String, dynamic>> _industries = [];
String? _selectedIndustryId;
final List<String> _sizes = [
'1-10',
'11-50',
'51-200',
'201-1000',
'1000+',
];
final Map<String, String> _sizeApiMap = {
'1-10': 'less than 10',
'11-50': '11 to 50',
'51-200': '51 to 200',
'201-1000': 'more than 200',
'1000+': 'more than 1000',
};
String? _selectedSize;
bool _agreed = false;
@override
void initState() {
super.initState();
_loadIndustries();
validator.addField<String>('organizationName',
required: true, controller: TextEditingController());
validator.addField<String>('email',
required: true, controller: TextEditingController());
validator.addField<String>('contactPerson',
required: true, controller: TextEditingController());
validator.addField<String>('contactNumber',
required: true, controller: TextEditingController());
validator.addField<String>('about', controller: TextEditingController());
validator.addField<String>('address',
required: true, controller: TextEditingController());
}
Future<void> _loadIndustries() async {
final industries = await AuthService.getIndustries();
if (industries != null) {
setState(() {
_industries = industries;
});
}
}
@override
Widget build(BuildContext context) {
return Container(
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
padding: const EdgeInsets.all(20),
child: SingleChildScrollView(
controller: widget.scrollController,
child: Form(
key: validator.formKey,
child: Column(
children: [
Container(
width: 40,
height: 5,
margin: const EdgeInsets.only(bottom: 20),
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(10),
),
),
Text(
'Adventure starts here 🚀',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 4),
const Text("Make your app management easy and fun!"),
const SizedBox(height: 20),
_buildTextField('organizationName', 'Organization Name'),
_buildTextField('email', 'Email',
keyboardType: TextInputType.emailAddress),
_buildTextField('contactPerson', 'Contact Person'),
_buildTextField('contactNumber', 'Contact Number',
keyboardType: TextInputType.phone),
_buildTextField('about', 'About Organization'),
_buildTextField('address', 'Current Address'),
const SizedBox(height: 10),
_buildPopupMenuField(
'Organization Size',
_sizes,
_selectedSize,
(val) => setState(() => _selectedSize = val),
'Please select organization size',
),
_buildPopupMenuField(
'Industry',
_industries.map((e) => e['name'] as String).toList(),
_selectedIndustryId != null
? _industries.firstWhere(
(e) => e['id'] == _selectedIndustryId)['name']
: null,
(val) {
setState(() {
final selectedIndustry = _industries.firstWhere(
(element) => element['name'] == val,
orElse: () => {});
_selectedIndustryId = selectedIndustry['id'];
});
},
'Please select industry',
),
const SizedBox(height: 10),
Row(
children: [
Checkbox(
value: _agreed,
onChanged: (val) => setState(() => _agreed = val ?? false),
fillColor: MaterialStateProperty.all(Colors.white),
checkColor: Colors.white,
side: MaterialStateBorderSide.resolveWith(
(states) =>
BorderSide(color: Colors.blueAccent, width: 2),
),
),
const Expanded(
child: Text('I agree to privacy policy & terms')),
],
),
const SizedBox(height: 10),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blueAccent,
),
onPressed: _submitForm,
child: const Text("Submit"),
),
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text("Back to login"),
),
],
),
),
),
);
}
Widget _buildPopupMenuField(
String label,
List<String> items,
String? selectedValue,
ValueChanged<String?> onSelected,
String errorText,
) {
final bool hasError = selectedValue == null;
final GlobalKey _key = GlobalKey();
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: FormField<String>(
validator: (value) => hasError ? errorText : null,
builder: (fieldState) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label,
style: TextStyle(fontSize: 12, color: Colors.grey[700])),
const SizedBox(height: 6),
GestureDetector(
key: _key,
onTap: () async {
final RenderBox renderBox =
_key.currentContext!.findRenderObject() as RenderBox;
final Offset offset = renderBox.localToGlobal(Offset.zero);
final Size size = renderBox.size;
final selected = await showMenu<String>(
context: fieldState.context,
position: RelativeRect.fromLTRB(
offset.dx,
offset.dy + size.height,
offset.dx + size.width,
offset.dy,
),
items: items
.map((item) => PopupMenuItem<String>(
value: item,
child: Text(item),
))
.toList(),
);
if (selected != null) {
onSelected(selected);
fieldState.didChange(selected);
}
},
child: InputDecorator(
decoration: InputDecoration(
border: const OutlineInputBorder(),
errorText: fieldState.errorText,
contentPadding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 14),
),
child: Text(
selectedValue ?? 'Select $label',
style: TextStyle(
color: selectedValue == null ? Colors.grey : Colors.black,
fontSize: 16,
),
),
),
),
],
);
},
),
);
}
Widget _buildTextField(String fieldName, String label,
{TextInputType keyboardType = TextInputType.text}) {
final controller = validator.getController(fieldName);
final validatorFunc = validator.getValidation<String>(fieldName);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: TextFormField(
controller: controller,
keyboardType: keyboardType,
decoration: InputDecoration(
labelText: label,
border: const OutlineInputBorder(),
),
validator: validatorFunc,
),
);
}
void _submitForm() async {
bool isValid = validator.validateForm();
if (_selectedSize == null || _selectedIndustryId == null) {
isValid = false;
setState(() {});
}
if (!_agreed) {
isValid = false;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Please agree to the privacy policy & terms')),
);
}
if (isValid) {
final formData = validator.getData();
final Map<String, dynamic> requestBody = {
'organizatioinName': formData['organizationName'],
'email': formData['email'],
'about': formData['about'],
'contactNumber': formData['contactNumber'],
'contactPerson': formData['contactPerson'],
'industryId': _selectedIndustryId ?? '',
'oragnizationSize': _sizeApiMap[_selectedSize] ?? '',
'terms': _agreed,
'address': formData['address'],
};
final error = await AuthService.requestDemo(requestBody);
if (error == null) {
showAppSnackbar(
title: "Success",
message: "Demo request submitted successfully!.",
type: SnackbarType.success,
);
Navigator.pop(context);
} else {
showAppSnackbar(
title: "Success",
message: (error['error'] ?? 'Unknown error'),
type: SnackbarType.success,
);
}
}
}
}

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: [
@ -202,6 +201,10 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
);
}
String _formatDate(DateTime date) {
return "${date.day}/${date.month}/${date.year}";
}
Widget employeeListTab() {
return Obx(() {
final isLoading = attendanceController.isLoadingEmployees.value;
@ -220,6 +223,12 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
fontWeight: 600,
),
),
MyText.bodySmall(
_formatDate(DateTime.now()),
fontWeight: 600,
color: Colors.grey[700],
overflow: TextOverflow.ellipsis,
),
],
),
),
@ -394,6 +403,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 +411,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);
}
@ -369,7 +369,7 @@ Widget _buildDateRangeButton() {
'activity': activityName,
'assigned': assigned,
'taskId': taskId,
'assignedBy': assignedBy,
'assignedBy': assignedBy,
'completedWork': completedWork,
'plannedWork': plannedWork,
'assignedOn': assignedOn,

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: 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')]),
],
),
),
MySpacing.height(flexSpacing),
Padding(
padding: MySpacing.x(flexSpacing / 2),
child: Column(
children: _buildDashboardStats(),
),
),
],
child: SingleChildScrollView(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.titleMedium("Dashboard", fontWeight: 600),
MySpacing.height(12),
_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(
onTap: () => Get.toNamed(statItem.route),
child: MyCard.bordered(
borderRadiusAll: 10,
border: Border.all(color: Colors.grey.withOpacity(0.2)),
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
paddingAll: 24,
height: 140,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildStatCardIcon(statItem),
MySpacing.height(12),
MyText.labelSmall(statItem.title, maxLines: 1),
],
),
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.15)),
shadow: MyShadow(elevation: 1.5, position: MyShadowPosition.bottom),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildStatCardIcon(statItem),
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,25 +21,26 @@ 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();
final EmployeeInfo? employeeInfo = LocalStorage.getEmployeeInfo();
Layout({super.key, this.child});
Layout({super.key, this.child, this.floatingActionButton});
@override
Widget build(BuildContext context) {
return MyResponsive(builder: (BuildContext context, _, screenMT) {
return GetBuilder(
init: controller,
builder: (controller) {
if (screenMT.isMobile || screenMT.isTablet) {
return mobileScreen();
} else {
return largeScreen();
}
init: controller,
builder: (controller) {
if (screenMT.isMobile || screenMT.isTablet) {
return mobileScreen();
} else {
return largeScreen();
}
});
});
}
@ -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: EdgeInsets.only(top: 50),
child: InkWell(
onTap: () => Get.toNamed('/home'),
child: Image.asset(
@ -107,7 +106,6 @@ class _LeftBarState extends State<LeftBar>
physics: BouncingScrollPhysics(),
clipBehavior: Clip.antiAliasWithSaveLayer,
children: [
Divider(),
labelWidget("Dashboard"),
NavigationItem(
iconData: LucideIcons.layout_dashboard,
@ -115,7 +113,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 +123,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 +146,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 +241,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 +265,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 +395,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