-- 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/controller/my_controller.dart';
|
||||||
import 'package:marco/helpers/widgets/my_form_validator.dart';
|
import 'package:marco/helpers/widgets/my_form_validator.dart';
|
||||||
import 'package:marco/helpers/services/api_service.dart';
|
import 'package:marco/helpers/services/api_service.dart';
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:logger/logger.dart';
|
import 'package:logger/logger.dart';
|
||||||
|
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||||
|
|
||||||
enum Gender {
|
enum Gender {
|
||||||
male,
|
male,
|
||||||
@ -77,16 +77,16 @@ class AddEmployeeController extends MyController {
|
|||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> createEmployees() async {
|
Future<bool> createEmployees() async {
|
||||||
logger.i("Starting employee creation...");
|
logger.i("Starting employee creation...");
|
||||||
if (selectedGender == null || selectedRoleId == null) {
|
if (selectedGender == null || selectedRoleId == null) {
|
||||||
logger.w("Missing gender or role.");
|
logger.w("Missing gender or role.");
|
||||||
Get.snackbar(
|
showAppSnackbar(
|
||||||
"Missing Fields",
|
title: "Missing Fields",
|
||||||
"Please select both Gender and Role.",
|
message: "Please select both Gender and Role.",
|
||||||
snackPosition: SnackPosition.BOTTOM,
|
type: SnackbarType.warning,
|
||||||
);
|
);
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
final firstName = basicValidator.getController("first_name")?.text.trim();
|
final firstName = basicValidator.getController("first_name")?.text.trim();
|
||||||
@ -107,13 +107,20 @@ class AddEmployeeController extends MyController {
|
|||||||
|
|
||||||
if (response == true) {
|
if (response == true) {
|
||||||
logger.i("Employee created successfully.");
|
logger.i("Employee created successfully.");
|
||||||
Get.back(); // Or navigate as needed
|
showAppSnackbar(
|
||||||
Get.snackbar("Success", "Employee created successfully!",
|
title: "Success",
|
||||||
snackPosition: SnackPosition.BOTTOM);
|
message: "Employee created successfully!",
|
||||||
|
type: SnackbarType.success,
|
||||||
|
);
|
||||||
|
return true;
|
||||||
} else {
|
} else {
|
||||||
logger.e("Failed to create employee.");
|
logger.e("Failed to create employee.");
|
||||||
Get.snackbar("Error", "Failed to create employee.",
|
showAppSnackbar(
|
||||||
snackPosition: SnackPosition.BOTTOM);
|
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:geolocator/geolocator.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:logger/logger.dart';
|
import 'package:logger/logger.dart';
|
||||||
|
import 'dart:io';
|
||||||
import 'package:marco/helpers/services/api_service.dart';
|
import 'package:marco/helpers/services/api_service.dart';
|
||||||
import 'package:marco/model/attendance_model.dart';
|
import 'package:marco/model/attendance_model.dart';
|
||||||
import 'package:marco/model/project_model.dart';
|
import 'package:marco/model/project_model.dart';
|
||||||
@ -12,6 +12,7 @@ import 'package:marco/model/employee_model.dart';
|
|||||||
import 'package:marco/model/attendance_log_model.dart';
|
import 'package:marco/model/attendance_log_model.dart';
|
||||||
import 'package:marco/model/regularization_log_model.dart';
|
import 'package:marco/model/regularization_log_model.dart';
|
||||||
import 'package:marco/model/attendance_log_view_model.dart';
|
import 'package:marco/model/attendance_log_view_model.dart';
|
||||||
|
import 'package:marco/helpers/widgets/my_image_compressor.dart';
|
||||||
|
|
||||||
final Logger log = Logger();
|
final Logger log = Logger();
|
||||||
|
|
||||||
@ -28,9 +29,7 @@ class AttendanceController extends GetxController {
|
|||||||
List<RegularizationLogModel> regularizationLogs = [];
|
List<RegularizationLogModel> regularizationLogs = [];
|
||||||
List<AttendanceLogViewModel> attendenceLogsView = [];
|
List<AttendanceLogViewModel> attendenceLogsView = [];
|
||||||
|
|
||||||
RxBool isLoading = true.obs;
|
RxBool isLoading = true.obs; // initially true
|
||||||
|
|
||||||
// New separate loading states per feature
|
|
||||||
RxBool isLoadingProjects = true.obs;
|
RxBool isLoadingProjects = true.obs;
|
||||||
RxBool isLoadingEmployees = true.obs;
|
RxBool isLoadingEmployees = true.obs;
|
||||||
RxBool isLoadingAttendanceLogs = true.obs;
|
RxBool isLoadingAttendanceLogs = true.obs;
|
||||||
@ -47,7 +46,7 @@ class AttendanceController extends GetxController {
|
|||||||
|
|
||||||
void _initializeDefaults() {
|
void _initializeDefaults() {
|
||||||
_setDefaultDateRange();
|
_setDefaultDateRange();
|
||||||
fetchProjects();
|
fetchProjects(); // fetchProjects will set isLoading to false after loading
|
||||||
}
|
}
|
||||||
|
|
||||||
void _setDefaultDateRange() {
|
void _setDefaultDateRange() {
|
||||||
@ -58,9 +57,7 @@ class AttendanceController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> _handleLocationPermission() async {
|
Future<bool> _handleLocationPermission() async {
|
||||||
LocationPermission permission;
|
LocationPermission permission = await Geolocator.checkPermission();
|
||||||
|
|
||||||
permission = await Geolocator.checkPermission();
|
|
||||||
if (permission == LocationPermission.denied) {
|
if (permission == LocationPermission.denied) {
|
||||||
permission = await Geolocator.requestPermission();
|
permission = await Geolocator.requestPermission();
|
||||||
if (permission == LocationPermission.denied) {
|
if (permission == LocationPermission.denied) {
|
||||||
@ -68,34 +65,33 @@ class AttendanceController extends GetxController {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (permission == LocationPermission.deniedForever) {
|
if (permission == LocationPermission.deniedForever) {
|
||||||
log.e('Location permissions are permanently denied');
|
log.e('Location permissions are permanently denied');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> fetchProjects() async {
|
Future<void> fetchProjects() async {
|
||||||
// Both old and new loading state set for safety
|
|
||||||
isLoading.value = true;
|
|
||||||
isLoadingProjects.value = true;
|
isLoadingProjects.value = true;
|
||||||
|
isLoading.value = true;
|
||||||
|
|
||||||
final response = await ApiService.getProjects();
|
final response = await ApiService.getProjects();
|
||||||
|
|
||||||
isLoadingProjects.value = false;
|
|
||||||
isLoading.value = false;
|
|
||||||
|
|
||||||
if (response != null && response.isNotEmpty) {
|
if (response != null && response.isNotEmpty) {
|
||||||
projects = response.map((json) => ProjectModel.fromJson(json)).toList();
|
projects = response.map((json) => ProjectModel.fromJson(json)).toList();
|
||||||
selectedProjectId = projects.first.id.toString();
|
selectedProjectId = projects.first.id.toString();
|
||||||
log.i("Projects fetched: ${projects.length} projects loaded.");
|
log.i("Projects fetched: ${projects.length} projects loaded.");
|
||||||
|
|
||||||
await fetchProjectData(selectedProjectId);
|
await fetchProjectData(selectedProjectId);
|
||||||
update(['attendance_dashboard_controller']);
|
|
||||||
} else {
|
} else {
|
||||||
log.w("No project data found or API call failed.");
|
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 {
|
Future<void> fetchProjectData(String? projectId) async {
|
||||||
@ -117,28 +113,23 @@ class AttendanceController extends GetxController {
|
|||||||
Future<void> fetchEmployeesByProject(String? projectId) async {
|
Future<void> fetchEmployeesByProject(String? projectId) async {
|
||||||
if (projectId == null) return;
|
if (projectId == null) return;
|
||||||
|
|
||||||
isLoading.value = true;
|
|
||||||
isLoadingEmployees.value = true;
|
isLoadingEmployees.value = true;
|
||||||
|
|
||||||
final response = await ApiService.getEmployeesByProject(projectId);
|
final response = await ApiService.getEmployeesByProject(projectId);
|
||||||
|
|
||||||
isLoadingEmployees.value = false;
|
|
||||||
isLoading.value = false;
|
|
||||||
|
|
||||||
if (response != null) {
|
if (response != null) {
|
||||||
employees = response.map((json) => EmployeeModel.fromJson(json)).toList();
|
employees = response.map((json) => EmployeeModel.fromJson(json)).toList();
|
||||||
|
|
||||||
// Initialize per-employee uploading state
|
|
||||||
for (var emp in employees) {
|
for (var emp in employees) {
|
||||||
uploadingStates[emp.id] = false.obs;
|
uploadingStates[emp.id] = false.obs;
|
||||||
}
|
}
|
||||||
|
|
||||||
log.i(
|
log.i(
|
||||||
"Employees fetched: ${employees.length} employees for project $projectId");
|
"Employees fetched: ${employees.length} employees for project $projectId");
|
||||||
update();
|
update();
|
||||||
} else {
|
} else {
|
||||||
log.e("Failed to fetch employees for project $projectId");
|
log.e("Failed to fetch employees for project $projectId");
|
||||||
}
|
}
|
||||||
|
isLoadingEmployees.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> captureAndUploadAttendance(
|
Future<bool> captureAndUploadAttendance(
|
||||||
@ -164,6 +155,16 @@ class AttendanceController extends GetxController {
|
|||||||
uploadingStates[employeeId]?.value = false;
|
uploadingStates[employeeId]?.value = false;
|
||||||
return 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();
|
final hasLocationPermission = await _handleLocationPermission();
|
||||||
@ -224,12 +225,37 @@ class AttendanceController extends GetxController {
|
|||||||
selectableDayPredicate: (DateTime day, DateTime? start, DateTime? end) {
|
selectableDayPredicate: (DateTime day, DateTime? start, DateTime? end) {
|
||||||
final dayDateOnly = DateTime(day.year, day.month, day.day);
|
final dayDateOnly = DateTime(day.year, day.month, day.day);
|
||||||
if (dayDateOnly == todayDateOnly) {
|
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) {
|
if (picked != null) {
|
||||||
startDateAttendance = picked.start;
|
startDateAttendance = picked.start;
|
||||||
endDateAttendance = picked.end;
|
endDateAttendance = picked.end;
|
||||||
@ -251,8 +277,8 @@ class AttendanceController extends GetxController {
|
|||||||
}) async {
|
}) async {
|
||||||
if (projectId == null) return;
|
if (projectId == null) return;
|
||||||
|
|
||||||
isLoading.value = true;
|
|
||||||
isLoadingAttendanceLogs.value = true;
|
isLoadingAttendanceLogs.value = true;
|
||||||
|
isLoading.value = true;
|
||||||
|
|
||||||
final response = await ApiService.getAttendanceLogs(
|
final response = await ApiService.getAttendanceLogs(
|
||||||
projectId,
|
projectId,
|
||||||
@ -260,9 +286,6 @@ class AttendanceController extends GetxController {
|
|||||||
dateTo: dateTo,
|
dateTo: dateTo,
|
||||||
);
|
);
|
||||||
|
|
||||||
isLoadingAttendanceLogs.value = false;
|
|
||||||
isLoading.value = false;
|
|
||||||
|
|
||||||
if (response != null) {
|
if (response != null) {
|
||||||
attendanceLogs =
|
attendanceLogs =
|
||||||
response.map((json) => AttendanceLogModel.fromJson(json)).toList();
|
response.map((json) => AttendanceLogModel.fromJson(json)).toList();
|
||||||
@ -271,6 +294,8 @@ class AttendanceController extends GetxController {
|
|||||||
} else {
|
} else {
|
||||||
log.e("Failed to fetch attendance logs for project $projectId");
|
log.e("Failed to fetch attendance logs for project $projectId");
|
||||||
}
|
}
|
||||||
|
isLoadingAttendanceLogs.value = false;
|
||||||
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, List<AttendanceLogModel>> groupLogsByCheckInDate() {
|
Map<String, List<AttendanceLogModel>> groupLogsByCheckInDate() {
|
||||||
@ -284,8 +309,6 @@ class AttendanceController extends GetxController {
|
|||||||
groupedLogs.putIfAbsent(checkInDate, () => []);
|
groupedLogs.putIfAbsent(checkInDate, () => []);
|
||||||
groupedLogs[checkInDate]!.add(logItem);
|
groupedLogs[checkInDate]!.add(logItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort by date descending
|
|
||||||
final sortedEntries = groupedLogs.entries.toList()
|
final sortedEntries = groupedLogs.entries.toList()
|
||||||
..sort((a, b) {
|
..sort((a, b) {
|
||||||
if (a.key == 'Unknown') return 1;
|
if (a.key == 'Unknown') return 1;
|
||||||
@ -309,14 +332,11 @@ class AttendanceController extends GetxController {
|
|||||||
}) async {
|
}) async {
|
||||||
if (projectId == null) return;
|
if (projectId == null) return;
|
||||||
|
|
||||||
isLoading.value = true;
|
|
||||||
isLoadingRegularizationLogs.value = true;
|
isLoadingRegularizationLogs.value = true;
|
||||||
|
isLoading.value = true;
|
||||||
|
|
||||||
final response = await ApiService.getRegularizationLogs(projectId);
|
final response = await ApiService.getRegularizationLogs(projectId);
|
||||||
|
|
||||||
isLoadingRegularizationLogs.value = false;
|
|
||||||
isLoading.value = false;
|
|
||||||
|
|
||||||
if (response != null) {
|
if (response != null) {
|
||||||
regularizationLogs = response
|
regularizationLogs = response
|
||||||
.map((json) => RegularizationLogModel.fromJson(json))
|
.map((json) => RegularizationLogModel.fromJson(json))
|
||||||
@ -326,25 +346,23 @@ class AttendanceController extends GetxController {
|
|||||||
} else {
|
} else {
|
||||||
log.e("Failed to fetch regularization logs for project $projectId");
|
log.e("Failed to fetch regularization logs for project $projectId");
|
||||||
}
|
}
|
||||||
|
isLoadingRegularizationLogs.value = false;
|
||||||
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> fetchLogsView(String? id) async {
|
Future<void> fetchLogsView(String? id) async {
|
||||||
if (id == null) return;
|
if (id == null) return;
|
||||||
|
|
||||||
isLoading.value = true;
|
|
||||||
isLoadingLogView.value = true;
|
isLoadingLogView.value = true;
|
||||||
|
isLoading.value = true;
|
||||||
|
|
||||||
final response = await ApiService.getAttendanceLogView(id);
|
final response = await ApiService.getAttendanceLogView(id);
|
||||||
|
|
||||||
isLoadingLogView.value = false;
|
|
||||||
isLoading.value = false;
|
|
||||||
|
|
||||||
if (response != null) {
|
if (response != null) {
|
||||||
attendenceLogsView = response
|
attendenceLogsView = response
|
||||||
.map((json) => AttendanceLogViewModel.fromJson(json))
|
.map((json) => AttendanceLogViewModel.fromJson(json))
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
// Sort by activityTime field (latest first)
|
|
||||||
attendenceLogsView.sort((a, b) {
|
attendenceLogsView.sort((a, b) {
|
||||||
if (a.activityTime == null || b.activityTime == null) return 0;
|
if (a.activityTime == null || b.activityTime == null) return 0;
|
||||||
return b.activityTime!.compareTo(a.activityTime!);
|
return b.activityTime!.compareTo(a.activityTime!);
|
||||||
@ -355,5 +373,8 @@ class AttendanceController extends GetxController {
|
|||||||
} else {
|
} else {
|
||||||
log.e("Failed to fetch attendance log view for ID $id");
|
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;
|
DateTime? endDateTask;
|
||||||
|
|
||||||
List<TaskModel> dailyTasks = [];
|
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;
|
RxBool isLoading = false.obs;
|
||||||
|
Map<String, List<TaskModel>> groupedDailyTasks = {};
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
@ -50,47 +59,46 @@ class DailyTaskController extends GetxController {
|
|||||||
projects = response!.map((json) => ProjectModel.fromJson(json)).toList();
|
projects = response!.map((json) => ProjectModel.fromJson(json)).toList();
|
||||||
selectedProjectId = projects.first.id.toString();
|
selectedProjectId = projects.first.id.toString();
|
||||||
log.i("Projects fetched: ${projects.length} projects loaded.");
|
log.i("Projects fetched: ${projects.length} projects loaded.");
|
||||||
update();
|
update();
|
||||||
await fetchTaskData(selectedProjectId);
|
await fetchTaskData(selectedProjectId);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> fetchTaskData(String? projectId) async {
|
Future<void> fetchTaskData(String? projectId) async {
|
||||||
if (projectId == null) return;
|
if (projectId == null) return;
|
||||||
|
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
final response = await ApiService.getDailyTasks(
|
final response = await ApiService.getDailyTasks(
|
||||||
projectId,
|
projectId,
|
||||||
dateFrom: startDateTask,
|
dateFrom: startDateTask,
|
||||||
dateTo: endDateTask,
|
dateTo: endDateTask,
|
||||||
);
|
);
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
|
|
||||||
if (response != null) {
|
if (response != null) {
|
||||||
Map<String, List<TaskModel>> groupedTasks = {};
|
groupedDailyTasks.clear();
|
||||||
|
|
||||||
for (var taskJson in response) {
|
for (var taskJson in response) {
|
||||||
TaskModel task = TaskModel.fromJson(taskJson);
|
TaskModel task = TaskModel.fromJson(taskJson);
|
||||||
String assignmentDateKey = task.assignmentDate;
|
String assignmentDateKey =
|
||||||
|
task.assignmentDate.toIso8601String().split('T')[0];
|
||||||
|
|
||||||
if (groupedTasks.containsKey(assignmentDateKey)) {
|
if (groupedDailyTasks.containsKey(assignmentDateKey)) {
|
||||||
groupedTasks[assignmentDateKey]?.add(task);
|
groupedDailyTasks[assignmentDateKey]?.add(task);
|
||||||
} else {
|
} else {
|
||||||
groupedTasks[assignmentDateKey] = [task];
|
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(
|
Future<void> selectDateRangeForTaskData(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
@ -101,7 +109,8 @@ Future<void> fetchTaskData(String? projectId) async {
|
|||||||
firstDate: DateTime(2022),
|
firstDate: DateTime(2022),
|
||||||
lastDate: DateTime.now(),
|
lastDate: DateTime.now(),
|
||||||
initialDateRange: DateTimeRange(
|
initialDateRange: DateTimeRange(
|
||||||
start: startDateTask ?? DateTime.now().subtract(const Duration(days: 7)),
|
start:
|
||||||
|
startDateTask ?? DateTime.now().subtract(const Duration(days: 7)),
|
||||||
end: endDateTask ?? DateTime.now(),
|
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/attendance_model.dart';
|
||||||
import 'package:marco/model/project_model.dart';
|
import 'package:marco/model/project_model.dart';
|
||||||
import 'package:marco/model/employee_model.dart';
|
import 'package:marco/model/employee_model.dart';
|
||||||
|
import 'package:marco/model/employees/employee_details_model.dart';
|
||||||
|
|
||||||
final Logger log = Logger();
|
final Logger log = Logger();
|
||||||
|
|
||||||
@ -12,14 +13,18 @@ class EmployeesScreenController extends GetxController {
|
|||||||
List<ProjectModel> projects = [];
|
List<ProjectModel> projects = [];
|
||||||
String? selectedProjectId;
|
String? selectedProjectId;
|
||||||
List<EmployeeModel> employees = [];
|
List<EmployeeModel> employees = [];
|
||||||
|
List<EmployeeDetailsModel> employeeDetails = [];
|
||||||
|
|
||||||
RxBool isLoading = false.obs;
|
RxBool isLoading = false.obs;
|
||||||
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
|
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
|
||||||
|
Rxn<EmployeeDetailsModel> selectedEmployeeDetails =
|
||||||
|
Rxn<EmployeeDetailsModel>();
|
||||||
|
RxBool isLoadingEmployeeDetails = false.obs;
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
fetchAllProjects();
|
fetchAllProjects();
|
||||||
|
fetchAllEmployees();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> fetchAllProjects() async {
|
Future<void> fetchAllProjects() async {
|
||||||
@ -69,8 +74,8 @@ class EmployeesScreenController extends GetxController {
|
|||||||
},
|
},
|
||||||
onEmpty: () {
|
onEmpty: () {
|
||||||
log.w("No employees found for project $projectId.");
|
log.w("No employees found for project $projectId.");
|
||||||
employees = [];
|
employees = [];
|
||||||
update();
|
update();
|
||||||
},
|
},
|
||||||
onError: (e) =>
|
onError: (e) =>
|
||||||
log.e("Error fetching employees for project $projectId: $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:marco/helpers/services/api_service.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:logger/logger.dart';
|
import 'package:logger/logger.dart';
|
||||||
|
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||||
|
import 'package:marco/controller/task_planing/daily_task_planing_controller.dart';
|
||||||
|
|
||||||
final Logger logger = Logger();
|
final Logger logger = Logger();
|
||||||
|
|
||||||
enum ApiStatus { idle, loading, success, failure }
|
enum ApiStatus { idle, loading, success, failure }
|
||||||
|
final DailyTaskPlaningController taskController = Get.put(DailyTaskPlaningController());
|
||||||
class ReportTaskController extends MyController {
|
class ReportTaskController extends MyController {
|
||||||
List<PlatformFile> files = [];
|
List<PlatformFile> files = [];
|
||||||
MyFormValidator basicValidator = MyFormValidator();
|
MyFormValidator basicValidator = MyFormValidator();
|
||||||
@ -96,18 +100,30 @@ class ReportTaskController extends MyController {
|
|||||||
basicValidator.getController('completed_work')?.text.trim();
|
basicValidator.getController('completed_work')?.text.trim();
|
||||||
|
|
||||||
if (completedWork == null || completedWork.isEmpty) {
|
if (completedWork == null || completedWork.isEmpty) {
|
||||||
Get.snackbar("Error", "Completed work is required.");
|
showAppSnackbar(
|
||||||
|
title: "Error",
|
||||||
|
message: "Completed work is required.",
|
||||||
|
type: SnackbarType.error,
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final completedWorkInt = int.tryParse(completedWork);
|
final completedWorkInt = int.tryParse(completedWork);
|
||||||
if (completedWorkInt == null || completedWorkInt <= 0) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
final commentField = basicValidator.getController('comment')?.text.trim();
|
final commentField = basicValidator.getController('comment')?.text.trim();
|
||||||
|
|
||||||
if (commentField == null || commentField.isEmpty) {
|
if (commentField == null || commentField.isEmpty) {
|
||||||
Get.snackbar("Error", "Comment is required.");
|
showAppSnackbar(
|
||||||
|
title: "Error",
|
||||||
|
message: "Comment is required.",
|
||||||
|
type: SnackbarType.error,
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,13 +138,25 @@ class ReportTaskController extends MyController {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
Get.snackbar("Success", "Task reported successfully!");
|
showAppSnackbar(
|
||||||
|
title: "Success",
|
||||||
|
message: "Task reported successfully!",
|
||||||
|
type: SnackbarType.success,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
Get.snackbar("Error", "Failed to report task.");
|
showAppSnackbar(
|
||||||
|
title: "Error",
|
||||||
|
message: "Failed to report task.",
|
||||||
|
type: SnackbarType.error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.e("Error reporting task: $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 {
|
} finally {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
@ -137,23 +165,17 @@ class ReportTaskController extends MyController {
|
|||||||
Future<void> commentTask({
|
Future<void> commentTask({
|
||||||
required String projectId,
|
required String projectId,
|
||||||
required String comment,
|
required String comment,
|
||||||
required int completedTask,
|
|
||||||
required List<Map<String, dynamic>> checklist,
|
|
||||||
required DateTime reportedDate,
|
|
||||||
}) async {
|
}) 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();
|
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) {
|
if (commentField == null || commentField.isEmpty) {
|
||||||
Get.snackbar("Error", "Comment is required.");
|
showAppSnackbar(
|
||||||
|
title: "Error",
|
||||||
|
message: "Comment is required.",
|
||||||
|
type: SnackbarType.error,
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -166,13 +188,27 @@ class ReportTaskController extends MyController {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
Get.snackbar("Success", "Task commented successfully!");
|
showAppSnackbar(
|
||||||
|
title: "Success",
|
||||||
|
message: "Task commented successfully!",
|
||||||
|
type: SnackbarType.success,
|
||||||
|
);
|
||||||
|
|
||||||
|
await taskController.fetchTaskData(projectId);
|
||||||
} else {
|
} else {
|
||||||
Get.snackbar("Error", "Failed to comment task.");
|
showAppSnackbar(
|
||||||
|
title: "Error",
|
||||||
|
message: "Failed to comment task.",
|
||||||
|
type: SnackbarType.error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.e("Error commenting task: $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 {
|
} finally {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
|
@ -14,9 +14,12 @@ class ApiEndpoints {
|
|||||||
static const String getAllEmployees = "/employee/list";
|
static const String getAllEmployees = "/employee/list";
|
||||||
static const String getRoles = "/roles/jobrole";
|
static const String getRoles = "/roles/jobrole";
|
||||||
static const String createEmployee = "/employee/manage";
|
static const String createEmployee = "/employee/manage";
|
||||||
|
static const String getEmployeeInfo = "/employee/profile/get";
|
||||||
|
|
||||||
// Daily Task Screen API Endpoints
|
// Daily Task Screen API Endpoints
|
||||||
static const String getDailyTask = "/task/list";
|
static const String getDailyTask = "/task/list";
|
||||||
static const String reportTask = "/task/report";
|
static const String reportTask = "/task/report";
|
||||||
static const String commentTask = "task/comment";
|
static const String commentTask = "/task/comment";
|
||||||
|
static const String dailyTaskDetails = "/project/details";
|
||||||
|
static const String assignDailyTask = "/task/assign";
|
||||||
}
|
}
|
||||||
|
@ -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/auth_service.dart';
|
||||||
import 'package:marco/helpers/services/api_endpoints.dart';
|
import 'package:marco/helpers/services/api_endpoints.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
final Logger logger = Logger();
|
final Logger logger = Logger();
|
||||||
|
|
||||||
class ApiService {
|
class ApiService {
|
||||||
@ -46,6 +47,21 @@ class ApiService {
|
|||||||
return null;
|
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,
|
static Future<http.Response?> _getRequest(String endpoint,
|
||||||
{Map<String, String>? queryParams, bool hasRetried = false}) async {
|
{Map<String, String>? queryParams, bool hasRetried = false}) async {
|
||||||
String? token = await _getToken();
|
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 =====
|
// ===== Daily Tasks API Calls =====
|
||||||
static Future<List<dynamic>?> getDailyTasks(String projectId,
|
static Future<List<dynamic>?> getDailyTasks(String projectId,
|
||||||
{DateTime? dateFrom, DateTime? dateTo}) async {
|
{DateTime? dateFrom, DateTime? dateTo}) async {
|
||||||
@ -339,6 +369,7 @@ class ApiService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<bool> commentTask({
|
static Future<bool> commentTask({
|
||||||
required String id,
|
required String id,
|
||||||
required String comment,
|
required String comment,
|
||||||
@ -359,11 +390,58 @@ class ApiService {
|
|||||||
final json = jsonDecode(response.body);
|
final json = jsonDecode(response.body);
|
||||||
|
|
||||||
if (response.statusCode == 200 && json['success'] == true) {
|
if (response.statusCode == 200 && json['success'] == true) {
|
||||||
Get.back();
|
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
_log("Failed to comment task: ${json['message'] ?? 'Unknown error'}");
|
_log("Failed to comment task: ${json['message'] ?? 'Unknown error'}");
|
||||||
return false;
|
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:get/get.dart';
|
||||||
import 'package:marco/helpers/services/storage/local_storage.dart';
|
import 'package:marco/helpers/services/storage/local_storage.dart';
|
||||||
import 'package:marco/controller/permission_controller.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();
|
final Logger logger = Logger();
|
||||||
|
|
||||||
class AuthService {
|
class AuthService {
|
||||||
@ -13,8 +12,6 @@ class AuthService {
|
|||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
};
|
};
|
||||||
static bool isLoggedIn = false;
|
static bool isLoggedIn = false;
|
||||||
|
|
||||||
/// Logs in the user and stores tokens if successful.
|
|
||||||
static Future<Map<String, String>?> loginUser(
|
static Future<Map<String, String>?> loginUser(
|
||||||
Map<String, dynamic> data) async {
|
Map<String, dynamic> data) async {
|
||||||
try {
|
try {
|
||||||
@ -44,7 +41,7 @@ class AuthService {
|
|||||||
|
|
||||||
Get.put(PermissionController());
|
Get.put(PermissionController());
|
||||||
|
|
||||||
return null; // Success
|
return null;
|
||||||
} else if (response.statusCode == 401) {
|
} else if (response.statusCode == 401) {
|
||||||
return {"password": "Invalid email or password"};
|
return {"password": "Invalid email or password"};
|
||||||
} else {
|
} 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:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:intl/intl.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/controller/dashboard/attendance_screen_controller.dart';
|
||||||
import 'package:marco/helpers/utils/attendance_actions.dart';
|
import 'package:marco/helpers/utils/attendance_actions.dart';
|
||||||
|
|
||||||
@ -19,6 +19,98 @@ class AttendanceActionButton extends StatefulWidget {
|
|||||||
State<AttendanceActionButton> createState() => _AttendanceActionButtonState();
|
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> {
|
class _AttendanceActionButtonState extends State<AttendanceActionButton> {
|
||||||
late final String uniqueLogKey;
|
late final String uniqueLogKey;
|
||||||
|
|
||||||
@ -28,7 +120,6 @@ class _AttendanceActionButtonState extends State<AttendanceActionButton> {
|
|||||||
uniqueLogKey = AttendanceButtonHelper.getUniqueKey(
|
uniqueLogKey = AttendanceButtonHelper.getUniqueKey(
|
||||||
widget.employee.employeeId, widget.employee.id);
|
widget.employee.employeeId, widget.employee.id);
|
||||||
|
|
||||||
// Defer the Rx initialization after first frame to avoid setState during build
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
if (!widget.attendanceController.uploadingStates
|
if (!widget.attendanceController.uploadingStates
|
||||||
.containsKey(uniqueLogKey)) {
|
.containsKey(uniqueLogKey)) {
|
||||||
@ -58,9 +149,10 @@ class _AttendanceActionButtonState extends State<AttendanceActionButton> {
|
|||||||
if (selectedDateTime.isAfter(checkInTime)) {
|
if (selectedDateTime.isAfter(checkInTime)) {
|
||||||
return selectedDateTime;
|
return selectedDateTime;
|
||||||
} else {
|
} else {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
showAppSnackbar(
|
||||||
const SnackBar(
|
title: "Invalid Time",
|
||||||
content: Text("Please select a time after check-in time.")),
|
message: "Please select a time after check-in time.",
|
||||||
|
type: SnackbarType.warning,
|
||||||
);
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -69,14 +161,13 @@ class _AttendanceActionButtonState extends State<AttendanceActionButton> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _handleButtonPressed(BuildContext context) async {
|
void _handleButtonPressed(BuildContext context) async {
|
||||||
// Set uploading state true safely
|
|
||||||
widget.attendanceController.uploadingStates[uniqueLogKey]?.value = true;
|
widget.attendanceController.uploadingStates[uniqueLogKey]?.value = true;
|
||||||
|
|
||||||
if (widget.attendanceController.selectedProjectId == null) {
|
if (widget.attendanceController.selectedProjectId == null) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
showAppSnackbar(
|
||||||
const SnackBar(
|
title: "Project Required",
|
||||||
content: Text("Please select a project first"),
|
message: "Please select a project first",
|
||||||
),
|
type: SnackbarType.error,
|
||||||
);
|
);
|
||||||
widget.attendanceController.uploadingStates[uniqueLogKey]?.value = false;
|
widget.attendanceController.uploadingStates[uniqueLogKey]?.value = false;
|
||||||
return;
|
return;
|
||||||
@ -122,6 +213,12 @@ class _AttendanceActionButtonState extends State<AttendanceActionButton> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final userComment = await _showCommentBottomSheet(context, actionText);
|
||||||
|
if (userComment == null || userComment.isEmpty) {
|
||||||
|
widget.attendanceController.uploadingStates[uniqueLogKey]?.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
bool success = false;
|
bool success = false;
|
||||||
if (actionText == ButtonActions.requestRegularize) {
|
if (actionText == ButtonActions.requestRegularize) {
|
||||||
final selectedTime = await showTimePickerForRegularization(
|
final selectedTime = await showTimePickerForRegularization(
|
||||||
@ -135,37 +232,31 @@ class _AttendanceActionButtonState extends State<AttendanceActionButton> {
|
|||||||
widget.employee.id,
|
widget.employee.id,
|
||||||
widget.employee.employeeId,
|
widget.employee.employeeId,
|
||||||
widget.attendanceController.selectedProjectId!,
|
widget.attendanceController.selectedProjectId!,
|
||||||
comment: actionText,
|
comment: userComment,
|
||||||
action: updatedAction,
|
action: updatedAction,
|
||||||
imageCapture: imageCapture,
|
imageCapture: imageCapture,
|
||||||
markTime: formattedSelectedTime,
|
markTime: formattedSelectedTime,
|
||||||
);
|
);
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text(success
|
|
||||||
? '${actionText.toLowerCase()} marked successfully!'
|
|
||||||
: 'Failed to ${actionText.toLowerCase()}'),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
success = await widget.attendanceController.captureAndUploadAttendance(
|
success = await widget.attendanceController.captureAndUploadAttendance(
|
||||||
widget.employee.id,
|
widget.employee.id,
|
||||||
widget.employee.employeeId,
|
widget.employee.employeeId,
|
||||||
widget.attendanceController.selectedProjectId!,
|
widget.attendanceController.selectedProjectId!,
|
||||||
comment: actionText,
|
comment: userComment,
|
||||||
action: updatedAction,
|
action: updatedAction,
|
||||||
imageCapture: imageCapture,
|
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;
|
widget.attendanceController.uploadingStates[uniqueLogKey]?.value = false;
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:marco/controller/permission_controller.dart';
|
import 'package:marco/controller/permission_controller.dart';
|
||||||
import 'package:marco/controller/dashboard/attendance_screen_controller.dart';
|
import 'package:marco/controller/dashboard/attendance_screen_controller.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:marco/helpers/widgets/my_text.dart';
|
||||||
|
|
||||||
class AttendanceFilterBottomSheet extends StatelessWidget {
|
class AttendanceFilterBottomSheet extends StatelessWidget {
|
||||||
final AttendanceController controller;
|
final AttendanceController controller;
|
||||||
@ -23,7 +24,7 @@ class AttendanceFilterBottomSheet extends StatelessWidget {
|
|||||||
final end = DateFormat('dd MM yyyy').format(endDate);
|
final end = DateFormat('dd MM yyyy').format(endDate);
|
||||||
return "$start - $end";
|
return "$start - $end";
|
||||||
}
|
}
|
||||||
return "Select Date Range";
|
return "Date Range";
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -72,16 +73,17 @@ class AttendanceFilterBottomSheet extends StatelessWidget {
|
|||||||
)
|
)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
final selectedProjectName =
|
final selectedProjectName = selectedProject?.name ?? "Select Project";
|
||||||
selectedProject?.name ?? "Select Project";
|
|
||||||
|
|
||||||
filterWidgets = [
|
filterWidgets = [
|
||||||
const Padding(
|
Padding(
|
||||||
padding: EdgeInsets.fromLTRB(16, 12, 16, 4),
|
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
|
||||||
child: Align(
|
child: Align(
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
child: Text('Select Project',
|
child: MyText.titleSmall(
|
||||||
style: TextStyle(fontWeight: FontWeight.bold)),
|
"Project",
|
||||||
|
fontWeight: 600,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
@ -92,18 +94,23 @@ class AttendanceFilterBottomSheet extends StatelessWidget {
|
|||||||
onTap: () => setState(() => showProjectList = true),
|
onTap: () => setState(() => showProjectList = true),
|
||||||
),
|
),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
const Padding(
|
Padding(
|
||||||
padding: EdgeInsets.fromLTRB(16, 12, 16, 4),
|
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
|
||||||
child: Align(
|
child: Align(
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
child: Text('Select View',
|
child: MyText.titleSmall(
|
||||||
style: TextStyle(fontWeight: FontWeight.bold)),
|
"View",
|
||||||
|
fontWeight: 600,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
...[
|
...[
|
||||||
{'label': 'Today\'s Attendance', 'value': 'todaysAttendance'},
|
{'label': 'Today\'s Attendance', 'value': 'todaysAttendance'},
|
||||||
{'label': 'Attendance Logs', 'value': 'attendanceLogs'},
|
{'label': 'Attendance Logs', 'value': 'attendanceLogs'},
|
||||||
{'label': 'Regularization Requests', 'value': 'regularizationRequests'},
|
{
|
||||||
|
'label': 'Regularization Requests',
|
||||||
|
'value': 'regularizationRequests'
|
||||||
|
},
|
||||||
].map((item) {
|
].map((item) {
|
||||||
return RadioListTile<String>(
|
return RadioListTile<String>(
|
||||||
dense: true,
|
dense: true,
|
||||||
@ -119,13 +126,13 @@ class AttendanceFilterBottomSheet extends StatelessWidget {
|
|||||||
if (tempSelectedTab == 'attendanceLogs') {
|
if (tempSelectedTab == 'attendanceLogs') {
|
||||||
filterWidgets.addAll([
|
filterWidgets.addAll([
|
||||||
const Divider(),
|
const Divider(),
|
||||||
const Padding(
|
Padding(
|
||||||
padding: EdgeInsets.fromLTRB(16, 12, 16, 4),
|
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
|
||||||
child: Align(
|
child: Align(
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
child: Text(
|
child: MyText.titleSmall(
|
||||||
"Select Date Range",
|
"Date Range",
|
||||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
fontWeight: 600,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -139,7 +146,7 @@ class AttendanceFilterBottomSheet extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
child: Ink(
|
child: Ink(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.grey.shade100,
|
color: const Color.fromARGB(255, 255, 255, 255),
|
||||||
border: Border.all(color: Colors.grey.shade400),
|
border: Border.all(color: Colors.grey.shade400),
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
@ -147,7 +154,8 @@ class AttendanceFilterBottomSheet extends StatelessWidget {
|
|||||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
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),
|
const SizedBox(width: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
@ -160,7 +168,8 @@ class AttendanceFilterBottomSheet extends StatelessWidget {
|
|||||||
overflow: TextOverflow.ellipsis,
|
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(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
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,
|
...filterWidgets,
|
||||||
const Divider(),
|
const Divider(),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Color.fromARGB(255, 95, 132, 255),
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:marco/helpers/utils/attendance_actions.dart';
|
import 'package:marco/helpers/utils/attendance_actions.dart';
|
||||||
|
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||||
enum ButtonActions { approve, reject }
|
enum ButtonActions { approve, reject }
|
||||||
|
|
||||||
class RegularizeActionButton extends StatefulWidget {
|
class RegularizeActionButton extends StatefulWidget {
|
||||||
final dynamic attendanceController; // Replace dynamic with your controller's type
|
final dynamic
|
||||||
final dynamic log; // Replace dynamic with your log model type
|
attendanceController;
|
||||||
|
final dynamic log;
|
||||||
final String uniqueLogKey;
|
final String uniqueLogKey;
|
||||||
final ButtonActions action;
|
final ButtonActions action;
|
||||||
|
|
||||||
@ -21,6 +22,11 @@ class RegularizeActionButton extends StatefulWidget {
|
|||||||
State<RegularizeActionButton> createState() => _RegularizeActionButtonState();
|
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> {
|
class _RegularizeActionButtonState extends State<RegularizeActionButton> {
|
||||||
bool isUploading = false;
|
bool isUploading = false;
|
||||||
|
|
||||||
@ -41,13 +47,16 @@ class _RegularizeActionButtonState extends State<RegularizeActionButton> {
|
|||||||
|
|
||||||
Color get backgroundColor {
|
Color get backgroundColor {
|
||||||
// Use string keys for correct color lookup
|
// 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 {
|
Future<void> _handlePress() async {
|
||||||
if (widget.attendanceController.selectedProjectId == null) {
|
if (widget.attendanceController.selectedProjectId == null) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
showAppSnackbar(
|
||||||
const SnackBar(content: Text("Please select a project first")),
|
title: 'Warning',
|
||||||
|
message: 'Please select a project first',
|
||||||
|
type: SnackbarType.warning,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -56,9 +65,11 @@ class _RegularizeActionButtonState extends State<RegularizeActionButton> {
|
|||||||
isUploading = true;
|
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.id,
|
||||||
widget.log.employeeId,
|
widget.log.employeeId,
|
||||||
widget.attendanceController.selectedProjectId!,
|
widget.attendanceController.selectedProjectId!,
|
||||||
@ -67,22 +78,27 @@ class _RegularizeActionButtonState extends State<RegularizeActionButton> {
|
|||||||
imageCapture: false,
|
imageCapture: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
showAppSnackbar(
|
||||||
SnackBar(
|
title: success ? 'Success' : 'Error',
|
||||||
content: Text(success
|
message: success
|
||||||
? '${_buttonTexts[widget.action]} marked successfully!'
|
? '${capitalizeFirstLetter(_buttonTexts[widget.action]!)} marked successfully!'
|
||||||
: 'Failed to mark ${_buttonTexts[widget.action]}.'),
|
: 'Failed to mark ${capitalizeFirstLetter(_buttonTexts[widget.action]!)}.',
|
||||||
),
|
type: success ? SnackbarType.success : SnackbarType.error,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
widget.attendanceController.fetchEmployeesByProject(widget.attendanceController.selectedProjectId!);
|
widget.attendanceController.fetchEmployeesByProject(
|
||||||
widget.attendanceController.fetchAttendanceLogs(widget.attendanceController.selectedProjectId!);
|
widget.attendanceController.selectedProjectId!);
|
||||||
await widget.attendanceController.fetchRegularizationLogs(widget.attendanceController.selectedProjectId!);
|
widget.attendanceController
|
||||||
await widget.attendanceController.fetchProjectData(widget.attendanceController.selectedProjectId!);
|
.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(() {
|
setState(() {
|
||||||
isUploading = false;
|
isUploading = false;
|
||||||
@ -101,7 +117,8 @@ class _RegularizeActionButtonState extends State<RegularizeActionButton> {
|
|||||||
onPressed: isUploading ? null : _handlePress,
|
onPressed: isUploading ? null : _handlePress,
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: backgroundColor,
|
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),
|
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 6),
|
||||||
minimumSize: const Size(60, 20),
|
minimumSize: const Size(60, 20),
|
||||||
textStyle: const TextStyle(fontSize: 12),
|
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 {
|
class TaskModel {
|
||||||
final String assignmentDate;
|
final DateTime assignmentDate;
|
||||||
|
final DateTime? reportedDate;
|
||||||
final String id;
|
final String id;
|
||||||
final WorkItem? workItem;
|
final WorkItem? workItem;
|
||||||
final String workItemId;
|
final String workItemId;
|
||||||
@ -9,11 +10,9 @@ class TaskModel {
|
|||||||
final List<TeamMember> teamMembers;
|
final List<TeamMember> teamMembers;
|
||||||
final List<Comment> comments;
|
final List<Comment> comments;
|
||||||
|
|
||||||
int get plannedWork => workItem?.plannedWork ?? 0;
|
|
||||||
int get completedWork => workItem?.completedWork ?? 0;
|
|
||||||
|
|
||||||
TaskModel({
|
TaskModel({
|
||||||
required this.assignmentDate,
|
required this.assignmentDate,
|
||||||
|
this.reportedDate,
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.workItem,
|
required this.workItem,
|
||||||
required this.workItemId,
|
required this.workItemId,
|
||||||
@ -25,15 +24,15 @@ class TaskModel {
|
|||||||
});
|
});
|
||||||
|
|
||||||
factory TaskModel.fromJson(Map<String, dynamic> json) {
|
factory TaskModel.fromJson(Map<String, dynamic> json) {
|
||||||
final workItemJson = json['workItem'];
|
|
||||||
final workItem =
|
|
||||||
workItemJson != null ? WorkItem.fromJson(workItemJson) : null;
|
|
||||||
|
|
||||||
return TaskModel(
|
return TaskModel(
|
||||||
assignmentDate: json['assignmentDate'],
|
assignmentDate: DateTime.parse(json['assignmentDate']),
|
||||||
id: json['id'] ?? '',
|
reportedDate: json['reportedDate'] != null
|
||||||
|
? DateTime.tryParse(json['reportedDate'])
|
||||||
|
: null,
|
||||||
|
id: json['id'],
|
||||||
|
workItem:
|
||||||
|
json['workItem'] != null ? WorkItem.fromJson(json['workItem']) : null,
|
||||||
workItemId: json['workItemId'],
|
workItemId: json['workItemId'],
|
||||||
workItem: workItem,
|
|
||||||
plannedTask: json['plannedTask'],
|
plannedTask: json['plannedTask'],
|
||||||
completedTask: json['completedTask'],
|
completedTask: json['completedTask'],
|
||||||
assignedBy: AssignedBy.fromJson(json['assignedBy']),
|
assignedBy: AssignedBy.fromJson(json['assignedBy']),
|
||||||
@ -47,12 +46,14 @@ class TaskModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class WorkItem {
|
class WorkItem {
|
||||||
|
final String? id;
|
||||||
final ActivityMaster? activityMaster;
|
final ActivityMaster? activityMaster;
|
||||||
final WorkArea? workArea;
|
final WorkArea? workArea;
|
||||||
final int? plannedWork;
|
final int? plannedWork;
|
||||||
final int? completedWork;
|
final int? completedWork;
|
||||||
|
|
||||||
WorkItem({
|
WorkItem({
|
||||||
|
this.id,
|
||||||
this.activityMaster,
|
this.activityMaster,
|
||||||
this.workArea,
|
this.workArea,
|
||||||
this.plannedWork,
|
this.plannedWork,
|
||||||
@ -61,6 +62,7 @@ class WorkItem {
|
|||||||
|
|
||||||
factory WorkItem.fromJson(Map<String, dynamic> json) {
|
factory WorkItem.fromJson(Map<String, dynamic> json) {
|
||||||
return WorkItem(
|
return WorkItem(
|
||||||
|
id: json['id']?.toString(),
|
||||||
activityMaster: json['activityMaster'] != null
|
activityMaster: json['activityMaster'] != null
|
||||||
? ActivityMaster.fromJson(json['activityMaster'])
|
? ActivityMaster.fromJson(json['activityMaster'])
|
||||||
: null,
|
: null,
|
||||||
@ -78,7 +80,7 @@ class ActivityMaster {
|
|||||||
ActivityMaster({required this.activityName});
|
ActivityMaster({required this.activityName});
|
||||||
|
|
||||||
factory ActivityMaster.fromJson(Map<String, dynamic> json) {
|
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) {
|
factory WorkArea.fromJson(Map<String, dynamic> json) {
|
||||||
return WorkArea(
|
return WorkArea(
|
||||||
areaName: json['areaName'],
|
areaName: json['areaName'] ?? '',
|
||||||
floor: json['floor'] != null ? Floor.fromJson(json['floor']) : null,
|
floor: json['floor'] != null ? Floor.fromJson(json['floor']) : null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -104,7 +106,7 @@ class Floor {
|
|||||||
|
|
||||||
factory Floor.fromJson(Map<String, dynamic> json) {
|
factory Floor.fromJson(Map<String, dynamic> json) {
|
||||||
return Floor(
|
return Floor(
|
||||||
floorName: json['floorName'],
|
floorName: json['floorName'] ?? '',
|
||||||
building:
|
building:
|
||||||
json['building'] != null ? Building.fromJson(json['building']) : null,
|
json['building'] != null ? Building.fromJson(json['building']) : null,
|
||||||
);
|
);
|
||||||
@ -117,34 +119,46 @@ class Building {
|
|||||||
Building({required this.name});
|
Building({required this.name});
|
||||||
|
|
||||||
factory Building.fromJson(Map<String, dynamic> json) {
|
factory Building.fromJson(Map<String, dynamic> json) {
|
||||||
return Building(name: json['name']);
|
return Building(name: json['name'] ?? '');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AssignedBy {
|
class AssignedBy {
|
||||||
|
final String id;
|
||||||
final String firstName;
|
final String firstName;
|
||||||
final String? lastName;
|
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) {
|
factory AssignedBy.fromJson(Map<String, dynamic> json) {
|
||||||
return AssignedBy(
|
return AssignedBy(
|
||||||
firstName: json['firstName'],
|
id: json['id']?.toString() ?? '',
|
||||||
|
firstName: json['firstName'] ?? '',
|
||||||
lastName: json['lastName'],
|
lastName: json['lastName'],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TeamMember {
|
class TeamMember {
|
||||||
|
final String id;
|
||||||
final String firstName;
|
final String firstName;
|
||||||
final String? lastName;
|
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) {
|
factory TeamMember.fromJson(Map<String, dynamic> json) {
|
||||||
return TeamMember(
|
return TeamMember(
|
||||||
firstName: json['firstName'],
|
id: json['id']?.toString() ?? '',
|
||||||
lastName: json['lastName'],
|
firstName: json['firstName']?.toString() ?? '',
|
||||||
|
lastName: json['lastName']?.toString(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -152,7 +166,7 @@ class TeamMember {
|
|||||||
class Comment {
|
class Comment {
|
||||||
final String comment;
|
final String comment;
|
||||||
final TeamMember commentedBy;
|
final TeamMember commentedBy;
|
||||||
final String timestamp;
|
final DateTime timestamp;
|
||||||
|
|
||||||
Comment({
|
Comment({
|
||||||
required this.comment,
|
required this.comment,
|
||||||
@ -162,9 +176,11 @@ class Comment {
|
|||||||
|
|
||||||
factory Comment.fromJson(Map<String, dynamic> json) {
|
factory Comment.fromJson(Map<String, dynamic> json) {
|
||||||
return Comment(
|
return Comment(
|
||||||
comment: json['comment'],
|
comment: json['comment']?.toString() ?? '',
|
||||||
commentedBy: TeamMember.fromJson(json['employee']),
|
commentedBy: json['employee'] != null
|
||||||
timestamp: json['commentDate'],
|
? 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 jobRole;
|
||||||
final String email;
|
final String email;
|
||||||
final String phoneNumber;
|
final String phoneNumber;
|
||||||
|
final String jobRoleID;
|
||||||
|
|
||||||
EmployeeModel({
|
EmployeeModel({
|
||||||
required this.id,
|
required this.id,
|
||||||
|
required this.jobRoleID,
|
||||||
required this.employeeId,
|
required this.employeeId,
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.designation,
|
required this.designation,
|
||||||
@ -33,6 +35,7 @@ class EmployeeModel {
|
|||||||
return EmployeeModel(
|
return EmployeeModel(
|
||||||
id: json['id']?.toString() ?? '',
|
id: json['id']?.toString() ?? '',
|
||||||
employeeId: json['employeeId']?.toString() ?? '',
|
employeeId: json['employeeId']?.toString() ?? '',
|
||||||
|
jobRoleID: json['jobRoleId']?.toString() ?? '',
|
||||||
name: '${json['firstName'] ?? ''} ${json['lastName'] ?? ''}'.trim(),
|
name: '${json['firstName'] ?? ''} ${json['lastName'] ?? ''}'.trim(),
|
||||||
designation: json['jobRoleName'] ?? '',
|
designation: json['jobRoleName'] ?? '',
|
||||||
checkIn: json['checkInTime'] != null
|
checkIn: json['checkInTime'] != null
|
||||||
@ -57,6 +60,7 @@ class EmployeeModel {
|
|||||||
'employeeId': employeeId,
|
'employeeId': employeeId,
|
||||||
'firstName': name.split(' ').first,
|
'firstName': name.split(' ').first,
|
||||||
'lastName': name.split(' ').length > 1 ? name.split(' ').last : '',
|
'lastName': name.split(' ').length > 1 ? name.split(' ').last : '',
|
||||||
|
'jobRoleId': jobRoleID,
|
||||||
'jobRoleName': designation,
|
'jobRoleName': designation,
|
||||||
'checkInTime': checkIn?.toIso8601String(),
|
'checkInTime': checkIn?.toIso8601String(),
|
||||||
'checkOutTime': checkOut?.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/report_task_screen.dart';
|
||||||
import 'package:marco/view/taskPlaning/comment_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/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 {
|
class AuthMiddleware extends GetMiddleware {
|
||||||
@override
|
@override
|
||||||
@ -30,9 +33,8 @@ getPageRoute() {
|
|||||||
var routes = [
|
var routes = [
|
||||||
GetPage(
|
GetPage(
|
||||||
name: '/',
|
name: '/',
|
||||||
page: () => AttendanceScreen(),
|
page: () => AttendanceScreen(),
|
||||||
middlewares: [AuthMiddleware()]),
|
middlewares: [AuthMiddleware()]),
|
||||||
|
|
||||||
// Dashboard
|
// Dashboard
|
||||||
GetPage(
|
GetPage(
|
||||||
name: '/dashboard/attendance',
|
name: '/dashboard/attendance',
|
||||||
@ -44,7 +46,7 @@ getPageRoute() {
|
|||||||
middlewares: [AuthMiddleware()]),
|
middlewares: [AuthMiddleware()]),
|
||||||
GetPage(
|
GetPage(
|
||||||
name: '/dashboard/employees',
|
name: '/dashboard/employees',
|
||||||
page: () => EmployeeScreen(),
|
page: () => EmployeesScreen(),
|
||||||
middlewares: [AuthMiddleware()]),
|
middlewares: [AuthMiddleware()]),
|
||||||
// Employees Creation
|
// Employees Creation
|
||||||
GetPage(
|
GetPage(
|
||||||
@ -56,6 +58,14 @@ getPageRoute() {
|
|||||||
name: '/dashboard/daily-task',
|
name: '/dashboard/daily-task',
|
||||||
page: () => DailyTaskScreen(),
|
page: () => DailyTaskScreen(),
|
||||||
middlewares: [AuthMiddleware()]),
|
middlewares: [AuthMiddleware()]),
|
||||||
|
GetPage(
|
||||||
|
name: '/dashboard/daily-task-planing',
|
||||||
|
page: () => DailyTaskPlaningScreen(),
|
||||||
|
middlewares: [AuthMiddleware()]),
|
||||||
|
GetPage(
|
||||||
|
name: '/dashboard/daily-task-progress',
|
||||||
|
page: () => DailyProgressReportScreen(),
|
||||||
|
middlewares: [AuthMiddleware()]),
|
||||||
GetPage(
|
GetPage(
|
||||||
name: '/daily-task/report-task',
|
name: '/daily-task/report-task',
|
||||||
page: () => ReportTaskScreen(),
|
page: () => ReportTaskScreen(),
|
||||||
|
@ -181,7 +181,6 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
MySpacing.height(flexSpacing),
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: MySpacing.x(flexSpacing / 2),
|
padding: MySpacing.x(flexSpacing / 2),
|
||||||
child: MyFlex(children: [
|
child: MyFlex(children: [
|
||||||
@ -394,6 +393,7 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||||
child: Row(
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: MyText.titleMedium(
|
child: MyText.titleMedium(
|
||||||
@ -401,6 +401,29 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
|||||||
fontWeight: 600,
|
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 = {};
|
Map<String, List<dynamic>> groupedTasks = {};
|
||||||
for (var task in dailyTaskController.dailyTasks) {
|
for (var task in dailyTaskController.dailyTasks) {
|
||||||
String dateKey =
|
String dateKey =
|
||||||
DateFormat('dd-MM-yyyy').format(DateTime.parse(task.assignmentDate));
|
DateFormat('dd-MM-yyyy').format(task.assignmentDate);
|
||||||
groupedTasks.putIfAbsent(dateKey, () => []).add(task);
|
groupedTasks.putIfAbsent(dateKey, () => []).add(task);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -369,7 +369,7 @@ Widget _buildDateRangeButton() {
|
|||||||
'activity': activityName,
|
'activity': activityName,
|
||||||
'assigned': assigned,
|
'assigned': assigned,
|
||||||
'taskId': taskId,
|
'taskId': taskId,
|
||||||
'assignedBy': assignedBy,
|
'assignedBy': assignedBy,
|
||||||
'completedWork': completedWork,
|
'completedWork': completedWork,
|
||||||
'plannedWork': plannedWork,
|
'plannedWork': plannedWork,
|
||||||
'assignedOn': assignedOn,
|
'assignedOn': assignedOn,
|
||||||
|
@ -1,115 +1,111 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_lucide/flutter_lucide.dart';
|
import 'package:flutter_lucide/flutter_lucide.dart';
|
||||||
import 'package:get/get.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/mixins/ui_mixin.dart';
|
||||||
import 'package:marco/helpers/utils/my_shadow.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_card.dart';
|
||||||
import 'package:marco/helpers/widgets/my_container.dart';
|
import 'package:marco/helpers/widgets/my_container.dart';
|
||||||
import 'package:marco/helpers/widgets/my_spacing.dart';
|
import 'package:marco/helpers/widgets/my_spacing.dart';
|
||||||
import 'package:marco/helpers/widgets/my_text.dart';
|
import 'package:marco/helpers/widgets/my_text.dart';
|
||||||
import 'package:marco/view/layouts/layout.dart';
|
import 'package:marco/view/layouts/layout.dart';
|
||||||
|
|
||||||
class DashboardScreen extends StatelessWidget with UIMixin {
|
class DashboardScreen extends StatefulWidget {
|
||||||
DashboardScreen({super.key});
|
const DashboardScreen({super.key});
|
||||||
|
|
||||||
static const String dashboardRoute = "/dashboard";
|
static const String dashboardRoute = "/dashboard";
|
||||||
static const String employeesRoute = "/dashboard/employees";
|
static const String employeesRoute = "/dashboard/employees";
|
||||||
static const String projectsRoute = "/dashboard";
|
static const String projectsRoute = "/dashboard";
|
||||||
static const String attendanceRoute = "/dashboard/attendance";
|
static const String attendanceRoute = "/dashboard/attendance";
|
||||||
static const String tasksRoute = "/dashboard/daily-task";
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Layout(
|
return Layout(
|
||||||
child: Column(
|
child: SingleChildScrollView(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
padding: const EdgeInsets.all(12),
|
||||||
children: [
|
child: Column(
|
||||||
Padding(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
padding: MySpacing.x(flexSpacing),
|
children: [
|
||||||
child: Row(
|
MyText.titleMedium("Dashboard", fontWeight: 600),
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
MySpacing.height(12),
|
||||||
children: [
|
_buildDashboardStats(),
|
||||||
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(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> _buildDashboardStats() {
|
Widget _buildDashboardStats() {
|
||||||
final stats = [
|
final stats = [
|
||||||
_StatItem(
|
|
||||||
LucideIcons.gauge, "Dashboard", contentTheme.primary, dashboardRoute),
|
|
||||||
_StatItem(LucideIcons.scan_face, "Attendance", contentTheme.success,
|
_StatItem(LucideIcons.scan_face, "Attendance", contentTheme.success,
|
||||||
attendanceRoute),
|
DashboardScreen.attendanceRoute),
|
||||||
_StatItem(
|
_StatItem(LucideIcons.users, "Employees", contentTheme.warning,
|
||||||
LucideIcons.users, "Employees", contentTheme.warning, employeesRoute),
|
DashboardScreen.employeesRoute),
|
||||||
_StatItem(LucideIcons.logs, "Daily Progress Report", contentTheme.info,
|
|
||||||
tasksRoute),
|
|
||||||
_StatItem(LucideIcons.logs, "Daily Task Planing", contentTheme.info,
|
_StatItem(LucideIcons.logs, "Daily Task Planing", contentTheme.info,
|
||||||
tasksRoute),
|
DashboardScreen.dailyTasksRoute),
|
||||||
_StatItem(LucideIcons.folder, "Projects", contentTheme.secondary,
|
_StatItem(LucideIcons.list_todo, "Daily Task Progress", contentTheme.info,
|
||||||
projectsRoute),
|
DashboardScreen.dailyTasksProgressRoute),
|
||||||
];
|
];
|
||||||
|
|
||||||
return List.generate(
|
return LayoutBuilder(
|
||||||
(stats.length / 2).ceil(),
|
builder: (context, constraints) {
|
||||||
(index) => Row(
|
double maxWidth = constraints.maxWidth;
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
int crossAxisCount =
|
||||||
children: [
|
(maxWidth / 100).floor().clamp(2, 4);
|
||||||
_buildStatCard(stats[index * 2]),
|
double cardWidth =
|
||||||
if (index * 2 + 1 < stats.length)
|
(maxWidth - (crossAxisCount - 1) * 10) / crossAxisCount;
|
||||||
_buildStatCard(stats[index * 2 + 1]),
|
|
||||||
],
|
return Wrap(
|
||||||
),
|
spacing: 10,
|
||||||
|
runSpacing: 10,
|
||||||
|
children:
|
||||||
|
stats.map((stat) => _buildStatCard(stat, cardWidth)).toList(),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildStatCard(_StatItem statItem) {
|
Widget _buildStatCard(_StatItem statItem, double width) {
|
||||||
return Expanded(
|
return InkWell(
|
||||||
child: InkWell(
|
onTap: () => Get.toNamed(statItem.route),
|
||||||
onTap: () => Get.toNamed(statItem.route),
|
borderRadius: BorderRadius.circular(10),
|
||||||
child: MyCard.bordered(
|
child: MyCard.bordered(
|
||||||
borderRadiusAll: 10,
|
width: width,
|
||||||
border: Border.all(color: Colors.grey.withOpacity(0.2)),
|
height: 100,
|
||||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
paddingAll: 5,
|
||||||
paddingAll: 24,
|
borderRadiusAll: 10,
|
||||||
height: 140,
|
border: Border.all(color: Colors.grey.withOpacity(0.15)),
|
||||||
child: Column(
|
shadow: MyShadow(elevation: 1.5, position: MyShadowPosition.bottom),
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
child: Column(
|
||||||
children: [
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
_buildStatCardIcon(statItem),
|
children: [
|
||||||
MySpacing.height(12),
|
_buildStatCardIcon(statItem),
|
||||||
MyText.labelSmall(statItem.title, maxLines: 1),
|
MySpacing.height(8),
|
||||||
],
|
MyText.labelSmall(
|
||||||
),
|
statItem.title,
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.visible,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildStatCardIcon(_StatItem statItem) {
|
Widget _buildStatCardIcon(_StatItem statItem) {
|
||||||
return MyContainer(
|
return MyContainer.rounded(
|
||||||
paddingAll: 16,
|
paddingAll: 10,
|
||||||
color: statItem.color.withOpacity(0.2),
|
color: statItem.color.withOpacity(0.1),
|
||||||
child: MyContainer(
|
child: Icon(statItem.icon, size: 18, color: statItem.color),
|
||||||
paddingAll: 8,
|
|
||||||
color: statItem.color,
|
|
||||||
child: Icon(statItem.icon, size: 16, color: contentTheme.light),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -118,7 +114,7 @@ class _StatItem {
|
|||||||
final IconData icon;
|
final IconData icon;
|
||||||
final String title;
|
final String title;
|
||||||
final Color color;
|
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);
|
_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 {
|
class Layout extends StatelessWidget {
|
||||||
final Widget? child;
|
final Widget? child;
|
||||||
|
final Widget? floatingActionButton;
|
||||||
|
|
||||||
final LayoutController controller = LayoutController();
|
final LayoutController controller = LayoutController();
|
||||||
final topBarTheme = AdminTheme.theme.topBarTheme;
|
final topBarTheme = AdminTheme.theme.topBarTheme;
|
||||||
final contentTheme = AdminTheme.theme.contentTheme;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MyResponsive(builder: (BuildContext context, _, screenMT) {
|
return MyResponsive(builder: (BuildContext context, _, screenMT) {
|
||||||
return GetBuilder(
|
return GetBuilder(
|
||||||
init: controller,
|
init: controller,
|
||||||
builder: (controller) {
|
builder: (controller) {
|
||||||
if (screenMT.isMobile || screenMT.isTablet) {
|
if (screenMT.isMobile || screenMT.isTablet) {
|
||||||
return mobileScreen();
|
return mobileScreen();
|
||||||
} else {
|
} else {
|
||||||
return largeScreen();
|
return largeScreen();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -50,6 +51,22 @@ class Layout extends StatelessWidget {
|
|||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
actions: [
|
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),
|
MySpacing.width(8),
|
||||||
CustomPopupMenu(
|
CustomPopupMenu(
|
||||||
backdrop: true,
|
backdrop: true,
|
||||||
@ -71,9 +88,9 @@ class Layout extends StatelessWidget {
|
|||||||
backdrop: true,
|
backdrop: true,
|
||||||
onChange: (_) {},
|
onChange: (_) {},
|
||||||
offsetX: -90,
|
offsetX: -90,
|
||||||
offsetY: 4,
|
offsetY: 0,
|
||||||
menu: Padding(
|
menu: Padding(
|
||||||
padding: MySpacing.xy(8, 8),
|
padding: MySpacing.xy(0, 8),
|
||||||
child: MyContainer.rounded(
|
child: MyContainer.rounded(
|
||||||
paddingAll: 0,
|
paddingAll: 0,
|
||||||
child: Avatar(
|
child: Avatar(
|
||||||
@ -88,6 +105,7 @@ class Layout extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
drawer: LeftBar(),
|
drawer: LeftBar(),
|
||||||
|
floatingActionButton: floatingActionButton,
|
||||||
body: SingleChildScrollView(
|
body: SingleChildScrollView(
|
||||||
key: controller.scrollKey,
|
key: controller.scrollKey,
|
||||||
child: child,
|
child: child,
|
||||||
@ -99,6 +117,7 @@ class Layout extends StatelessWidget {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
key: controller.scaffoldKey,
|
key: controller.scaffoldKey,
|
||||||
endDrawer: RightBar(),
|
endDrawer: RightBar(),
|
||||||
|
floatingActionButton: floatingActionButton,
|
||||||
body: Row(
|
body: Row(
|
||||||
children: [
|
children: [
|
||||||
LeftBar(isCondensed: ThemeCustomizer.instance.leftBarCondensed),
|
LeftBar(isCondensed: ThemeCustomizer.instance.leftBarCondensed),
|
||||||
@ -111,14 +130,16 @@ class Layout extends StatelessWidget {
|
|||||||
left: 0,
|
left: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
padding: MySpacing.fromLTRB(0, 58 + flexSpacing, 0, flexSpacing),
|
padding:
|
||||||
|
MySpacing.fromLTRB(0, 58 + flexSpacing, 0, flexSpacing),
|
||||||
key: controller.scrollKey,
|
key: controller.scrollKey,
|
||||||
child: child,
|
child: child,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Positioned(top: 0, left: 0, right: 0, child: TopBar()),
|
Positioned(top: 0, left: 0, right: 0, child: TopBar()),
|
||||||
],
|
],
|
||||||
)),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -128,7 +149,11 @@ class Layout extends StatelessWidget {
|
|||||||
Widget buildNotification(String title, String description) {
|
Widget buildNotification(String title, String description) {
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
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),
|
padding: MySpacing.xy(16, 12),
|
||||||
child: MyText.titleMedium("Notification", fontWeight: 600),
|
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(
|
||||||
padding: MySpacing.xy(16, 12),
|
padding: MySpacing.xy(16, 12),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
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),
|
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(
|
||||||
padding: MySpacing.xy(16, 0),
|
padding: MySpacing.xy(16, 0),
|
||||||
child: Row(
|
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/theme/theme_customizer.dart';
|
||||||
import 'package:marco/helpers/services/url_service.dart';
|
import 'package:marco/helpers/services/url_service.dart';
|
||||||
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
|
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
|
||||||
@ -81,7 +80,7 @@ class _LeftBarState extends State<LeftBar>
|
|||||||
children: [
|
children: [
|
||||||
Center(
|
Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: MySpacing.y(12),
|
padding: MySpacing.fromLTRB(0, 24, 0, 0),
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () => Get.toNamed('/home'),
|
onTap: () => Get.toNamed('/home'),
|
||||||
child: Image.asset(
|
child: Image.asset(
|
||||||
@ -115,7 +114,7 @@ class _LeftBarState extends State<LeftBar>
|
|||||||
isCondensed: isCondensed,
|
isCondensed: isCondensed,
|
||||||
route: '/dashboard'),
|
route: '/dashboard'),
|
||||||
NavigationItem(
|
NavigationItem(
|
||||||
iconData: LucideIcons.layout_template,
|
iconData: LucideIcons.scan_face,
|
||||||
title: "Attendance",
|
title: "Attendance",
|
||||||
isCondensed: isCondensed,
|
isCondensed: isCondensed,
|
||||||
route: '/dashboard/attendance'),
|
route: '/dashboard/attendance'),
|
||||||
@ -125,15 +124,15 @@ class _LeftBarState extends State<LeftBar>
|
|||||||
isCondensed: isCondensed,
|
isCondensed: isCondensed,
|
||||||
route: '/dashboard/employees'),
|
route: '/dashboard/employees'),
|
||||||
NavigationItem(
|
NavigationItem(
|
||||||
iconData: LucideIcons.list,
|
iconData: LucideIcons.logs,
|
||||||
title: "Daily Progress Report",
|
|
||||||
isCondensed: isCondensed,
|
|
||||||
route: '/dashboard/daily-task'),
|
|
||||||
NavigationItem(
|
|
||||||
iconData: LucideIcons.list_todo,
|
|
||||||
title: "Daily Task Planing",
|
title: "Daily Task Planing",
|
||||||
isCondensed: isCondensed,
|
isCondensed: isCondensed,
|
||||||
route: '/dashboard/daily-task-planing'),
|
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() {
|
Widget userInfoSection() {
|
||||||
if (employeeInfo == null) {
|
if (employeeInfo == null) {
|
||||||
return Center(
|
return Center(child: CircularProgressIndicator());
|
||||||
child:
|
|
||||||
CircularProgressIndicator()); // Show loading indicator if employeeInfo is not yet loaded.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
@ -245,9 +242,7 @@ class _MenuWidgetState extends State<MenuWidget>
|
|||||||
}
|
}
|
||||||
|
|
||||||
void onChangeMenuActive(String key) {
|
void onChangeMenuActive(String key) {
|
||||||
if (key != widget.title) {
|
if (key != widget.title) {}
|
||||||
// onChangeExpansion(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void onChangeExpansion(value) {
|
void onChangeExpansion(value) {
|
||||||
@ -271,22 +266,16 @@ class _MenuWidgetState extends State<MenuWidget>
|
|||||||
if (hideFn != null) {
|
if (hideFn != null) {
|
||||||
hideFn!();
|
hideFn!();
|
||||||
}
|
}
|
||||||
// popupShowing = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// var route = Uri.base.fragment;
|
|
||||||
// isActive = widget.children.any((element) => element.route == route);
|
|
||||||
|
|
||||||
if (widget.isCondensed) {
|
if (widget.isCondensed) {
|
||||||
return CustomPopupMenu(
|
return CustomPopupMenu(
|
||||||
backdrop: true,
|
backdrop: true,
|
||||||
show: popupShowing,
|
show: popupShowing,
|
||||||
hideFn: (hide) => hideFn = hide,
|
hideFn: (hide) => hideFn = hide,
|
||||||
onChange: (_) {
|
onChange: (_) {},
|
||||||
// popupShowing = _;
|
|
||||||
},
|
|
||||||
placement: CustomPopupMenuPlacement.right,
|
placement: CustomPopupMenuPlacement.right,
|
||||||
menu: MouseRegion(
|
menu: MouseRegion(
|
||||||
cursor: SystemMouseCursors.click,
|
cursor: SystemMouseCursors.click,
|
||||||
@ -407,7 +396,6 @@ class _MenuWidgetState extends State<MenuWidget>
|
|||||||
void dispose() {
|
void dispose() {
|
||||||
_controller.dispose();
|
_controller.dispose();
|
||||||
super.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: ^4.0.17
|
||||||
image_picker: ^1.0.7
|
image_picker: ^1.0.7
|
||||||
logger: ^2.0.2
|
logger: ^2.0.2
|
||||||
|
flutter_image_compress: ^2.1.0
|
||||||
|
path_provider: ^2.1.2
|
||||||
|
path: ^1.9.0
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
@ -89,17 +92,17 @@ flutter:
|
|||||||
# To add assets to your application, add an assets section, like this:
|
# To add assets to your application, add an assets section, like this:
|
||||||
assets:
|
assets:
|
||||||
- assets/
|
- assets/
|
||||||
- assets/lang/
|
|
||||||
- assets/avatar/
|
- assets/avatar/
|
||||||
- assets/logo/
|
- assets/coin/
|
||||||
|
- assets/country/
|
||||||
- assets/data/
|
- assets/data/
|
||||||
- assets/dummy/
|
- assets/dummy/
|
||||||
- assets/social/
|
|
||||||
- assets/country/
|
|
||||||
- assets/coin/
|
|
||||||
- assets/dummy/ecommerce/
|
- assets/dummy/ecommerce/
|
||||||
- assets/dummy/single_product/
|
- assets/dummy/single_product/
|
||||||
|
- assets/lang/
|
||||||
|
- assets/logo/
|
||||||
- assets/logo/loading_logo.png
|
- assets/logo/loading_logo.png
|
||||||
|
- assets/social/
|
||||||
# assets:
|
# assets:
|
||||||
# - images/a_dot_burr.jpeg
|
# - images/a_dot_burr.jpeg
|
||||||
# - images/a_dot_ham.jpeg
|
# - images/a_dot_ham.jpeg
|
||||||
|