feat: Enhance project and task management features
- Added clearProjects method in ProjectController to reset project states. - Updated fetchProjects and updateSelectedProject methods for better state management. - Enhanced ReportTaskController to support image uploads with base64 encoding. - Modified ApiService to handle image data in report and comment tasks. - Integrated ProjectController in AuthService to fetch projects upon login. - Updated LocalStorage to clear selectedProjectId on logout. - Introduced ImageViewerDialog for displaying images in a dialog. - Enhanced CommentTaskBottomSheet and ReportTaskBottomSheet to support image attachments. - Improved AttendanceScreen to handle project selection and data fetching more robustly. - Refactored EmployeesScreen to manage employee data based on project selection. - Updated Layout to handle project selection and display appropriate messages. - Enhanced DailyProgressReportScreen and DailyTaskPlaningScreen to reactively fetch task data based on project changes. - Added photo_view dependency for improved image handling.
This commit is contained in:
parent
b38d987eac
commit
3a449441fa
@ -5,6 +5,7 @@ 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';
|
import 'package:marco/model/employees/employee_details_model.dart';
|
||||||
|
import 'package:marco/controller/project_controller.dart';
|
||||||
|
|
||||||
final Logger log = Logger();
|
final Logger log = Logger();
|
||||||
|
|
||||||
@ -12,8 +13,9 @@ class EmployeesScreenController extends GetxController {
|
|||||||
List<AttendanceModel> attendances = [];
|
List<AttendanceModel> attendances = [];
|
||||||
List<ProjectModel> projects = [];
|
List<ProjectModel> projects = [];
|
||||||
String? selectedProjectId;
|
String? selectedProjectId;
|
||||||
List<EmployeeModel> employees = [];
|
|
||||||
List<EmployeeDetailsModel> employeeDetails = [];
|
List<EmployeeDetailsModel> employeeDetails = [];
|
||||||
|
RxBool isAllEmployeeSelected = false.obs;
|
||||||
|
RxList<EmployeeModel> employees = <EmployeeModel>[].obs;
|
||||||
|
|
||||||
RxBool isLoading = false.obs;
|
RxBool isLoading = false.obs;
|
||||||
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
|
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
|
||||||
@ -24,7 +26,17 @@ class EmployeesScreenController extends GetxController {
|
|||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
fetchAllProjects();
|
fetchAllProjects();
|
||||||
|
|
||||||
|
final projectId = Get.find<ProjectController>().selectedProject?.id;
|
||||||
|
|
||||||
|
if (projectId != null) {
|
||||||
|
selectedProjectId = projectId;
|
||||||
|
fetchEmployeesByProject(projectId);
|
||||||
|
} else if (isAllEmployeeSelected.value) {
|
||||||
fetchAllEmployees();
|
fetchAllEmployees();
|
||||||
|
} else {
|
||||||
|
clearEmployees();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> fetchAllProjects() async {
|
Future<void> fetchAllProjects() async {
|
||||||
@ -41,18 +53,27 @@ class EmployeesScreenController extends GetxController {
|
|||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void clearEmployees() {
|
||||||
|
employees.clear(); // Correct way to clear RxList
|
||||||
|
log.i("Employees cleared");
|
||||||
|
update(['employee_screen_controller']);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> fetchAllEmployees() async {
|
Future<void> fetchAllEmployees() async {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
await _handleApiCall(
|
await _handleApiCall(
|
||||||
ApiService.getAllEmployees,
|
ApiService.getAllEmployees,
|
||||||
onSuccess: (data) {
|
onSuccess: (data) {
|
||||||
employees = data.map((json) => EmployeeModel.fromJson(json)).toList();
|
employees.assignAll(data.map((json) => EmployeeModel.fromJson(json)));
|
||||||
log.i("All Employees fetched: ${employees.length} employees loaded.");
|
log.i("All Employees fetched: ${employees.length} employees loaded.");
|
||||||
},
|
},
|
||||||
onEmpty: () => log.w("No Employee data found or API call failed."),
|
onEmpty: () {
|
||||||
|
employees.clear(); // Always clear on empty
|
||||||
|
log.w("No Employee data found or API call failed.");
|
||||||
|
},
|
||||||
);
|
);
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
update();
|
update(['employee_screen_controller']);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> fetchEmployeesByProject(String? projectId) async {
|
Future<void> fetchEmployeesByProject(String? projectId) async {
|
||||||
@ -65,22 +86,21 @@ class EmployeesScreenController extends GetxController {
|
|||||||
await _handleApiCall(
|
await _handleApiCall(
|
||||||
() => ApiService.getAllEmployeesByProject(projectId),
|
() => ApiService.getAllEmployeesByProject(projectId),
|
||||||
onSuccess: (data) {
|
onSuccess: (data) {
|
||||||
employees = data.map((json) => EmployeeModel.fromJson(json)).toList();
|
employees.assignAll(data.map((json) => EmployeeModel.fromJson(json)));
|
||||||
for (var emp in employees) {
|
for (var emp in employees) {
|
||||||
uploadingStates[emp.id] = false.obs;
|
uploadingStates[emp.id] = false.obs;
|
||||||
}
|
}
|
||||||
log.i("Employees fetched: ${employees.length} for project $projectId");
|
log.i("Employees fetched: ${employees.length} for project $projectId");
|
||||||
update();
|
|
||||||
},
|
},
|
||||||
onEmpty: () {
|
onEmpty: () {
|
||||||
|
employees.clear();
|
||||||
log.w("No employees found for project $projectId.");
|
log.w("No employees found for project $projectId.");
|
||||||
employees = [];
|
|
||||||
update();
|
|
||||||
},
|
},
|
||||||
onError: (e) =>
|
onError: (e) =>
|
||||||
log.e("Error fetching employees for project $projectId: $e"),
|
log.e("Error fetching employees for project $projectId: $e"),
|
||||||
);
|
);
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
|
update(['employee_screen_controller']);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _handleApiCall(
|
Future<void> _handleApiCall(
|
||||||
|
|||||||
@ -28,6 +28,19 @@ class ProjectController extends GetxController {
|
|||||||
fetchProjects();
|
fetchProjects();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void clearProjects() {
|
||||||
|
projects.clear();
|
||||||
|
selectedProjectId = null;
|
||||||
|
isProjectSelectionExpanded.value = false;
|
||||||
|
isProjectListExpanded.value = false;
|
||||||
|
isProjectDropdownExpanded.value = false;
|
||||||
|
isLoadingProjects.value = false;
|
||||||
|
isLoading.value = false;
|
||||||
|
uploadingStates.clear();
|
||||||
|
LocalStorage.saveString('selectedProjectId', '');
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
/// Fetches projects and initializes selected project.
|
/// Fetches projects and initializes selected project.
|
||||||
Future<void> fetchProjects() async {
|
Future<void> fetchProjects() async {
|
||||||
isLoadingProjects.value = true;
|
isLoadingProjects.value = true;
|
||||||
@ -62,6 +75,8 @@ class ProjectController extends GetxController {
|
|||||||
Future<void> updateSelectedProject(String projectId) async {
|
Future<void> updateSelectedProject(String projectId) async {
|
||||||
selectedProjectId?.value = projectId;
|
selectedProjectId?.value = projectId;
|
||||||
await LocalStorage.saveString('selectedProjectId', projectId);
|
await LocalStorage.saveString('selectedProjectId', projectId);
|
||||||
update();
|
update([
|
||||||
|
'selected_project'
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import 'package:marco/helpers/widgets/my_snackbar.dart';
|
|||||||
import 'package:marco/controller/task_planing/daily_task_planing_controller.dart';
|
import 'package:marco/controller/task_planing/daily_task_planing_controller.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
final Logger logger = Logger();
|
final Logger logger = Logger();
|
||||||
|
|
||||||
@ -96,6 +97,7 @@ class ReportTaskController extends MyController {
|
|||||||
required int completedTask,
|
required int completedTask,
|
||||||
required List<Map<String, dynamic>> checklist,
|
required List<Map<String, dynamic>> checklist,
|
||||||
required DateTime reportedDate,
|
required DateTime reportedDate,
|
||||||
|
List<File>? images,
|
||||||
}) async {
|
}) async {
|
||||||
logger.i("Starting task report...");
|
logger.i("Starting task report...");
|
||||||
|
|
||||||
@ -132,12 +134,24 @@ class ReportTaskController extends MyController {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
|
List<Map<String, dynamic>>? imageData;
|
||||||
|
if (images != null && images.isNotEmpty) {
|
||||||
|
imageData = await Future.wait(images.map((file) async {
|
||||||
|
final bytes = await file.readAsBytes();
|
||||||
|
final base64Image = base64Encode(bytes);
|
||||||
|
return {
|
||||||
|
"fileName": file.path.split('/').last,
|
||||||
|
"fileData": base64Image,
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
final success = await ApiService.reportTask(
|
final success = await ApiService.reportTask(
|
||||||
id: projectId,
|
id: projectId,
|
||||||
comment: commentField,
|
comment: commentField,
|
||||||
completedTask: completedTask,
|
completedTask: completedWorkInt,
|
||||||
checkList: checklist,
|
checkList: checklist,
|
||||||
|
images: imageData,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
@ -169,6 +183,7 @@ class ReportTaskController extends MyController {
|
|||||||
Future<void> commentTask({
|
Future<void> commentTask({
|
||||||
required String projectId,
|
required String projectId,
|
||||||
required String comment,
|
required String comment,
|
||||||
|
List<File>? images,
|
||||||
}) async {
|
}) async {
|
||||||
logger.i("Starting task comment...");
|
logger.i("Starting task comment...");
|
||||||
|
|
||||||
@ -184,10 +199,22 @@ class ReportTaskController extends MyController {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
|
List<Map<String, dynamic>>? imageData;
|
||||||
|
if (images != null && images.isNotEmpty) {
|
||||||
|
imageData = await Future.wait(images.map((file) async {
|
||||||
|
final bytes = await file.readAsBytes();
|
||||||
|
final base64Image = base64Encode(bytes);
|
||||||
|
return {
|
||||||
|
"fileName": file.path.split('/').last,
|
||||||
|
"fileData": base64Image,
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
final success = await ApiService.commentTask(
|
final success = await ApiService.commentTask(
|
||||||
id: projectId,
|
id: projectId,
|
||||||
comment: commentField,
|
comment: commentField,
|
||||||
|
images: imageData,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
|
|||||||
@ -343,6 +343,7 @@ class ApiService {
|
|||||||
required int completedTask,
|
required int completedTask,
|
||||||
required String comment,
|
required String comment,
|
||||||
required List<Map<String, dynamic>> checkList,
|
required List<Map<String, dynamic>> checkList,
|
||||||
|
List<Map<String, dynamic>>? images,
|
||||||
}) async {
|
}) async {
|
||||||
final body = {
|
final body = {
|
||||||
"id": id,
|
"id": id,
|
||||||
@ -350,6 +351,7 @@ class ApiService {
|
|||||||
"comment": comment,
|
"comment": comment,
|
||||||
"reportedDate": DateTime.now().toUtc().toIso8601String(),
|
"reportedDate": DateTime.now().toUtc().toIso8601String(),
|
||||||
"checkList": checkList,
|
"checkList": checkList,
|
||||||
|
if (images != null && images.isNotEmpty) "images": images,
|
||||||
};
|
};
|
||||||
|
|
||||||
final response = await _postRequest(ApiEndpoints.reportTask, body);
|
final response = await _postRequest(ApiEndpoints.reportTask, body);
|
||||||
@ -373,11 +375,13 @@ class ApiService {
|
|||||||
static Future<bool> commentTask({
|
static Future<bool> commentTask({
|
||||||
required String id,
|
required String id,
|
||||||
required String comment,
|
required String comment,
|
||||||
|
List<Map<String, dynamic>>? images,
|
||||||
}) async {
|
}) async {
|
||||||
final body = {
|
final body = {
|
||||||
"taskAllocationId": id,
|
"taskAllocationId": id,
|
||||||
"comment": comment,
|
"comment": comment,
|
||||||
"commentDate": DateTime.now().toUtc().toIso8601String(),
|
"commentDate": DateTime.now().toUtc().toIso8601String(),
|
||||||
|
if (images != null && images.isNotEmpty) "images": images,
|
||||||
};
|
};
|
||||||
|
|
||||||
final response = await _postRequest(ApiEndpoints.commentTask, body);
|
final response = await _postRequest(ApiEndpoints.commentTask, body);
|
||||||
|
|||||||
@ -5,6 +5,7 @@ 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';
|
import 'package:logger/logger.dart';
|
||||||
import 'package:marco/helpers/services/api_endpoints.dart';
|
import 'package:marco/helpers/services/api_endpoints.dart';
|
||||||
|
import 'package:marco/controller/project_controller.dart';
|
||||||
|
|
||||||
final Logger logger = Logger();
|
final Logger logger = Logger();
|
||||||
|
|
||||||
@ -370,6 +371,7 @@ class AuthService {
|
|||||||
// ✅ Put and load PermissionController
|
// ✅ Put and load PermissionController
|
||||||
final permissionController = Get.put(PermissionController());
|
final permissionController = Get.put(PermissionController());
|
||||||
await permissionController.loadData(jwtToken);
|
await permissionController.loadData(jwtToken);
|
||||||
|
await Get.find<ProjectController>().fetchProjects();
|
||||||
|
|
||||||
isLoggedIn = true;
|
isLoggedIn = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,9 @@ import 'package:shared_preferences/shared_preferences.dart';
|
|||||||
import 'package:marco/model/user_permission.dart';
|
import 'package:marco/model/user_permission.dart';
|
||||||
import 'package:marco/model/employee_info.dart';
|
import 'package:marco/model/employee_info.dart';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:get/route_manager.dart';
|
import 'package:marco/controller/project_controller.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
|
|
||||||
class LocalStorage {
|
class LocalStorage {
|
||||||
static const String _loggedInUserKey = "user";
|
static const String _loggedInUserKey = "user";
|
||||||
@ -145,6 +147,11 @@ class LocalStorage {
|
|||||||
await preferences.remove("mpin_verified");
|
await preferences.remove("mpin_verified");
|
||||||
await preferences.remove(_languageKey);
|
await preferences.remove(_languageKey);
|
||||||
await preferences.remove(_themeCustomizerKey);
|
await preferences.remove(_themeCustomizerKey);
|
||||||
|
await preferences.remove('selectedProjectId');
|
||||||
|
if (Get.isRegistered<ProjectController>()) {
|
||||||
|
Get.find<ProjectController>().clearProjects();
|
||||||
|
}
|
||||||
|
|
||||||
Get.offAllNamed('/auth/login-option');
|
Get.offAllNamed('/auth/login-option');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
122
lib/helpers/widgets/image_viewer_dialog.dart
Normal file
122
lib/helpers/widgets/image_viewer_dialog.dart
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class ImageViewerDialog extends StatefulWidget {
|
||||||
|
final List<dynamic> imageSources;
|
||||||
|
final int initialIndex;
|
||||||
|
|
||||||
|
const ImageViewerDialog({
|
||||||
|
Key? key,
|
||||||
|
required this.imageSources,
|
||||||
|
required this.initialIndex,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ImageViewerDialog> createState() => _ImageViewerDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ImageViewerDialogState extends State<ImageViewerDialog> {
|
||||||
|
late final PageController _controller;
|
||||||
|
late int currentIndex;
|
||||||
|
|
||||||
|
bool isFile(dynamic item) => item is File;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
currentIndex = widget.initialIndex;
|
||||||
|
_controller = PageController(initialPage: widget.initialIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final double dialogHeight = MediaQuery.of(context).size.height * 0.55;
|
||||||
|
|
||||||
|
return Dialog(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
insetPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 100),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
child: Container(
|
||||||
|
height: dialogHeight,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.1),
|
||||||
|
blurRadius: 12,
|
||||||
|
offset: const Offset(0, 4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// Top Close Button
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.topRight,
|
||||||
|
child: IconButton(
|
||||||
|
icon: const Icon(Icons.close, size: 26),
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
splashRadius: 22,
|
||||||
|
tooltip: 'Close',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Image Viewer
|
||||||
|
Expanded(
|
||||||
|
child: PageView.builder(
|
||||||
|
controller: _controller,
|
||||||
|
itemCount: widget.imageSources.length,
|
||||||
|
onPageChanged: (index) {
|
||||||
|
setState(() => currentIndex = index);
|
||||||
|
},
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final item = widget.imageSources[index];
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
|
child: isFile(item)
|
||||||
|
? Image.file(item, fit: BoxFit.contain)
|
||||||
|
: Image.network(
|
||||||
|
item,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
loadingBuilder: (context, child, loadingProgress) {
|
||||||
|
if (loadingProgress == null) return child;
|
||||||
|
return Center(
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
value: loadingProgress.expectedTotalBytes != null
|
||||||
|
? loadingProgress.cumulativeBytesLoaded /
|
||||||
|
(loadingProgress.expectedTotalBytes ?? 1)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
errorBuilder: (context, error, stackTrace) =>
|
||||||
|
const Center(
|
||||||
|
child: Icon(Icons.broken_image,
|
||||||
|
size: 48, color: Colors.grey),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Index Indicator
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8, bottom: 12),
|
||||||
|
child: Text(
|
||||||
|
'${currentIndex + 1} / ${widget.imageSources.length}',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.black87,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -10,6 +10,7 @@ import 'package:marco/helpers/widgets/my_text_style.dart';
|
|||||||
import 'package:marco/helpers/widgets/avatar.dart';
|
import 'package:marco/helpers/widgets/avatar.dart';
|
||||||
import 'package:marco/helpers/widgets/my_team_model_sheet.dart';
|
import 'package:marco/helpers/widgets/my_team_model_sheet.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:marco/helpers/widgets/image_viewer_dialog.dart';
|
||||||
|
|
||||||
class CommentTaskBottomSheet extends StatefulWidget {
|
class CommentTaskBottomSheet extends StatefulWidget {
|
||||||
final Map<String, dynamic> taskData;
|
final Map<String, dynamic> taskData;
|
||||||
@ -206,6 +207,148 @@ class _CommentTaskBottomSheetState extends State<CommentTaskBottomSheet>
|
|||||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
MySpacing.height(16),
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.camera_alt_outlined,
|
||||||
|
size: 18, color: Colors.grey[700]),
|
||||||
|
MySpacing.width(8),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
MyText.titleSmall("Attach Photos:",
|
||||||
|
fontWeight: 600),
|
||||||
|
MySpacing.height(12),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Obx(() {
|
||||||
|
final images = controller.selectedImages;
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (images.isEmpty)
|
||||||
|
Container(
|
||||||
|
height: 70,
|
||||||
|
width: double.infinity,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(
|
||||||
|
color: Colors.grey.shade300, width: 2),
|
||||||
|
color: Colors.grey.shade100,
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Icon(Icons.photo_camera_outlined,
|
||||||
|
size: 48, color: Colors.grey.shade400),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
SizedBox(
|
||||||
|
height: 70,
|
||||||
|
child: ListView.separated(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
itemCount: images.length,
|
||||||
|
separatorBuilder: (_, __) =>
|
||||||
|
MySpacing.width(12),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final file = images[index];
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (_) =>
|
||||||
|
ImageViewerDialog(
|
||||||
|
imageSources: images,
|
||||||
|
initialIndex: index,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius:
|
||||||
|
BorderRadius.circular(12),
|
||||||
|
child: Image.file(
|
||||||
|
file,
|
||||||
|
height: 70,
|
||||||
|
width: 70,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
top: 4,
|
||||||
|
right: 4,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () => controller
|
||||||
|
.removeImageAt(index),
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black54,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(Icons.close,
|
||||||
|
size: 20,
|
||||||
|
color: Colors.white),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
MySpacing.height(16),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: MyButton.outlined(
|
||||||
|
onPressed: () => controller.pickImages(
|
||||||
|
fromCamera: true),
|
||||||
|
padding: MySpacing.xy(12, 10),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.camera_alt,
|
||||||
|
size: 16,
|
||||||
|
color: Colors.blueAccent),
|
||||||
|
MySpacing.width(6),
|
||||||
|
MyText.bodySmall('Capture',
|
||||||
|
color: Colors.blueAccent),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
MySpacing.width(12),
|
||||||
|
Expanded(
|
||||||
|
child: MyButton.outlined(
|
||||||
|
onPressed: () => controller.pickImages(
|
||||||
|
fromCamera: false),
|
||||||
|
padding: MySpacing.xy(12, 10),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.upload_file,
|
||||||
|
size: 16,
|
||||||
|
color: Colors.blueAccent),
|
||||||
|
MySpacing.width(6),
|
||||||
|
MyText.bodySmall('Upload',
|
||||||
|
color: Colors.blueAccent),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
MySpacing.height(24),
|
MySpacing.height(24),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
@ -233,7 +376,9 @@ class _CommentTaskBottomSheetState extends State<CommentTaskBottomSheet>
|
|||||||
.getController('comment')
|
.getController('comment')
|
||||||
?.text ??
|
?.text ??
|
||||||
'',
|
'',
|
||||||
|
images: controller.selectedImages,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (widget.onCommentSuccess != null) {
|
if (widget.onCommentSuccess != null) {
|
||||||
widget.onCommentSuccess!();
|
widget.onCommentSuccess!();
|
||||||
}
|
}
|
||||||
@ -262,12 +407,13 @@ class _CommentTaskBottomSheetState extends State<CommentTaskBottomSheet>
|
|||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
MySpacing.height(24),
|
MySpacing.height(10),
|
||||||
if ((widget.taskData['taskComments'] as List<dynamic>?)
|
if ((widget.taskData['taskComments'] as List<dynamic>?)
|
||||||
?.isNotEmpty ==
|
?.isNotEmpty ==
|
||||||
true) ...[
|
true) ...[
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
|
MySpacing.width(10),
|
||||||
Icon(Icons.chat_bubble_outline,
|
Icon(Icons.chat_bubble_outline,
|
||||||
size: 18, color: Colors.grey[700]),
|
size: 18, color: Colors.grey[700]),
|
||||||
MySpacing.width(8),
|
MySpacing.width(8),
|
||||||
@ -277,6 +423,7 @@ class _CommentTaskBottomSheetState extends State<CommentTaskBottomSheet>
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
Divider(),
|
||||||
MySpacing.height(12),
|
MySpacing.height(12),
|
||||||
Builder(
|
Builder(
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
@ -301,81 +448,233 @@ class _CommentTaskBottomSheetState extends State<CommentTaskBottomSheet>
|
|||||||
itemCount: comments.length,
|
itemCount: comments.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final comment = comments[index];
|
final comment = comments[index];
|
||||||
final commentText = comment['text'] ?? '-';
|
final commentText =
|
||||||
|
comment['text'] ?? '-';
|
||||||
final commentedBy =
|
final commentedBy =
|
||||||
comment['commentedBy'] ?? 'Unknown';
|
comment['commentedBy'] ?? 'Unknown';
|
||||||
final relativeTime =
|
final relativeTime =
|
||||||
timeAgo(comment['date'] ?? '');
|
timeAgo(comment['date'] ?? '');
|
||||||
|
|
||||||
|
// Dummy image URLs (simulate as if coming from backend)
|
||||||
|
final imageUrls = [
|
||||||
|
'https://picsum.photos/seed/${index}a/100',
|
||||||
|
'https://picsum.photos/seed/${index}b/100',
|
||||||
|
'https://picsum.photos/seed/${index}a/100',
|
||||||
|
'https://picsum.photos/seed/${index}b/100',
|
||||||
|
'https://picsum.photos/seed/${index}a/100',
|
||||||
|
'https://picsum.photos/seed/${index}b/100',
|
||||||
|
];
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
margin: EdgeInsets.symmetric(
|
margin: EdgeInsets.symmetric(
|
||||||
vertical: 6, horizontal: 8),
|
vertical: 0, horizontal: 0),
|
||||||
padding: EdgeInsets.all(12),
|
padding: EdgeInsets.all(12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.grey.shade200,
|
color: Colors.grey.shade200,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius:
|
||||||
|
BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment:
|
crossAxisAlignment:
|
||||||
CrossAxisAlignment.start,
|
CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// Avatar for commenter
|
|
||||||
Avatar(
|
|
||||||
firstName:
|
|
||||||
commentedBy.split(' ').first,
|
|
||||||
lastName: commentedBy
|
|
||||||
.split(' ')
|
|
||||||
.length >
|
|
||||||
1
|
|
||||||
? commentedBy.split(' ').last
|
|
||||||
: '',
|
|
||||||
size: 32,
|
|
||||||
),
|
|
||||||
SizedBox(width: 12),
|
SizedBox(width: 12),
|
||||||
// Comment text and meta
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment:
|
crossAxisAlignment:
|
||||||
CrossAxisAlignment.start,
|
CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
// 🔹 Top Row: Avatar + Name + Time
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment:
|
||||||
|
CrossAxisAlignment
|
||||||
|
.center,
|
||||||
|
children: [
|
||||||
|
Avatar(
|
||||||
|
firstName: commentedBy
|
||||||
|
.split(' ')
|
||||||
|
.first,
|
||||||
|
lastName: commentedBy
|
||||||
|
.split(' ')
|
||||||
|
.length >
|
||||||
|
1
|
||||||
|
? commentedBy
|
||||||
|
.split(' ')
|
||||||
|
.last
|
||||||
|
: '',
|
||||||
|
size: 32,
|
||||||
|
),
|
||||||
|
SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment
|
||||||
|
.spaceBetween,
|
||||||
|
children: [
|
||||||
|
MyText.bodyMedium(
|
||||||
|
commentedBy,
|
||||||
|
fontWeight: 700,
|
||||||
|
color: Colors
|
||||||
|
.black87,
|
||||||
|
),
|
||||||
|
MyText.bodySmall(
|
||||||
|
relativeTime,
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors
|
||||||
|
.black54,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 12),
|
||||||
|
// 🔹 Comment text below attachments
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment:
|
mainAxisAlignment:
|
||||||
MainAxisAlignment
|
MainAxisAlignment
|
||||||
.spaceBetween,
|
.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
MyText.bodyMedium(
|
||||||
commentedBy,
|
commentText,
|
||||||
style: TextStyle(
|
fontWeight: 500,
|
||||||
fontWeight:
|
|
||||||
FontWeight.bold,
|
|
||||||
color: Colors.black87,
|
color: Colors.black87,
|
||||||
),
|
),
|
||||||
|
]),
|
||||||
|
SizedBox(height: 12),
|
||||||
|
|
||||||
|
// 🔹 Attachments row: full width below top row
|
||||||
|
if (imageUrls.isNotEmpty) ...[
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment:
|
||||||
|
CrossAxisAlignment
|
||||||
|
.start,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons
|
||||||
|
.attach_file_outlined,
|
||||||
|
size: 18,
|
||||||
|
color: Colors
|
||||||
|
.grey[700]),
|
||||||
|
MyText.bodyMedium(
|
||||||
|
'Attachments',
|
||||||
|
fontWeight: 600,
|
||||||
|
color:
|
||||||
|
Colors.black87,
|
||||||
),
|
),
|
||||||
Text(
|
]),
|
||||||
relativeTime,
|
SizedBox(height: 8),
|
||||||
style: TextStyle(
|
SizedBox(
|
||||||
fontSize: 12,
|
height: 60,
|
||||||
color: Colors.black54,
|
child: ListView.separated(
|
||||||
|
padding: EdgeInsets
|
||||||
|
.symmetric(
|
||||||
|
horizontal: 0),
|
||||||
|
scrollDirection:
|
||||||
|
Axis.horizontal,
|
||||||
|
itemCount:
|
||||||
|
imageUrls.length,
|
||||||
|
itemBuilder: (context,
|
||||||
|
imageIndex) {
|
||||||
|
final imageUrl =
|
||||||
|
imageUrls[
|
||||||
|
imageIndex];
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
showDialog(
|
||||||
|
context:
|
||||||
|
context,
|
||||||
|
barrierColor:
|
||||||
|
Colors
|
||||||
|
.black54,
|
||||||
|
builder: (_) =>
|
||||||
|
ImageViewerDialog(
|
||||||
|
imageSources:
|
||||||
|
imageUrls,
|
||||||
|
initialIndex:
|
||||||
|
imageIndex,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 60,
|
||||||
|
height: 60,
|
||||||
|
decoration:
|
||||||
|
BoxDecoration(
|
||||||
|
borderRadius:
|
||||||
|
BorderRadius.circular(
|
||||||
|
12),
|
||||||
|
color: Colors
|
||||||
|
.grey[
|
||||||
|
100],
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors
|
||||||
|
.black26,
|
||||||
|
blurRadius:
|
||||||
|
6,
|
||||||
|
offset: Offset(
|
||||||
|
2,
|
||||||
|
2),
|
||||||
),
|
),
|
||||||
)
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
SizedBox(height: 6),
|
child:
|
||||||
Text(
|
ClipRRect(
|
||||||
commentText,
|
borderRadius:
|
||||||
style: TextStyle(
|
BorderRadius.circular(
|
||||||
fontWeight: FontWeight.w500,
|
12),
|
||||||
color: Colors.black87,
|
child: Image
|
||||||
|
.network(
|
||||||
|
imageUrl,
|
||||||
|
fit: BoxFit
|
||||||
|
.cover,
|
||||||
|
errorBuilder: (context,
|
||||||
|
error,
|
||||||
|
stackTrace) =>
|
||||||
|
Container(
|
||||||
|
color: Colors
|
||||||
|
.grey[300],
|
||||||
|
child: Icon(
|
||||||
|
Icons
|
||||||
|
.broken_image,
|
||||||
|
color:
|
||||||
|
Colors.grey[700]),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
right: 4,
|
||||||
|
bottom: 4,
|
||||||
|
child: Icon(
|
||||||
|
Icons
|
||||||
|
.zoom_in,
|
||||||
|
color: Colors
|
||||||
|
.white70,
|
||||||
|
size: 16),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
separatorBuilder: (_,
|
||||||
|
__) =>
|
||||||
|
SizedBox(width: 12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 12),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
}),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@ -11,7 +11,11 @@ import 'package:marco/helpers/widgets/my_text_style.dart';
|
|||||||
class ReportTaskBottomSheet extends StatefulWidget {
|
class ReportTaskBottomSheet extends StatefulWidget {
|
||||||
final Map<String, dynamic> taskData;
|
final Map<String, dynamic> taskData;
|
||||||
final VoidCallback? onReportSuccess;
|
final VoidCallback? onReportSuccess;
|
||||||
const ReportTaskBottomSheet({super.key, required this.taskData,this.onReportSuccess,});
|
const ReportTaskBottomSheet({
|
||||||
|
super.key,
|
||||||
|
required this.taskData,
|
||||||
|
this.onReportSuccess,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ReportTaskBottomSheet> createState() => _ReportTaskBottomSheetState();
|
State<ReportTaskBottomSheet> createState() => _ReportTaskBottomSheetState();
|
||||||
@ -201,6 +205,147 @@ class _ReportTaskBottomSheetState extends State<ReportTaskBottomSheet>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
MySpacing.height(24),
|
MySpacing.height(24),
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.camera_alt_outlined,
|
||||||
|
size: 18, color: Colors.grey[700]),
|
||||||
|
MySpacing.width(8),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
MyText.titleSmall("Attach Photos:",
|
||||||
|
fontWeight: 600),
|
||||||
|
MySpacing.height(12),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Obx(() {
|
||||||
|
final images = controller.selectedImages;
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (images.isEmpty)
|
||||||
|
Container(
|
||||||
|
height: 70,
|
||||||
|
width: double.infinity,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(
|
||||||
|
color: Colors.grey.shade300, width: 2),
|
||||||
|
color: Colors.grey.shade100,
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Icon(Icons.photo_camera_outlined,
|
||||||
|
size: 48, color: Colors.grey.shade400),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
SizedBox(
|
||||||
|
height: 70,
|
||||||
|
child: ListView.separated(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
itemCount: images.length,
|
||||||
|
separatorBuilder: (_, __) =>
|
||||||
|
MySpacing.width(12),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final file = images[index];
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => Dialog(
|
||||||
|
child: InteractiveViewer(
|
||||||
|
child: Image.file(file),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius:
|
||||||
|
BorderRadius.circular(12),
|
||||||
|
child: Image.file(
|
||||||
|
file,
|
||||||
|
height: 70,
|
||||||
|
width: 70,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
top: 4,
|
||||||
|
right: 4,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () => controller
|
||||||
|
.removeImageAt(index),
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black54,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(Icons.close,
|
||||||
|
size: 20,
|
||||||
|
color: Colors.white),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
MySpacing.height(16),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: MyButton.outlined(
|
||||||
|
onPressed: () => controller.pickImages(
|
||||||
|
fromCamera: true),
|
||||||
|
padding: MySpacing.xy(12, 10),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.camera_alt,
|
||||||
|
size: 16,
|
||||||
|
color: Colors.blueAccent),
|
||||||
|
MySpacing.width(6),
|
||||||
|
MyText.bodySmall('Capture',
|
||||||
|
color: Colors.blueAccent),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
MySpacing.width(12),
|
||||||
|
Expanded(
|
||||||
|
child: MyButton.outlined(
|
||||||
|
onPressed: () => controller.pickImages(
|
||||||
|
fromCamera: false),
|
||||||
|
padding: MySpacing.xy(12, 10),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.upload_file,
|
||||||
|
size: 16,
|
||||||
|
color: Colors.blueAccent),
|
||||||
|
MySpacing.width(6),
|
||||||
|
MyText.bodySmall('Upload',
|
||||||
|
color: Colors.blueAccent),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
@ -237,6 +382,7 @@ class _ReportTaskBottomSheetState extends State<ReportTaskBottomSheet>
|
|||||||
0,
|
0,
|
||||||
checklist: [],
|
checklist: [],
|
||||||
reportedDate: DateTime.now(),
|
reportedDate: DateTime.now(),
|
||||||
|
images: controller.selectedImages,
|
||||||
);
|
);
|
||||||
if (widget.onReportSuccess != null) {
|
if (widget.onReportSuccess != null) {
|
||||||
widget.onReportSuccess!();
|
widget.onReportSuccess!();
|
||||||
|
|||||||
@ -236,8 +236,17 @@ class _MPINAuthScreenState extends State<MPINAuthScreen> with UIMixin {
|
|||||||
),
|
),
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||||
onChanged: (value) =>
|
onChanged: (value) {
|
||||||
controller.onDigitChanged(value, index, isRetype: isRetype),
|
controller.onDigitChanged(value, index, isRetype: isRetype);
|
||||||
|
|
||||||
|
if (!isRetype) {
|
||||||
|
final isComplete =
|
||||||
|
controller.digitControllers.every((c) => c.text.isNotEmpty);
|
||||||
|
if (isComplete && !controller.isLoading.value) {
|
||||||
|
controller.onSubmitMPIN();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
counterText: '',
|
counterText: '',
|
||||||
filled: true,
|
filled: true,
|
||||||
|
|||||||
@ -36,8 +36,14 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
final projectController = Get.find<ProjectController>();
|
final projectController = Get.find<ProjectController>();
|
||||||
ever<String?>(projectController.selectedProjectId!, (projectId) async {
|
final attendanceController = Get.find<AttendanceController>();
|
||||||
|
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
ever<String?>(
|
||||||
|
projectController.selectedProjectId!,
|
||||||
|
(projectId) async {
|
||||||
if (projectId != null && projectId.isNotEmpty) {
|
if (projectId != null && projectId.isNotEmpty) {
|
||||||
try {
|
try {
|
||||||
await attendanceController.fetchEmployeesByProject(projectId);
|
await attendanceController.fetchEmployeesByProject(projectId);
|
||||||
@ -49,6 +55,8 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
|||||||
debugPrint("Error updating data on project change: $e");
|
debugPrint("Error updating data on project change: $e");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,6 +115,12 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
|||||||
init: attendanceController,
|
init: attendanceController,
|
||||||
tag: 'attendance_dashboard_controller',
|
tag: 'attendance_dashboard_controller',
|
||||||
builder: (controller) {
|
builder: (controller) {
|
||||||
|
final selectedProjectId =
|
||||||
|
Get.find<ProjectController>().selectedProjectId?.value;
|
||||||
|
|
||||||
|
final bool noProjectSelected =
|
||||||
|
selectedProjectId == null || selectedProjectId.isEmpty;
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@ -234,7 +248,18 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
|||||||
MyFlex(children: [
|
MyFlex(children: [
|
||||||
MyFlexItem(
|
MyFlexItem(
|
||||||
sizes: 'lg-12 md-12 sm-12',
|
sizes: 'lg-12 md-12 sm-12',
|
||||||
child: selectedTab == 'todaysAttendance'
|
child: noProjectSelected
|
||||||
|
? Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(24.0),
|
||||||
|
child: MyText.titleMedium(
|
||||||
|
'No Records Found',
|
||||||
|
fontWeight: 600,
|
||||||
|
color: Colors.grey[600],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: selectedTab == 'todaysAttendance'
|
||||||
? employeeListTab()
|
? employeeListTab()
|
||||||
: selectedTab == 'attendanceLogs'
|
: selectedTab == 'attendanceLogs'
|
||||||
? employeeLog()
|
? employeeLog()
|
||||||
|
|||||||
@ -9,12 +9,12 @@ import 'package:marco/helpers/widgets/my_card.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/controller/permission_controller.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/model/employees/add_employee_bottom_sheet.dart';
|
||||||
import 'package:marco/controller/dashboard/employees_screen_controller.dart';
|
import 'package:marco/controller/dashboard/employees_screen_controller.dart';
|
||||||
import 'package:marco/helpers/widgets/avatar.dart';
|
import 'package:marco/helpers/widgets/avatar.dart';
|
||||||
import 'package:marco/model/employees/employee_detail_bottom_sheet.dart';
|
import 'package:marco/model/employees/employee_detail_bottom_sheet.dart';
|
||||||
import 'package:marco/controller/project_controller.dart';
|
import 'package:marco/controller/project_controller.dart';
|
||||||
|
|
||||||
class EmployeesScreen extends StatefulWidget {
|
class EmployeesScreen extends StatefulWidget {
|
||||||
const EmployeesScreen({super.key});
|
const EmployeesScreen({super.key});
|
||||||
|
|
||||||
@ -27,52 +27,45 @@ class _EmployeesScreenState extends State<EmployeesScreen> with UIMixin {
|
|||||||
Get.put(EmployeesScreenController());
|
Get.put(EmployeesScreenController());
|
||||||
final PermissionController permissionController =
|
final PermissionController permissionController =
|
||||||
Get.put(PermissionController());
|
Get.put(PermissionController());
|
||||||
|
Future<void> _refreshEmployees() async {
|
||||||
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 {
|
try {
|
||||||
if (selectedProjectId == null) {
|
final selectedProjectId =
|
||||||
|
Get.find<ProjectController>().selectedProject?.id;
|
||||||
|
final isAllSelected =
|
||||||
|
employeeScreenController.isAllEmployeeSelected.value;
|
||||||
|
|
||||||
|
if (isAllSelected) {
|
||||||
|
employeeScreenController.selectedProjectId = null;
|
||||||
await employeeScreenController.fetchAllEmployees();
|
await employeeScreenController.fetchAllEmployees();
|
||||||
} else {
|
} else if (selectedProjectId != null) {
|
||||||
|
employeeScreenController.selectedProjectId = selectedProjectId;
|
||||||
await employeeScreenController
|
await employeeScreenController
|
||||||
.fetchEmployeesByProject(selectedProjectId);
|
.fetchEmployeesByProject(selectedProjectId);
|
||||||
}
|
} else {
|
||||||
} catch (e) {
|
// ❗ Clear employees if neither selected
|
||||||
debugPrint('Error fetching employees: ${e.toString()}');
|
employeeScreenController.clearEmployees();
|
||||||
}
|
}
|
||||||
|
|
||||||
employeeScreenController.update(['employee_screen_controller']);
|
employeeScreenController.update(['employee_screen_controller']);
|
||||||
}
|
} catch (e, stackTrace) {
|
||||||
|
debugPrint('Error refreshing employee data: ${e.toString()}');
|
||||||
|
debugPrintStack(stackTrace: stackTrace);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _refreshEmployees() async {
|
@override
|
||||||
try {
|
void initState() {
|
||||||
final projectId = employeeScreenController.selectedProjectId;
|
super.initState();
|
||||||
if (projectId == null) {
|
final selectedProjectId = Get.find<ProjectController>().selectedProject?.id;
|
||||||
await employeeScreenController.fetchAllEmployees();
|
|
||||||
|
if (selectedProjectId != null) {
|
||||||
|
employeeScreenController.selectedProjectId = selectedProjectId;
|
||||||
|
employeeScreenController.fetchEmployeesByProject(selectedProjectId);
|
||||||
|
} else if (employeeScreenController.isAllEmployeeSelected.value) {
|
||||||
|
employeeScreenController.selectedProjectId = null;
|
||||||
|
employeeScreenController.fetchAllEmployees();
|
||||||
} else {
|
} else {
|
||||||
await employeeScreenController.fetchEmployeesByProject(projectId);
|
employeeScreenController.clearEmployees();
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint('Error refreshing employee data: ${e.toString()}');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -194,26 +187,66 @@ class _EmployeesScreenState extends State<EmployeesScreen> with UIMixin {
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
MyText.bodyMedium("Filter", fontWeight: 600),
|
Obx(() {
|
||||||
Tooltip(
|
return Row(
|
||||||
message: 'Project',
|
children: [
|
||||||
child: InkWell(
|
Checkbox(
|
||||||
borderRadius: BorderRadius.circular(24),
|
value: employeeScreenController
|
||||||
onTap: _openFilterSheet,
|
.isAllEmployeeSelected.value,
|
||||||
child: MouseRegion(
|
activeColor: Colors.blueAccent,
|
||||||
cursor: SystemMouseCursors.click,
|
fillColor:
|
||||||
child: const Padding(
|
MaterialStateProperty.resolveWith<Color?>(
|
||||||
padding: EdgeInsets.all(8),
|
(states) {
|
||||||
child: Icon(
|
if (states.contains(MaterialState.selected)) {
|
||||||
Icons.filter_list_alt,
|
return Colors.blueAccent;
|
||||||
color: Colors.blueAccent,
|
}
|
||||||
size: 28,
|
return Colors.transparent;
|
||||||
|
}),
|
||||||
|
checkColor: Colors.white,
|
||||||
|
side: BorderSide(
|
||||||
|
color: Colors.black,
|
||||||
|
width: 2,
|
||||||
),
|
),
|
||||||
|
onChanged: (value) async {
|
||||||
|
employeeScreenController
|
||||||
|
.isAllEmployeeSelected.value = value!;
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
employeeScreenController.selectedProjectId =
|
||||||
|
null;
|
||||||
|
await employeeScreenController
|
||||||
|
.fetchAllEmployees();
|
||||||
|
} else {
|
||||||
|
final selectedProjectId =
|
||||||
|
Get.find<ProjectController>()
|
||||||
|
.selectedProject
|
||||||
|
?.id;
|
||||||
|
|
||||||
|
if (selectedProjectId != null) {
|
||||||
|
employeeScreenController
|
||||||
|
.selectedProjectId =
|
||||||
|
selectedProjectId;
|
||||||
|
await employeeScreenController
|
||||||
|
.fetchEmployeesByProject(
|
||||||
|
selectedProjectId);
|
||||||
|
} else {
|
||||||
|
// ✅ THIS is your critical path
|
||||||
|
employeeScreenController.clearEmployees();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
employeeScreenController
|
||||||
|
.update(['employee_screen_controller']);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
|
MyText.bodyMedium(
|
||||||
|
"All Employees",
|
||||||
|
fontWeight: 600,
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
),
|
);
|
||||||
const SizedBox(width: 8),
|
}),
|
||||||
|
const SizedBox(width: 16),
|
||||||
MyText.bodyMedium("Refresh", fontWeight: 600),
|
MyText.bodyMedium("Refresh", fontWeight: 600),
|
||||||
Tooltip(
|
Tooltip(
|
||||||
message: 'Refresh Data',
|
message: 'Refresh Data',
|
||||||
@ -253,17 +286,18 @@ class _EmployeesScreenState extends State<EmployeesScreen> with UIMixin {
|
|||||||
return Obx(() {
|
return Obx(() {
|
||||||
final isLoading = employeeScreenController.isLoading.value;
|
final isLoading = employeeScreenController.isLoading.value;
|
||||||
final employees = employeeScreenController.employees;
|
final employees = employeeScreenController.employees;
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (employees.isEmpty) {
|
if (employees.isEmpty) {
|
||||||
return Center(
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 50),
|
||||||
|
child: Center(
|
||||||
child: MyText.bodySmall(
|
child: MyText.bodySmall(
|
||||||
"No Assigned Employees Found",
|
"No Assigned Employees Found",
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
|
|||||||
@ -100,7 +100,11 @@ class _LayoutState extends State<Layout> {
|
|||||||
(p) => p.id == selectedProjectId,
|
(p) => p.id == selectedProjectId,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (selectedProject == null && projectController.projects.isNotEmpty) {
|
final hasProjects = projectController.projects.isNotEmpty;
|
||||||
|
|
||||||
|
if (!hasProjects) {
|
||||||
|
projectController.selectedProjectId?.value = '';
|
||||||
|
} else if (selectedProject == null) {
|
||||||
projectController
|
projectController
|
||||||
.updateSelectedProject(projectController.projects.first.id);
|
.updateSelectedProject(projectController.projects.first.id);
|
||||||
}
|
}
|
||||||
@ -111,7 +115,7 @@ class _LayoutState extends State<Layout> {
|
|||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
margin: EdgeInsets.zero,
|
margin: EdgeInsets.zero,
|
||||||
clipBehavior: Clip.antiAlias, // important for overlap inside card
|
clipBehavior: Clip.antiAlias,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
@ -129,7 +133,8 @@ class _LayoutState extends State<Layout> {
|
|||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: GestureDetector(
|
child: hasProjects
|
||||||
|
? GestureDetector(
|
||||||
onTap: () => projectController
|
onTap: () => projectController
|
||||||
.isProjectSelectionExpanded
|
.isProjectSelectionExpanded
|
||||||
.toggle(),
|
.toggle(),
|
||||||
@ -153,7 +158,8 @@ class _LayoutState extends State<Layout> {
|
|||||||
Icon(
|
Icon(
|
||||||
isExpanded
|
isExpanded
|
||||||
? Icons.arrow_drop_up_outlined
|
? Icons.arrow_drop_up_outlined
|
||||||
: Icons.arrow_drop_down_outlined,
|
: Icons
|
||||||
|
.arrow_drop_down_outlined,
|
||||||
color: Colors.black,
|
color: Colors.black,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -167,6 +173,20 @@ class _LayoutState extends State<Layout> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
)
|
||||||
|
: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
MyText.bodyLarge(
|
||||||
|
"No Project Assigned",
|
||||||
|
fontWeight: 700,
|
||||||
|
color: Colors.redAccent,
|
||||||
|
),
|
||||||
|
MyText.bodyMedium(
|
||||||
|
"Hi, ${employeeInfo?.firstName ?? ''}",
|
||||||
|
color: Colors.black54,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (isBetaEnvironment)
|
if (isBetaEnvironment)
|
||||||
@ -193,10 +213,10 @@ class _LayoutState extends State<Layout> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
/// Expanded Project List inside card
|
// Expanded Project List inside card — only show if projects exist
|
||||||
if (isExpanded)
|
if (isExpanded && hasProjects)
|
||||||
Positioned(
|
Positioned(
|
||||||
top: 70, // slightly below the row
|
top: 70,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
child: Container(
|
child: Container(
|
||||||
|
|||||||
@ -50,8 +50,10 @@ class _DailyProgressReportScreenState extends State<DailyProgressReportScreen>
|
|||||||
dailyTaskController.fetchTaskData(initialProjectId);
|
dailyTaskController.fetchTaskData(initialProjectId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final selectedProjectIdRx = projectController.selectedProjectId;
|
||||||
|
if (selectedProjectIdRx != null) {
|
||||||
ever<String?>(
|
ever<String?>(
|
||||||
projectController.selectedProjectId!,
|
selectedProjectIdRx,
|
||||||
(newProjectId) async {
|
(newProjectId) async {
|
||||||
if (newProjectId != null &&
|
if (newProjectId != null &&
|
||||||
newProjectId != dailyTaskController.selectedProjectId) {
|
newProjectId != dailyTaskController.selectedProjectId) {
|
||||||
@ -61,6 +63,10 @@ class _DailyProgressReportScreenState extends State<DailyProgressReportScreen>
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
debugPrint(
|
||||||
|
"Warning: selectedProjectId is null, skipping listener setup.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -133,6 +139,7 @@ class _DailyProgressReportScreenState extends State<DailyProgressReportScreen>
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildActionBar() {
|
Widget _buildActionBar() {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: MySpacing.x(flexSpacing),
|
padding: MySpacing.x(flexSpacing),
|
||||||
|
|||||||
@ -31,18 +31,24 @@ class _DailyTaskPlaningScreenState extends State<DailyTaskPlaningScreen>
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
// Initial fetch
|
// Initial fetch if a project is already selected
|
||||||
final projectId = projectController.selectedProjectId?.value;
|
final projectId = projectController.selectedProjectId?.value;
|
||||||
if (projectId != null) {
|
if (projectId != null) {
|
||||||
dailyTaskPlaningController.fetchTaskData(projectId);
|
dailyTaskPlaningController.fetchTaskData(projectId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reactive fetch on project ID change
|
// Reactive fetch on project ID change
|
||||||
ever<String?>(projectController.selectedProjectId!, (projectId) {
|
final selectedProject = projectController.selectedProjectId;
|
||||||
if (projectId != null) {
|
if (selectedProject != null) {
|
||||||
dailyTaskPlaningController.fetchTaskData(projectId);
|
ever<String?>(
|
||||||
|
selectedProject,
|
||||||
|
(newProjectId) {
|
||||||
|
if (newProjectId != null) {
|
||||||
|
dailyTaskPlaningController.fetchTaskData(newProjectId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@ -69,6 +69,7 @@ dependencies:
|
|||||||
path: ^1.9.0
|
path: ^1.9.0
|
||||||
percent_indicator: ^4.2.2
|
percent_indicator: ^4.2.2
|
||||||
flutter_contacts: ^1.1.9+2
|
flutter_contacts: ^1.1.9+2
|
||||||
|
photo_view: ^0.15.0
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user