-- Enhance layout with floating action button and navigation improvements
- Added a floating action button to the Layout widget for better accessibility. - Updated the left bar navigation items for clarity and consistency. - Introduced Daily Progress Report and Daily Task Planning screens with comprehensive UI. - Implemented filtering and refreshing functionalities in task planning. - Improved user experience with better spacing and layout adjustments. - Updated pubspec.yaml to include new dependencies for image handling and path management.
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 327 KiB After Width: | Height: | Size: 151 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 43 KiB |
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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(),
|
||||
),
|
||||
);
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
190
lib/controller/task_planing/daily_task_planing_controller.dart
Normal 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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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";
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
47
lib/helpers/widgets/my_image_compressor.dart
Normal 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);
|
||||
}
|
47
lib/helpers/widgets/my_snackbar.dart
Normal 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,
|
||||
),
|
||||
);
|
||||
}
|
@ -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) {
|
||||
|
@ -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),
|
||||
|
@ -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),
|
||||
|
402
lib/model/dailyTaskPlaning/assign_task_bottom_sheet .dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
482
lib/model/dailyTaskPlaning/comment_task_bottom_sheet.dart
Normal 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! : "-"),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
206
lib/model/dailyTaskPlaning/daily_progress_report_filter.dart
Normal 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,
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
144
lib/model/dailyTaskPlaning/daily_task_planing_filter.dart
Normal 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,
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
234
lib/model/dailyTaskPlaning/daily_task_planing_model.dart
Normal 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?,
|
||||
);
|
||||
}
|
||||
}
|
309
lib/model/dailyTaskPlaning/report_task_bottom_sheet.dart
Normal 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! : "-"),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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'] ?? ''),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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(),
|
||||
|
245
lib/model/employees/add_employee_bottom_sheet.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
215
lib/model/employees/employee_detail_bottom_sheet.dart
Normal 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,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
106
lib/model/employees/employee_details_model.dart
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
171
lib/model/employees/employees_screen_filter_sheet.dart
Normal 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,
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
@ -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(),
|
||||
|
@ -181,7 +181,6 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
||||
),
|
||||
],
|
||||
),
|
||||
MySpacing.height(flexSpacing),
|
||||
Padding(
|
||||
padding: MySpacing.x(flexSpacing / 2),
|
||||
child: MyFlex(children: [
|
||||
@ -394,6 +393,7 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: MyText.titleMedium(
|
||||
@ -401,6 +401,29 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
||||
fontWeight: 600,
|
||||
),
|
||||
),
|
||||
Obx(() {
|
||||
if (attendanceController.isLoading.value) {
|
||||
return const SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: LinearProgressIndicator(),
|
||||
);
|
||||
}
|
||||
final dateFormat = DateFormat('dd MMM yyyy');
|
||||
final dateRangeText = attendanceController
|
||||
.startDateAttendance !=
|
||||
null &&
|
||||
attendanceController.endDateAttendance != null
|
||||
? '${dateFormat.format(attendanceController.endDateAttendance!)} - ${dateFormat.format(attendanceController.startDateAttendance!)}'
|
||||
: 'Select date range';
|
||||
|
||||
return MyText.bodySmall(
|
||||
dateRangeText,
|
||||
fontWeight: 600,
|
||||
color: Colors.grey[700],
|
||||
overflow: TextOverflow.ellipsis,
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
}
|
||||
|
327
lib/view/employees/employees_screen.dart
Normal 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(),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
@ -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,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -1,4 +1,3 @@
|
||||
// All import statements remain unchanged
|
||||
import 'package:marco/helpers/theme/theme_customizer.dart';
|
||||
import 'package:marco/helpers/services/url_service.dart';
|
||||
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
|
||||
@ -81,7 +80,7 @@ class _LeftBarState extends State<LeftBar>
|
||||
children: [
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: MySpacing.y(12),
|
||||
padding: MySpacing.fromLTRB(0, 24, 0, 0),
|
||||
child: InkWell(
|
||||
onTap: () => Get.toNamed('/home'),
|
||||
child: Image.asset(
|
||||
@ -115,7 +114,7 @@ class _LeftBarState extends State<LeftBar>
|
||||
isCondensed: isCondensed,
|
||||
route: '/dashboard'),
|
||||
NavigationItem(
|
||||
iconData: LucideIcons.layout_template,
|
||||
iconData: LucideIcons.scan_face,
|
||||
title: "Attendance",
|
||||
isCondensed: isCondensed,
|
||||
route: '/dashboard/attendance'),
|
||||
@ -125,15 +124,15 @@ class _LeftBarState extends State<LeftBar>
|
||||
isCondensed: isCondensed,
|
||||
route: '/dashboard/employees'),
|
||||
NavigationItem(
|
||||
iconData: LucideIcons.list,
|
||||
title: "Daily Progress Report",
|
||||
isCondensed: isCondensed,
|
||||
route: '/dashboard/daily-task'),
|
||||
NavigationItem(
|
||||
iconData: LucideIcons.list_todo,
|
||||
iconData: LucideIcons.logs,
|
||||
title: "Daily Task Planing",
|
||||
isCondensed: isCondensed,
|
||||
route: '/dashboard/daily-task-planing'),
|
||||
NavigationItem(
|
||||
iconData: LucideIcons.list_todo,
|
||||
title: "Daily Progress Report",
|
||||
isCondensed: isCondensed,
|
||||
route: '/dashboard/daily-task-progress'),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -148,9 +147,7 @@ class _LeftBarState extends State<LeftBar>
|
||||
|
||||
Widget userInfoSection() {
|
||||
if (employeeInfo == null) {
|
||||
return Center(
|
||||
child:
|
||||
CircularProgressIndicator()); // Show loading indicator if employeeInfo is not yet loaded.
|
||||
return Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
return Padding(
|
||||
@ -245,9 +242,7 @@ class _MenuWidgetState extends State<MenuWidget>
|
||||
}
|
||||
|
||||
void onChangeMenuActive(String key) {
|
||||
if (key != widget.title) {
|
||||
// onChangeExpansion(false);
|
||||
}
|
||||
if (key != widget.title) {}
|
||||
}
|
||||
|
||||
void onChangeExpansion(value) {
|
||||
@ -271,22 +266,16 @@ class _MenuWidgetState extends State<MenuWidget>
|
||||
if (hideFn != null) {
|
||||
hideFn!();
|
||||
}
|
||||
// popupShowing = false;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// var route = Uri.base.fragment;
|
||||
// isActive = widget.children.any((element) => element.route == route);
|
||||
|
||||
if (widget.isCondensed) {
|
||||
return CustomPopupMenu(
|
||||
backdrop: true,
|
||||
show: popupShowing,
|
||||
hideFn: (hide) => hideFn = hide,
|
||||
onChange: (_) {
|
||||
// popupShowing = _;
|
||||
},
|
||||
onChange: (_) {},
|
||||
placement: CustomPopupMenuPlacement.right,
|
||||
menu: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
@ -407,7 +396,6 @@ class _MenuWidgetState extends State<MenuWidget>
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
// LeftbarObserver.detachListener(widget.title);
|
||||
}
|
||||
}
|
||||
|
||||
|
602
lib/view/taskPlaning/daily_progress.dart
Normal 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(),
|
||||
);
|
||||
})
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
485
lib/view/taskPlaning/daily_task_planing.dart
Normal 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(),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
13
pubspec.yaml
@ -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
|
||||
|