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/employee_model.dart';
|
||||
import 'package:marco/model/employees/employee_details_model.dart';
|
||||
import 'package:marco/controller/project_controller.dart';
|
||||
|
||||
final Logger log = Logger();
|
||||
|
||||
@ -12,8 +13,9 @@ class EmployeesScreenController extends GetxController {
|
||||
List<AttendanceModel> attendances = [];
|
||||
List<ProjectModel> projects = [];
|
||||
String? selectedProjectId;
|
||||
List<EmployeeModel> employees = [];
|
||||
List<EmployeeDetailsModel> employeeDetails = [];
|
||||
RxBool isAllEmployeeSelected = false.obs;
|
||||
RxList<EmployeeModel> employees = <EmployeeModel>[].obs;
|
||||
|
||||
RxBool isLoading = false.obs;
|
||||
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
|
||||
@ -24,7 +26,17 @@ class EmployeesScreenController extends GetxController {
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
fetchAllProjects();
|
||||
fetchAllEmployees();
|
||||
|
||||
final projectId = Get.find<ProjectController>().selectedProject?.id;
|
||||
|
||||
if (projectId != null) {
|
||||
selectedProjectId = projectId;
|
||||
fetchEmployeesByProject(projectId);
|
||||
} else if (isAllEmployeeSelected.value) {
|
||||
fetchAllEmployees();
|
||||
} else {
|
||||
clearEmployees();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> fetchAllProjects() async {
|
||||
@ -41,18 +53,27 @@ class EmployeesScreenController extends GetxController {
|
||||
update();
|
||||
}
|
||||
|
||||
void clearEmployees() {
|
||||
employees.clear(); // Correct way to clear RxList
|
||||
log.i("Employees cleared");
|
||||
update(['employee_screen_controller']);
|
||||
}
|
||||
|
||||
Future<void> fetchAllEmployees() async {
|
||||
isLoading.value = true;
|
||||
await _handleApiCall(
|
||||
ApiService.getAllEmployees,
|
||||
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.");
|
||||
},
|
||||
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;
|
||||
update();
|
||||
update(['employee_screen_controller']);
|
||||
}
|
||||
|
||||
Future<void> fetchEmployeesByProject(String? projectId) async {
|
||||
@ -65,22 +86,21 @@ class EmployeesScreenController extends GetxController {
|
||||
await _handleApiCall(
|
||||
() => ApiService.getAllEmployeesByProject(projectId),
|
||||
onSuccess: (data) {
|
||||
employees = data.map((json) => EmployeeModel.fromJson(json)).toList();
|
||||
employees.assignAll(data.map((json) => EmployeeModel.fromJson(json)));
|
||||
for (var emp in employees) {
|
||||
uploadingStates[emp.id] = false.obs;
|
||||
}
|
||||
log.i("Employees fetched: ${employees.length} for project $projectId");
|
||||
update();
|
||||
},
|
||||
onEmpty: () {
|
||||
employees.clear();
|
||||
log.w("No employees found for project $projectId.");
|
||||
employees = [];
|
||||
update();
|
||||
},
|
||||
onError: (e) =>
|
||||
log.e("Error fetching employees for project $projectId: $e"),
|
||||
);
|
||||
isLoading.value = false;
|
||||
update(['employee_screen_controller']);
|
||||
}
|
||||
|
||||
Future<void> _handleApiCall(
|
||||
|
@ -28,6 +28,19 @@ class ProjectController extends GetxController {
|
||||
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.
|
||||
Future<void> fetchProjects() async {
|
||||
isLoadingProjects.value = true;
|
||||
@ -62,6 +75,8 @@ class ProjectController extends GetxController {
|
||||
Future<void> updateSelectedProject(String projectId) async {
|
||||
selectedProjectId?.value = 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:image_picker/image_picker.dart';
|
||||
import 'dart:io';
|
||||
import 'dart:convert';
|
||||
|
||||
final Logger logger = Logger();
|
||||
|
||||
@ -96,6 +97,7 @@ class ReportTaskController extends MyController {
|
||||
required int completedTask,
|
||||
required List<Map<String, dynamic>> checklist,
|
||||
required DateTime reportedDate,
|
||||
List<File>? images,
|
||||
}) async {
|
||||
logger.i("Starting task report...");
|
||||
|
||||
@ -132,12 +134,24 @@ class ReportTaskController extends MyController {
|
||||
|
||||
try {
|
||||
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(
|
||||
id: projectId,
|
||||
comment: commentField,
|
||||
completedTask: completedTask,
|
||||
completedTask: completedWorkInt,
|
||||
checkList: checklist,
|
||||
images: imageData,
|
||||
);
|
||||
|
||||
if (success) {
|
||||
@ -169,6 +183,7 @@ class ReportTaskController extends MyController {
|
||||
Future<void> commentTask({
|
||||
required String projectId,
|
||||
required String comment,
|
||||
List<File>? images,
|
||||
}) async {
|
||||
logger.i("Starting task comment...");
|
||||
|
||||
@ -184,10 +199,22 @@ class ReportTaskController extends MyController {
|
||||
|
||||
try {
|
||||
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(
|
||||
id: projectId,
|
||||
comment: commentField,
|
||||
images: imageData,
|
||||
);
|
||||
|
||||
if (success) {
|
||||
|
@ -343,6 +343,7 @@ class ApiService {
|
||||
required int completedTask,
|
||||
required String comment,
|
||||
required List<Map<String, dynamic>> checkList,
|
||||
List<Map<String, dynamic>>? images,
|
||||
}) async {
|
||||
final body = {
|
||||
"id": id,
|
||||
@ -350,6 +351,7 @@ class ApiService {
|
||||
"comment": comment,
|
||||
"reportedDate": DateTime.now().toUtc().toIso8601String(),
|
||||
"checkList": checkList,
|
||||
if (images != null && images.isNotEmpty) "images": images,
|
||||
};
|
||||
|
||||
final response = await _postRequest(ApiEndpoints.reportTask, body);
|
||||
@ -373,11 +375,13 @@ class ApiService {
|
||||
static Future<bool> commentTask({
|
||||
required String id,
|
||||
required String comment,
|
||||
List<Map<String, dynamic>>? images,
|
||||
}) async {
|
||||
final body = {
|
||||
"taskAllocationId": id,
|
||||
"comment": comment,
|
||||
"commentDate": DateTime.now().toUtc().toIso8601String(),
|
||||
if (images != null && images.isNotEmpty) "images": images,
|
||||
};
|
||||
|
||||
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:logger/logger.dart';
|
||||
import 'package:marco/helpers/services/api_endpoints.dart';
|
||||
import 'package:marco/controller/project_controller.dart';
|
||||
|
||||
final Logger logger = Logger();
|
||||
|
||||
@ -369,7 +370,8 @@ class AuthService {
|
||||
|
||||
// ✅ Put and load PermissionController
|
||||
final permissionController = Get.put(PermissionController());
|
||||
await permissionController.loadData(jwtToken);
|
||||
await permissionController.loadData(jwtToken);
|
||||
await Get.find<ProjectController>().fetchProjects();
|
||||
|
||||
isLoggedIn = true;
|
||||
}
|
||||
|
@ -5,7 +5,9 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:marco/model/user_permission.dart';
|
||||
import 'package:marco/model/employee_info.dart';
|
||||
import 'dart:convert';
|
||||
import 'package:get/route_manager.dart';
|
||||
import 'package:marco/controller/project_controller.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
|
||||
class LocalStorage {
|
||||
static const String _loggedInUserKey = "user";
|
||||
@ -134,20 +136,25 @@ class LocalStorage {
|
||||
return setToken(_refreshTokenKey, refreshToken);
|
||||
}
|
||||
|
||||
static Future<void> logout() async {
|
||||
await removeLoggedInUser();
|
||||
await removeToken(_jwtTokenKey);
|
||||
await removeToken(_refreshTokenKey);
|
||||
await removeUserPermissions();
|
||||
await removeEmployeeInfo();
|
||||
await removeMpinToken();
|
||||
await removeIsMpin();
|
||||
await preferences.remove("mpin_verified");
|
||||
await preferences.remove(_languageKey);
|
||||
await preferences.remove(_themeCustomizerKey);
|
||||
Get.offAllNamed('/auth/login-option');
|
||||
static Future<void> logout() async {
|
||||
await removeLoggedInUser();
|
||||
await removeToken(_jwtTokenKey);
|
||||
await removeToken(_refreshTokenKey);
|
||||
await removeUserPermissions();
|
||||
await removeEmployeeInfo();
|
||||
await removeMpinToken();
|
||||
await removeIsMpin();
|
||||
await preferences.remove("mpin_verified");
|
||||
await preferences.remove(_languageKey);
|
||||
await preferences.remove(_themeCustomizerKey);
|
||||
await preferences.remove('selectedProjectId');
|
||||
if (Get.isRegistered<ProjectController>()) {
|
||||
Get.find<ProjectController>().clearProjects();
|
||||
}
|
||||
|
||||
Get.offAllNamed('/auth/login-option');
|
||||
}
|
||||
|
||||
static Future<bool> setMpinToken(String token) {
|
||||
return preferences.setString(_mpinTokenKey, token);
|
||||
}
|
||||
|
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/my_team_model_sheet.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:marco/helpers/widgets/image_viewer_dialog.dart';
|
||||
|
||||
class CommentTaskBottomSheet extends StatefulWidget {
|
||||
final Map<String, dynamic> taskData;
|
||||
@ -206,6 +207,148 @@ class _CommentTaskBottomSheetState extends State<CommentTaskBottomSheet>
|
||||
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),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
@ -233,7 +376,9 @@ class _CommentTaskBottomSheetState extends State<CommentTaskBottomSheet>
|
||||
.getController('comment')
|
||||
?.text ??
|
||||
'',
|
||||
images: controller.selectedImages,
|
||||
);
|
||||
|
||||
if (widget.onCommentSuccess != null) {
|
||||
widget.onCommentSuccess!();
|
||||
}
|
||||
@ -262,12 +407,13 @@ class _CommentTaskBottomSheetState extends State<CommentTaskBottomSheet>
|
||||
}),
|
||||
],
|
||||
),
|
||||
MySpacing.height(24),
|
||||
MySpacing.height(10),
|
||||
if ((widget.taskData['taskComments'] as List<dynamic>?)
|
||||
?.isNotEmpty ==
|
||||
true) ...[
|
||||
Row(
|
||||
children: [
|
||||
MySpacing.width(10),
|
||||
Icon(Icons.chat_bubble_outline,
|
||||
size: 18, color: Colors.grey[700]),
|
||||
MySpacing.width(8),
|
||||
@ -277,6 +423,7 @@ class _CommentTaskBottomSheetState extends State<CommentTaskBottomSheet>
|
||||
),
|
||||
],
|
||||
),
|
||||
Divider(),
|
||||
MySpacing.height(12),
|
||||
Builder(
|
||||
builder: (context) {
|
||||
@ -298,84 +445,236 @@ class _CommentTaskBottomSheetState extends State<CommentTaskBottomSheet>
|
||||
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'] ?? '');
|
||||
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,
|
||||
// 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(
|
||||
margin: EdgeInsets.symmetric(
|
||||
vertical: 0, horizontal: 0),
|
||||
padding: EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade200,
|
||||
borderRadius:
|
||||
BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
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(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment
|
||||
.spaceBetween,
|
||||
children: [
|
||||
MyText.bodyMedium(
|
||||
commentText,
|
||||
fontWeight: 500,
|
||||
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,
|
||||
),
|
||||
]),
|
||||
SizedBox(height: 8),
|
||||
SizedBox(
|
||||
height: 60,
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
child:
|
||||
ClipRRect(
|
||||
borderRadius:
|
||||
BorderRadius.circular(
|
||||
12),
|
||||
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),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
relativeTime,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.black54,
|
||||
),
|
||||
)
|
||||
SizedBox(height: 12),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 6),
|
||||
Text(
|
||||
commentText,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
@ -10,8 +10,12 @@ import 'package:marco/helpers/widgets/my_text_style.dart';
|
||||
|
||||
class ReportTaskBottomSheet extends StatefulWidget {
|
||||
final Map<String, dynamic> taskData;
|
||||
final VoidCallback? onReportSuccess;
|
||||
const ReportTaskBottomSheet({super.key, required this.taskData,this.onReportSuccess,});
|
||||
final VoidCallback? onReportSuccess;
|
||||
const ReportTaskBottomSheet({
|
||||
super.key,
|
||||
required this.taskData,
|
||||
this.onReportSuccess,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ReportTaskBottomSheet> createState() => _ReportTaskBottomSheetState();
|
||||
@ -201,6 +205,147 @@ class _ReportTaskBottomSheetState extends State<ReportTaskBottomSheet>
|
||||
),
|
||||
),
|
||||
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(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
@ -237,6 +382,7 @@ class _ReportTaskBottomSheetState extends State<ReportTaskBottomSheet>
|
||||
0,
|
||||
checklist: [],
|
||||
reportedDate: DateTime.now(),
|
||||
images: controller.selectedImages,
|
||||
);
|
||||
if (widget.onReportSuccess != null) {
|
||||
widget.onReportSuccess!();
|
||||
|
@ -236,8 +236,17 @@ class _MPINAuthScreenState extends State<MPINAuthScreen> with UIMixin {
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
onChanged: (value) =>
|
||||
controller.onDigitChanged(value, index, isRetype: isRetype),
|
||||
onChanged: (value) {
|
||||
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(
|
||||
counterText: '',
|
||||
filled: true,
|
||||
|
@ -36,19 +36,27 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
final projectController = Get.find<ProjectController>();
|
||||
ever<String?>(projectController.selectedProjectId!, (projectId) async {
|
||||
if (projectId != null && projectId.isNotEmpty) {
|
||||
try {
|
||||
await attendanceController.fetchEmployeesByProject(projectId);
|
||||
await attendanceController.fetchAttendanceLogs(projectId);
|
||||
await attendanceController.fetchRegularizationLogs(projectId);
|
||||
await attendanceController.fetchProjectData(projectId);
|
||||
attendanceController.update(['attendance_dashboard_controller']);
|
||||
} catch (e) {
|
||||
debugPrint("Error updating data on project change: $e");
|
||||
}
|
||||
}
|
||||
final attendanceController = Get.find<AttendanceController>();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
ever<String?>(
|
||||
projectController.selectedProjectId!,
|
||||
(projectId) async {
|
||||
if (projectId != null && projectId.isNotEmpty) {
|
||||
try {
|
||||
await attendanceController.fetchEmployeesByProject(projectId);
|
||||
await attendanceController.fetchAttendanceLogs(projectId);
|
||||
await attendanceController.fetchRegularizationLogs(projectId);
|
||||
await attendanceController.fetchProjectData(projectId);
|
||||
attendanceController.update(['attendance_dashboard_controller']);
|
||||
} catch (e) {
|
||||
debugPrint("Error updating data on project change: $e");
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@ -107,6 +115,12 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
||||
init: attendanceController,
|
||||
tag: 'attendance_dashboard_controller',
|
||||
builder: (controller) {
|
||||
final selectedProjectId =
|
||||
Get.find<ProjectController>().selectedProjectId?.value;
|
||||
|
||||
final bool noProjectSelected =
|
||||
selectedProjectId == null || selectedProjectId.isEmpty;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -234,11 +248,22 @@ class _AttendanceScreenState extends State<AttendanceScreen> with UIMixin {
|
||||
MyFlex(children: [
|
||||
MyFlexItem(
|
||||
sizes: 'lg-12 md-12 sm-12',
|
||||
child: selectedTab == 'todaysAttendance'
|
||||
? employeeListTab()
|
||||
: selectedTab == 'attendanceLogs'
|
||||
? employeeLog()
|
||||
: regularizationScreen(),
|
||||
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()
|
||||
: selectedTab == 'attendanceLogs'
|
||||
? employeeLog()
|
||||
: regularizationScreen(),
|
||||
),
|
||||
]),
|
||||
],
|
||||
|
@ -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_text.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';
|
||||
import 'package:marco/controller/project_controller.dart';
|
||||
|
||||
class EmployeesScreen extends StatefulWidget {
|
||||
const EmployeesScreen({super.key});
|
||||
|
||||
@ -27,52 +27,45 @@ class _EmployeesScreenState extends State<EmployeesScreen> with UIMixin {
|
||||
Get.put(EmployeesScreenController());
|
||||
final PermissionController permissionController =
|
||||
Get.put(PermissionController());
|
||||
Future<void> _refreshEmployees() async {
|
||||
try {
|
||||
final selectedProjectId =
|
||||
Get.find<ProjectController>().selectedProject?.id;
|
||||
final isAllSelected =
|
||||
employeeScreenController.isAllEmployeeSelected.value;
|
||||
|
||||
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) {
|
||||
if (isAllSelected) {
|
||||
employeeScreenController.selectedProjectId = null;
|
||||
await employeeScreenController.fetchAllEmployees();
|
||||
} else if (selectedProjectId != null) {
|
||||
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']);
|
||||
await employeeScreenController
|
||||
.fetchEmployeesByProject(selectedProjectId);
|
||||
} else {
|
||||
// ❗ Clear employees if neither selected
|
||||
employeeScreenController.clearEmployees();
|
||||
}
|
||||
|
||||
employeeScreenController.update(['employee_screen_controller']);
|
||||
} catch (e, stackTrace) {
|
||||
debugPrint('Error refreshing employee data: ${e.toString()}');
|
||||
debugPrintStack(stackTrace: stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
void initState() {
|
||||
super.initState();
|
||||
final selectedProjectId = Get.find<ProjectController>().selectedProject?.id;
|
||||
|
||||
if (selectedProjectId != null) {
|
||||
employeeScreenController.selectedProjectId = selectedProjectId;
|
||||
employeeScreenController.fetchEmployeesByProject(selectedProjectId);
|
||||
} else if (employeeScreenController.isAllEmployeeSelected.value) {
|
||||
employeeScreenController.selectedProjectId = null;
|
||||
employeeScreenController.fetchAllEmployees();
|
||||
} else {
|
||||
employeeScreenController.clearEmployees();
|
||||
}
|
||||
}
|
||||
|
||||
@ -194,26 +187,66 @@ class _EmployeesScreenState extends State<EmployeesScreen> with UIMixin {
|
||||
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,
|
||||
Obx(() {
|
||||
return Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
value: employeeScreenController
|
||||
.isAllEmployeeSelected.value,
|
||||
activeColor: Colors.blueAccent,
|
||||
fillColor:
|
||||
MaterialStateProperty.resolveWith<Color?>(
|
||||
(states) {
|
||||
if (states.contains(MaterialState.selected)) {
|
||||
return Colors.blueAccent;
|
||||
}
|
||||
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']);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
MyText.bodyMedium(
|
||||
"All Employees",
|
||||
fontWeight: 600,
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
const SizedBox(width: 16),
|
||||
MyText.bodyMedium("Refresh", fontWeight: 600),
|
||||
Tooltip(
|
||||
message: 'Refresh Data',
|
||||
@ -253,16 +286,17 @@ class _EmployeesScreenState extends State<EmployeesScreen> with UIMixin {
|
||||
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 Padding(
|
||||
padding: const EdgeInsets.only(top: 50),
|
||||
child: Center(
|
||||
child: MyText.bodySmall(
|
||||
"No Assigned Employees Found",
|
||||
fontWeight: 600,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -100,7 +100,11 @@ class _LayoutState extends State<Layout> {
|
||||
(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
|
||||
.updateSelectedProject(projectController.projects.first.id);
|
||||
}
|
||||
@ -111,7 +115,7 @@ class _LayoutState extends State<Layout> {
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
margin: EdgeInsets.zero,
|
||||
clipBehavior: Clip.antiAlias, // important for overlap inside card
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Stack(
|
||||
children: [
|
||||
Padding(
|
||||
@ -129,45 +133,61 @@ class _LayoutState extends State<Layout> {
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => projectController
|
||||
.isProjectSelectionExpanded
|
||||
.toggle(),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
child: hasProjects
|
||||
? GestureDetector(
|
||||
onTap: () => projectController
|
||||
.isProjectSelectionExpanded
|
||||
.toggle(),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: MyText.bodyLarge(
|
||||
selectedProject?.name ??
|
||||
"Select Project",
|
||||
fontWeight: 700,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: MyText.bodyLarge(
|
||||
selectedProject?.name ??
|
||||
"Select Project",
|
||||
fontWeight: 700,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
isExpanded
|
||||
? Icons.arrow_drop_up_outlined
|
||||
: Icons
|
||||
.arrow_drop_down_outlined,
|
||||
color: Colors.black,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
isExpanded
|
||||
? Icons.arrow_drop_up_outlined
|
||||
: Icons.arrow_drop_down_outlined,
|
||||
color: Colors.black,
|
||||
),
|
||||
],
|
||||
),
|
||||
MyText.bodyMedium(
|
||||
"Hi, ${employeeInfo?.firstName ?? ''}",
|
||||
color: Colors.black54,
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.bodyLarge(
|
||||
"No Project Assigned",
|
||||
fontWeight: 700,
|
||||
color: Colors.redAccent,
|
||||
),
|
||||
MyText.bodyMedium(
|
||||
"Hi, ${employeeInfo?.firstName ?? ''}",
|
||||
color: Colors.black54,
|
||||
),
|
||||
],
|
||||
),
|
||||
MyText.bodyMedium(
|
||||
"Hi, ${employeeInfo?.firstName ?? ''}",
|
||||
color: Colors.black54,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isBetaEnvironment)
|
||||
Container(
|
||||
@ -193,10 +213,10 @@ class _LayoutState extends State<Layout> {
|
||||
),
|
||||
),
|
||||
|
||||
/// Expanded Project List inside card
|
||||
if (isExpanded)
|
||||
// Expanded Project List inside card — only show if projects exist
|
||||
if (isExpanded && hasProjects)
|
||||
Positioned(
|
||||
top: 70, // slightly below the row
|
||||
top: 70,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
|
@ -50,17 +50,23 @@ class _DailyProgressReportScreenState extends State<DailyProgressReportScreen>
|
||||
dailyTaskController.fetchTaskData(initialProjectId);
|
||||
}
|
||||
|
||||
ever<String?>(
|
||||
projectController.selectedProjectId!,
|
||||
(newProjectId) async {
|
||||
if (newProjectId != null &&
|
||||
newProjectId != dailyTaskController.selectedProjectId) {
|
||||
dailyTaskController.selectedProjectId = newProjectId;
|
||||
await dailyTaskController.fetchTaskData(newProjectId);
|
||||
dailyTaskController.update(['daily_progress_report_controller']);
|
||||
}
|
||||
},
|
||||
);
|
||||
final selectedProjectIdRx = projectController.selectedProjectId;
|
||||
if (selectedProjectIdRx != null) {
|
||||
ever<String?>(
|
||||
selectedProjectIdRx,
|
||||
(newProjectId) async {
|
||||
if (newProjectId != null &&
|
||||
newProjectId != dailyTaskController.selectedProjectId) {
|
||||
dailyTaskController.selectedProjectId = newProjectId;
|
||||
await dailyTaskController.fetchTaskData(newProjectId);
|
||||
dailyTaskController.update(['daily_progress_report_controller']);
|
||||
}
|
||||
},
|
||||
);
|
||||
} else {
|
||||
debugPrint(
|
||||
"Warning: selectedProjectId is null, skipping listener setup.");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@ -133,6 +139,7 @@ class _DailyProgressReportScreenState extends State<DailyProgressReportScreen>
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionBar() {
|
||||
return Padding(
|
||||
padding: MySpacing.x(flexSpacing),
|
||||
|
@ -31,18 +31,24 @@ class _DailyTaskPlaningScreenState extends State<DailyTaskPlaningScreen>
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// Initial fetch
|
||||
// Initial fetch if a project is already selected
|
||||
final projectId = projectController.selectedProjectId?.value;
|
||||
if (projectId != null) {
|
||||
dailyTaskPlaningController.fetchTaskData(projectId);
|
||||
}
|
||||
|
||||
// Reactive fetch on project ID change
|
||||
ever<String?>(projectController.selectedProjectId!, (projectId) {
|
||||
if (projectId != null) {
|
||||
dailyTaskPlaningController.fetchTaskData(projectId);
|
||||
}
|
||||
});
|
||||
final selectedProject = projectController.selectedProjectId;
|
||||
if (selectedProject != null) {
|
||||
ever<String?>(
|
||||
selectedProject,
|
||||
(newProjectId) {
|
||||
if (newProjectId != null) {
|
||||
dailyTaskPlaningController.fetchTaskData(newProjectId);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -182,7 +182,7 @@ class _ReportTaskScreenState extends State<ReportTaskScreen> with UIMixin {
|
||||
),
|
||||
),
|
||||
MySpacing.height(24),
|
||||
|
||||
|
||||
// Buttons
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
|
@ -69,6 +69,7 @@ dependencies:
|
||||
path: ^1.9.0
|
||||
percent_indicator: ^4.2.2
|
||||
flutter_contacts: ^1.1.9+2
|
||||
photo_view: ^0.15.0
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
Loading…
x
Reference in New Issue
Block a user