fixed the bugs
This commit is contained in:
parent
e6238ca5b0
commit
acb203848e
@ -7,6 +7,7 @@ import 'package:marco/model/directory/directory_comment_model.dart';
|
||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||
|
||||
class DirectoryController extends GetxController {
|
||||
// Contacts
|
||||
RxList<ContactModel> allContacts = <ContactModel>[].obs;
|
||||
RxList<ContactModel> filteredContacts = <ContactModel>[].obs;
|
||||
RxList<ContactCategory> contactCategories = <ContactCategory>[].obs;
|
||||
@ -16,16 +17,10 @@ class DirectoryController extends GetxController {
|
||||
RxBool isLoading = false.obs;
|
||||
RxList<ContactBucket> contactBuckets = <ContactBucket>[].obs;
|
||||
RxString searchQuery = ''.obs;
|
||||
RxBool showFabMenu = false.obs;
|
||||
final RxBool showFullEditorToolbar = false.obs;
|
||||
final RxBool isEditorFocused = false.obs;
|
||||
RxBool isNotesView = false.obs;
|
||||
|
||||
final Map<String, RxList<DirectoryComment>> contactCommentsMap = {};
|
||||
RxList<DirectoryComment> getCommentsForContact(String contactId) {
|
||||
return contactCommentsMap[contactId] ?? <DirectoryComment>[].obs;
|
||||
}
|
||||
|
||||
// Notes / Comments
|
||||
final Map<String, RxList<DirectoryComment>> activeCommentsMap = {};
|
||||
final Map<String, RxList<DirectoryComment>> inactiveCommentsMap = {};
|
||||
final editingCommentId = Rxn<String>();
|
||||
|
||||
@override
|
||||
@ -34,26 +29,53 @@ class DirectoryController extends GetxController {
|
||||
fetchContacts();
|
||||
fetchBuckets();
|
||||
}
|
||||
// inside DirectoryController
|
||||
|
||||
// -------------------- COMMENTS --------------------
|
||||
|
||||
RxList<DirectoryComment> getCommentsForContact(String contactId, {bool active = true}) {
|
||||
return active
|
||||
? activeCommentsMap[contactId] ?? <DirectoryComment>[].obs
|
||||
: inactiveCommentsMap[contactId] ?? <DirectoryComment>[].obs;
|
||||
}
|
||||
|
||||
Future<void> fetchCommentsForContact(String contactId, {bool active = true}) async {
|
||||
try {
|
||||
final data = await ApiService.getDirectoryComments(contactId, active: active);
|
||||
final comments = data?.map((e) => DirectoryComment.fromJson(e)).toList() ?? [];
|
||||
|
||||
if (active) {
|
||||
activeCommentsMap[contactId] = <DirectoryComment>[].obs..assignAll(comments);
|
||||
} else {
|
||||
inactiveCommentsMap[contactId] = <DirectoryComment>[].obs..assignAll(comments);
|
||||
}
|
||||
} catch (e, stack) {
|
||||
logSafe("Error fetching ${active ? 'active' : 'inactive'} comments: $e", level: LogLevel.error);
|
||||
logSafe(stack.toString(), level: LogLevel.debug);
|
||||
|
||||
if (active) {
|
||||
activeCommentsMap[contactId] = <DirectoryComment>[].obs;
|
||||
} else {
|
||||
inactiveCommentsMap[contactId] = <DirectoryComment>[].obs;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RxList<DirectoryComment> combinedComments(String contactId) {
|
||||
final active = getCommentsForContact(contactId, active: true).toList();
|
||||
final inactive = getCommentsForContact(contactId, active: false).toList();
|
||||
|
||||
final combined = [...active, ...inactive]
|
||||
..sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||
|
||||
return combined.obs;
|
||||
}
|
||||
|
||||
Future<void> updateComment(DirectoryComment comment) async {
|
||||
try {
|
||||
logSafe(
|
||||
"Attempting to update comment. id: ${comment.id}, contactId: ${comment.contactId}");
|
||||
|
||||
final commentList = contactCommentsMap[comment.contactId];
|
||||
final oldComment =
|
||||
commentList?.firstWhereOrNull((c) => c.id == comment.id);
|
||||
|
||||
if (oldComment == null) {
|
||||
logSafe("Old comment not found. id: ${comment.id}");
|
||||
} else {
|
||||
logSafe("Old comment note: ${oldComment.note}");
|
||||
logSafe("New comment note: ${comment.note}");
|
||||
}
|
||||
getCommentsForContact(comment.contactId).firstWhereOrNull((c) => c.id == comment.id);
|
||||
|
||||
if (oldComment != null && oldComment.note.trim() == comment.note.trim()) {
|
||||
logSafe("No changes detected in comment. id: ${comment.id}");
|
||||
showAppSnackbar(
|
||||
title: "No Changes",
|
||||
message: "No changes were made to the comment.",
|
||||
@ -62,33 +84,28 @@ class DirectoryController extends GetxController {
|
||||
return;
|
||||
}
|
||||
|
||||
final success = await ApiService.updateContactComment(
|
||||
comment.id,
|
||||
comment.note,
|
||||
comment.contactId,
|
||||
);
|
||||
final success = await ApiService.updateContactComment(comment.id, comment.note, comment.contactId);
|
||||
|
||||
if (success) {
|
||||
logSafe("Comment updated successfully. id: ${comment.id}");
|
||||
await fetchCommentsForContact(comment.contactId);
|
||||
await fetchCommentsForContact(comment.contactId, active: true);
|
||||
await fetchCommentsForContact(comment.contactId, active: false);
|
||||
|
||||
// ✅ Show success message
|
||||
showAppSnackbar(
|
||||
title: "Success",
|
||||
message: "Comment updated successfully.",
|
||||
type: SnackbarType.success,
|
||||
);
|
||||
} else {
|
||||
logSafe("Failed to update comment via API. id: ${comment.id}");
|
||||
showAppSnackbar(
|
||||
title: "Error",
|
||||
message: "Failed to update comment.",
|
||||
type: SnackbarType.error,
|
||||
);
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
logSafe("Update comment failed: ${e.toString()}");
|
||||
logSafe("StackTrace: ${stackTrace.toString()}");
|
||||
} catch (e, stack) {
|
||||
logSafe("Update comment failed: ${e.toString()}", level: LogLevel.error);
|
||||
logSafe(stack.toString(), level: LogLevel.debug);
|
||||
|
||||
showAppSnackbar(
|
||||
title: "Error",
|
||||
message: "Failed to update comment.",
|
||||
@ -97,45 +114,15 @@ class DirectoryController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> fetchCommentsForContact(String contactId,
|
||||
{bool active = true}) async {
|
||||
try {
|
||||
final data =
|
||||
await ApiService.getDirectoryComments(contactId, active: active);
|
||||
logSafe(
|
||||
"Fetched ${active ? 'active' : 'inactive'} comments for contact $contactId: $data");
|
||||
|
||||
final comments =
|
||||
data?.map((e) => DirectoryComment.fromJson(e)).toList() ?? [];
|
||||
|
||||
if (!contactCommentsMap.containsKey(contactId)) {
|
||||
contactCommentsMap[contactId] = <DirectoryComment>[].obs;
|
||||
}
|
||||
|
||||
contactCommentsMap[contactId]!.assignAll(comments);
|
||||
contactCommentsMap[contactId]?.refresh();
|
||||
} catch (e) {
|
||||
logSafe(
|
||||
"Error fetching ${active ? 'active' : 'inactive'} comments for contact $contactId: $e",
|
||||
level: LogLevel.error);
|
||||
|
||||
contactCommentsMap[contactId] ??= <DirectoryComment>[].obs;
|
||||
contactCommentsMap[contactId]!.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// 🗑️ Delete a comment (soft delete)
|
||||
Future<void> deleteComment(String commentId, String contactId) async {
|
||||
try {
|
||||
logSafe("Deleting comment. id: $commentId");
|
||||
|
||||
final success = await ApiService.restoreContactComment(commentId, false);
|
||||
|
||||
if (success) {
|
||||
logSafe("Comment deleted successfully. id: $commentId");
|
||||
if (editingCommentId.value == commentId) editingCommentId.value = null;
|
||||
|
||||
// Refresh comments after deletion
|
||||
await fetchCommentsForContact(contactId);
|
||||
await fetchCommentsForContact(contactId, active: true);
|
||||
await fetchCommentsForContact(contactId, active: false);
|
||||
|
||||
showAppSnackbar(
|
||||
title: "Deleted",
|
||||
@ -143,7 +130,6 @@ class DirectoryController extends GetxController {
|
||||
type: SnackbarType.success,
|
||||
);
|
||||
} else {
|
||||
logSafe("Failed to delete comment via API. id: $commentId");
|
||||
showAppSnackbar(
|
||||
title: "Error",
|
||||
message: "Failed to delete comment.",
|
||||
@ -151,8 +137,9 @@ class DirectoryController extends GetxController {
|
||||
);
|
||||
}
|
||||
} catch (e, stack) {
|
||||
logSafe("Delete comment failed: ${e.toString()}", level: LogLevel.error);
|
||||
logSafe("StackTrace: $stack", level: LogLevel.debug);
|
||||
logSafe("Delete comment failed: $e", level: LogLevel.error);
|
||||
logSafe(stack.toString(), level: LogLevel.debug);
|
||||
|
||||
showAppSnackbar(
|
||||
title: "Error",
|
||||
message: "Something went wrong while deleting comment.",
|
||||
@ -161,18 +148,13 @@ class DirectoryController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
/// ♻️ Restore a previously deleted comment
|
||||
Future<void> restoreComment(String commentId, String contactId) async {
|
||||
try {
|
||||
logSafe("Restoring comment. id: $commentId");
|
||||
|
||||
final success = await ApiService.restoreContactComment(commentId, true);
|
||||
|
||||
if (success) {
|
||||
logSafe("Comment restored successfully. id: $commentId");
|
||||
|
||||
// Refresh comments after restore
|
||||
await fetchCommentsForContact(contactId);
|
||||
await fetchCommentsForContact(contactId, active: true);
|
||||
await fetchCommentsForContact(contactId, active: false);
|
||||
|
||||
showAppSnackbar(
|
||||
title: "Restored",
|
||||
@ -180,7 +162,6 @@ class DirectoryController extends GetxController {
|
||||
type: SnackbarType.success,
|
||||
);
|
||||
} else {
|
||||
logSafe("Failed to restore comment via API. id: $commentId");
|
||||
showAppSnackbar(
|
||||
title: "Error",
|
||||
message: "Failed to restore comment.",
|
||||
@ -188,8 +169,9 @@ class DirectoryController extends GetxController {
|
||||
);
|
||||
}
|
||||
} catch (e, stack) {
|
||||
logSafe("Restore comment failed: ${e.toString()}", level: LogLevel.error);
|
||||
logSafe("StackTrace: $stack", level: LogLevel.debug);
|
||||
logSafe("Restore comment failed: $e", level: LogLevel.error);
|
||||
logSafe(stack.toString(), level: LogLevel.debug);
|
||||
|
||||
showAppSnackbar(
|
||||
title: "Error",
|
||||
message: "Something went wrong while restoring comment.",
|
||||
@ -198,6 +180,8 @@ class DirectoryController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------- CONTACTS --------------------
|
||||
|
||||
Future<void> fetchBuckets() async {
|
||||
try {
|
||||
final response = await ApiService.getContactBucketList();
|
||||
@ -219,7 +203,6 @@ class DirectoryController extends GetxController {
|
||||
isLoading.value = true;
|
||||
|
||||
final response = await ApiService.getDirectoryData(isActive: active);
|
||||
|
||||
if (response != null) {
|
||||
final contacts = response.map((e) => ContactModel.fromJson(e)).toList();
|
||||
allContacts.assignAll(contacts);
|
||||
@ -238,14 +221,12 @@ class DirectoryController extends GetxController {
|
||||
|
||||
void extractCategoriesFromContacts() {
|
||||
final uniqueCategories = <String, ContactCategory>{};
|
||||
|
||||
for (final contact in allContacts) {
|
||||
final category = contact.contactCategory;
|
||||
if (category != null && !uniqueCategories.containsKey(category.id)) {
|
||||
uniqueCategories[category.id] = category;
|
||||
}
|
||||
}
|
||||
|
||||
contactCategories.value = uniqueCategories.values.toList();
|
||||
}
|
||||
|
||||
@ -271,11 +252,8 @@ class DirectoryController extends GetxController {
|
||||
final categoryNameMatch =
|
||||
contact.contactCategory?.name.toLowerCase().contains(query) ?? false;
|
||||
final bucketNameMatch = contact.bucketIds.any((id) {
|
||||
final bucketName = contactBuckets
|
||||
.firstWhereOrNull((b) => b.id == id)
|
||||
?.name
|
||||
.toLowerCase() ??
|
||||
'';
|
||||
final bucketName =
|
||||
contactBuckets.firstWhereOrNull((b) => b.id == id)?.name.toLowerCase() ?? '';
|
||||
return bucketName.contains(query);
|
||||
});
|
||||
|
||||
@ -291,7 +269,6 @@ class DirectoryController extends GetxController {
|
||||
return categoryMatch && bucketMatch && searchMatch;
|
||||
}).toList();
|
||||
|
||||
// 🔑 Ensure results are always alphabetically sorted
|
||||
filteredContacts
|
||||
.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import 'package:marco/helpers/services/app_logger.dart';
|
||||
import 'package:marco/helpers/services/api_service.dart';
|
||||
import 'package:marco/model/project_model.dart';
|
||||
import 'package:marco/model/dailyTaskPlanning/daily_task_model.dart';
|
||||
import 'package:marco/model/dailyTaskPlanning/daily_progress_report_filter_response_model.dart';
|
||||
|
||||
class DailyTaskController extends GetxController {
|
||||
List<ProjectModel> projects = [];
|
||||
@ -23,6 +24,12 @@ class DailyTaskController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
RxSet<String> selectedBuildings = <String>{}.obs;
|
||||
RxSet<String> selectedFloors = <String>{}.obs;
|
||||
RxSet<String> selectedActivities = <String>{}.obs;
|
||||
RxSet<String> selectedServices = <String>{}.obs;
|
||||
|
||||
RxBool isFilterLoading = false.obs;
|
||||
RxBool isLoading = true.obs;
|
||||
RxBool isLoadingMore = false.obs;
|
||||
Map<String, List<TaskModel>> groupedDailyTasks = {};
|
||||
@ -51,9 +58,18 @@ class DailyTaskController extends GetxController {
|
||||
);
|
||||
}
|
||||
|
||||
void clearTaskFilters() {
|
||||
selectedBuildings.clear();
|
||||
selectedFloors.clear();
|
||||
selectedActivities.clear();
|
||||
selectedServices.clear();
|
||||
startDateTask = null;
|
||||
endDateTask = null;
|
||||
update();
|
||||
}
|
||||
|
||||
Future<void> fetchTaskData(
|
||||
String projectId, {
|
||||
List<String>? serviceIds,
|
||||
int pageNumber = 1,
|
||||
int pageSize = 20,
|
||||
bool isLoadMore = false,
|
||||
@ -68,11 +84,19 @@ class DailyTaskController extends GetxController {
|
||||
isLoadingMore.value = true;
|
||||
}
|
||||
|
||||
// Create the filter object
|
||||
final filter = {
|
||||
"buildingIds": selectedBuildings.toList(),
|
||||
"floorIds": selectedFloors.toList(),
|
||||
"activityIds": selectedActivities.toList(),
|
||||
"serviceIds": selectedServices.toList(),
|
||||
"dateFrom": startDateTask?.toIso8601String(),
|
||||
"dateTo": endDateTask?.toIso8601String(),
|
||||
};
|
||||
|
||||
final response = await ApiService.getDailyTasks(
|
||||
projectId,
|
||||
dateFrom: startDateTask,
|
||||
dateTo: endDateTask,
|
||||
serviceIds: serviceIds,
|
||||
filter: filter,
|
||||
pageNumber: pageNumber,
|
||||
pageSize: pageSize,
|
||||
);
|
||||
@ -95,6 +119,35 @@ class DailyTaskController extends GetxController {
|
||||
update();
|
||||
}
|
||||
|
||||
FilterData? taskFilterData;
|
||||
|
||||
Future<void> fetchTaskFilter(String projectId) async {
|
||||
isFilterLoading.value = true;
|
||||
try {
|
||||
final filterResponse = await ApiService.getDailyTaskFilter(projectId);
|
||||
|
||||
if (filterResponse != null && filterResponse.success) {
|
||||
taskFilterData =
|
||||
filterResponse.data; // now taskFilterData is FilterData?
|
||||
logSafe(
|
||||
"Task filter fetched successfully. Buildings: ${taskFilterData?.buildings.length}, Floors: ${taskFilterData?.floors.length}",
|
||||
level: LogLevel.info,
|
||||
);
|
||||
} else {
|
||||
logSafe(
|
||||
"Failed to fetch task filter for projectId: $projectId",
|
||||
level: LogLevel.warning,
|
||||
);
|
||||
}
|
||||
} catch (e, stack) {
|
||||
logSafe("Exception in fetchTaskFilter: $e", level: LogLevel.error);
|
||||
logSafe("StackTrace: $stack", level: LogLevel.debug);
|
||||
} finally {
|
||||
isFilterLoading.value = false;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> selectDateRangeForTaskData(
|
||||
BuildContext context,
|
||||
DailyTaskController controller,
|
||||
|
@ -19,7 +19,9 @@ class DailyTaskPlanningController extends GetxController {
|
||||
List<Map<String, dynamic>> roles = [];
|
||||
RxBool isAssigningTask = false.obs;
|
||||
RxnString selectedRoleId = RxnString();
|
||||
RxBool isLoading = false.obs;
|
||||
RxBool isFetchingTasks = true.obs;
|
||||
RxBool isFetchingProjects = true.obs;
|
||||
RxBool isFetchingEmployees = true.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
@ -109,7 +111,7 @@ class DailyTaskPlanningController extends GetxController {
|
||||
}
|
||||
|
||||
Future<void> fetchProjects() async {
|
||||
isLoading.value = true;
|
||||
isFetchingProjects.value = true;
|
||||
try {
|
||||
final response = await ApiService.getProjects();
|
||||
if (response?.isEmpty ?? true) {
|
||||
@ -126,7 +128,7 @@ class DailyTaskPlanningController extends GetxController {
|
||||
logSafe("Error fetching projects",
|
||||
level: LogLevel.error, error: e, stackTrace: stack);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
isFetchingProjects.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -137,7 +139,7 @@ class DailyTaskPlanningController extends GetxController {
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading.value = true;
|
||||
isFetchingTasks.value = true;
|
||||
try {
|
||||
// Fetch infra details
|
||||
final infraResponse = await ApiService.getInfraDetails(projectId);
|
||||
@ -232,7 +234,7 @@ class DailyTaskPlanningController extends GetxController {
|
||||
logSafe("Error fetching daily task data",
|
||||
level: LogLevel.error, error: e, stackTrace: stack);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
isFetchingTasks.value = false;
|
||||
update();
|
||||
}
|
||||
}
|
||||
@ -244,7 +246,7 @@ class DailyTaskPlanningController extends GetxController {
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading.value = true;
|
||||
isFetchingEmployees.value = true;
|
||||
try {
|
||||
final response = await ApiService.getAllEmployeesByProject(projectId);
|
||||
if (response != null && response.isNotEmpty) {
|
||||
@ -272,7 +274,7 @@ class DailyTaskPlanningController extends GetxController {
|
||||
stackTrace: stack,
|
||||
);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
isFetchingEmployees.value = false;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
@ -41,6 +41,7 @@ class ApiEndpoints {
|
||||
static const String approveReportAction = "/task/approve";
|
||||
static const String assignTask = "/project/task";
|
||||
static const String getmasterWorkCategories = "/Master/work-categories";
|
||||
static const String getDailyTaskProjectProgressFilter = "/task/filter";
|
||||
|
||||
////// Directory Module API Endpoints ///////
|
||||
static const String getDirectoryContacts = "/directory";
|
||||
|
@ -21,6 +21,7 @@ import 'package:marco/model/document/document_version_model.dart';
|
||||
import 'package:marco/model/attendance/organization_per_project_list_model.dart';
|
||||
import 'package:marco/model/tenant/tenant_services_model.dart';
|
||||
import 'package:marco/model/dailyTaskPlanning/daily_task_model.dart';
|
||||
import 'package:marco/model/dailyTaskPlanning/daily_progress_report_filter_response_model.dart';
|
||||
|
||||
class ApiService {
|
||||
static const bool enableLogs = true;
|
||||
@ -2125,43 +2126,68 @@ class ApiService {
|
||||
}
|
||||
|
||||
// === Daily Task APIs ===
|
||||
/// Get Daily Task Project Report Filter
|
||||
static Future<DailyProgressReportFilterResponse?> getDailyTaskFilter(
|
||||
String projectId) async {
|
||||
final endpoint =
|
||||
"${ApiEndpoints.getDailyTaskProjectProgressFilter}/$projectId";
|
||||
logSafe("Fetching daily task Progress filter for projectId: $projectId");
|
||||
|
||||
try {
|
||||
final response = await _getRequest(endpoint);
|
||||
|
||||
if (response == null) {
|
||||
logSafe("Daily task filter request failed: null response",
|
||||
level: LogLevel.error);
|
||||
return null;
|
||||
}
|
||||
|
||||
final jsonResponse = _parseResponseForAllData(response,
|
||||
label: "Daily Task Progress Filter");
|
||||
|
||||
if (jsonResponse != null) {
|
||||
return DailyProgressReportFilterResponse.fromJson(jsonResponse);
|
||||
}
|
||||
} catch (e, stack) {
|
||||
logSafe("Exception during getDailyTask Progress Filter: $e",
|
||||
level: LogLevel.error);
|
||||
logSafe("StackTrace: $stack", level: LogLevel.debug);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
static Future<List<TaskModel>?> getDailyTasks(
|
||||
String projectId, {
|
||||
DateTime? dateFrom,
|
||||
DateTime? dateTo,
|
||||
List<String>? serviceIds,
|
||||
Map<String, dynamic>? filter, // <-- New: combined filter
|
||||
int pageNumber = 1,
|
||||
int pageSize = 20,
|
||||
}) async {
|
||||
final filterBody = {
|
||||
"serviceIds": serviceIds ?? [],
|
||||
};
|
||||
|
||||
// Build query parameters
|
||||
final query = {
|
||||
"projectId": projectId,
|
||||
"pageNumber": pageNumber.toString(),
|
||||
"pageSize": pageSize.toString(),
|
||||
if (dateFrom != null)
|
||||
"dateFrom": DateFormat('yyyy-MM-dd').format(dateFrom),
|
||||
if (dateTo != null) "dateTo": DateFormat('yyyy-MM-dd').format(dateTo),
|
||||
"filter": jsonEncode(filterBody),
|
||||
if (filter != null) "filter": jsonEncode(filter),
|
||||
};
|
||||
|
||||
final uri =
|
||||
Uri.parse(ApiEndpoints.getDailyTask).replace(queryParameters: query);
|
||||
|
||||
final response = await _getRequest(uri.toString());
|
||||
final parsed = response != null ? _parseResponse(response, label: 'Daily Tasks') : null;
|
||||
final parsed = response != null
|
||||
? _parseResponse(response, label: 'Daily Tasks')
|
||||
: null;
|
||||
|
||||
if (parsed != null && parsed['data'] != null) {
|
||||
return (parsed['data'] as List).map((e) => TaskModel.fromJson(e)).toList();
|
||||
return (parsed['data'] as List)
|
||||
.map((e) => TaskModel.fromJson(e))
|
||||
.toList();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
static Future<bool> reportTask({
|
||||
required String id,
|
||||
required int completedTask,
|
||||
|
@ -152,7 +152,7 @@ class _AssignTaskBottomSheetState extends State<AssignTaskBottomSheet> {
|
||||
|
||||
Widget _buildEmployeeList() {
|
||||
return Obx(() {
|
||||
if (controller.isLoading.value) {
|
||||
if (controller.isFetchingTasks.value) {
|
||||
// Skeleton loader instead of CircularProgressIndicator
|
||||
return ListView.separated(
|
||||
shrinkWrap: true,
|
||||
|
@ -1,73 +1,200 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:marco/controller/task_planning/daily_task_controller.dart';
|
||||
import 'package:marco/controller/permission_controller.dart';
|
||||
import 'package:marco/helpers/utils/base_bottom_sheet.dart';
|
||||
import 'package:marco/helpers/widgets/my_spacing.dart';
|
||||
import 'package:marco/helpers/widgets/my_text.dart';
|
||||
|
||||
class DailyProgressReportFilter extends StatelessWidget {
|
||||
class DailyTaskFilterBottomSheet extends StatelessWidget {
|
||||
final DailyTaskController controller;
|
||||
final PermissionController permissionController;
|
||||
|
||||
const DailyProgressReportFilter({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.permissionController,
|
||||
});
|
||||
|
||||
String getLabelText() {
|
||||
final startDate = controller.startDateTask;
|
||||
final endDate = controller.endDateTask;
|
||||
if (startDate != null && endDate != null) {
|
||||
final start = DateFormat('dd MM yyyy').format(startDate);
|
||||
final end = DateFormat('dd MM yyyy').format(endDate);
|
||||
return "$start - $end";
|
||||
}
|
||||
return "Select Date Range";
|
||||
}
|
||||
const DailyTaskFilterBottomSheet({super.key, required this.controller});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final filterData = controller.taskFilterData;
|
||||
if (filterData == null) return const SizedBox.shrink();
|
||||
|
||||
final hasFilters = [
|
||||
filterData.buildings,
|
||||
filterData.floors,
|
||||
filterData.activities,
|
||||
filterData.services,
|
||||
].any((list) => list.isNotEmpty);
|
||||
|
||||
return BaseBottomSheet(
|
||||
title: "Filter Tasks",
|
||||
onCancel: () => Navigator.pop(context),
|
||||
|
||||
submitText: "Apply",
|
||||
showButtons: hasFilters,
|
||||
onCancel: () => Get.back(),
|
||||
onSubmit: () {
|
||||
Navigator.pop(context, {
|
||||
'startDate': controller.startDateTask,
|
||||
'endDate': controller.endDateTask,
|
||||
});
|
||||
if (controller.selectedProjectId != null) {
|
||||
controller.fetchTaskData(
|
||||
controller.selectedProjectId!,
|
||||
);
|
||||
}
|
||||
|
||||
Get.back();
|
||||
},
|
||||
child: Column(
|
||||
child: SingleChildScrollView(
|
||||
child: hasFilters
|
||||
? Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.titleSmall("Select Date Range", fontWeight: 600),
|
||||
const SizedBox(height: 8),
|
||||
InkWell(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
onTap: () => controller.selectDateRangeForTaskData(
|
||||
context,
|
||||
controller,
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
controller.clearTaskFilters();
|
||||
},
|
||||
child: MyText(
|
||||
"Reset Filter",
|
||||
style: const TextStyle(
|
||||
color: Colors.red,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
child: Ink(
|
||||
),
|
||||
),
|
||||
),
|
||||
MySpacing.height(8),
|
||||
_multiSelectField(
|
||||
label: "Buildings",
|
||||
items: filterData.buildings,
|
||||
fallback: "Select Buildings",
|
||||
selectedValues: controller.selectedBuildings,
|
||||
),
|
||||
_multiSelectField(
|
||||
label: "Floors",
|
||||
items: filterData.floors,
|
||||
fallback: "Select Floors",
|
||||
selectedValues: controller.selectedFloors,
|
||||
),
|
||||
_multiSelectField(
|
||||
label: "Activities",
|
||||
items: filterData.activities,
|
||||
fallback: "Select Activities",
|
||||
selectedValues: controller.selectedActivities,
|
||||
),
|
||||
_multiSelectField(
|
||||
label: "Services",
|
||||
items: filterData.services,
|
||||
fallback: "Select Services",
|
||||
selectedValues: controller.selectedServices,
|
||||
),
|
||||
MySpacing.height(8),
|
||||
_dateRangeSelector(context),
|
||||
],
|
||||
)
|
||||
: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: MyText(
|
||||
"No filters available",
|
||||
style: const TextStyle(color: Colors.grey),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _multiSelectField({
|
||||
required String label,
|
||||
required List<dynamic> items,
|
||||
required String fallback,
|
||||
required RxSet<String> selectedValues,
|
||||
}) {
|
||||
if (items.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.labelMedium(label),
|
||||
MySpacing.height(8),
|
||||
Obx(() {
|
||||
final selectedNames = items
|
||||
.where((item) => selectedValues.contains(item.id))
|
||||
.map((item) => item.name)
|
||||
.join(", ");
|
||||
final displayText =
|
||||
selectedNames.isNotEmpty ? selectedNames : fallback;
|
||||
|
||||
return Builder(
|
||||
builder: (context) {
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
final RenderBox button =
|
||||
context.findRenderObject() as RenderBox;
|
||||
final RenderBox overlay = Overlay.of(context)
|
||||
.context
|
||||
.findRenderObject() as RenderBox;
|
||||
|
||||
final position = button.localToGlobal(Offset.zero);
|
||||
|
||||
await showMenu(
|
||||
context: context,
|
||||
position: RelativeRect.fromLTRB(
|
||||
position.dx,
|
||||
position.dy + button.size.height,
|
||||
overlay.size.width - position.dx - button.size.width,
|
||||
0,
|
||||
),
|
||||
items: items.map((item) {
|
||||
return PopupMenuItem<String>(
|
||||
enabled: false,
|
||||
child: StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
final isChecked = selectedValues.contains(item.id);
|
||||
return CheckboxListTile(
|
||||
dense: true,
|
||||
value: isChecked,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
title: MyText(item.name),
|
||||
|
||||
// --- Styles to match Document Filter ---
|
||||
checkColor: Colors.white,
|
||||
side: const BorderSide(
|
||||
color: Colors.black, width: 1.5),
|
||||
fillColor:
|
||||
MaterialStateProperty.resolveWith<Color>(
|
||||
(states) {
|
||||
if (states.contains(MaterialState.selected)) {
|
||||
return Colors.indigo;
|
||||
}
|
||||
return Colors.white;
|
||||
},
|
||||
),
|
||||
|
||||
onChanged: (val) {
|
||||
if (val == true) {
|
||||
selectedValues.add(item.id);
|
||||
} else {
|
||||
selectedValues.remove(item.id);
|
||||
}
|
||||
setState(() {});
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
padding: MySpacing.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
border: Border.all(color: Colors.grey.shade400),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Icon(Icons.date_range, color: Colors.blue.shade600),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
getLabelText(),
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.black87,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
child: MyText(
|
||||
displayText,
|
||||
style: const TextStyle(color: Colors.black87),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
@ -75,9 +202,89 @@ class DailyProgressReportFilter extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
MySpacing.height(16),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _dateRangeSelector(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.labelMedium("Select Date Range"),
|
||||
MySpacing.height(8),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _dateButton(
|
||||
label: controller.startDateTask != null
|
||||
? "${controller.startDateTask!.day}/${controller.startDateTask!.month}/${controller.startDateTask!.year}"
|
||||
: "From Date",
|
||||
onTap: () async {
|
||||
final picked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: controller.startDateTask ?? DateTime.now(),
|
||||
firstDate: DateTime(2022),
|
||||
lastDate: DateTime.now(),
|
||||
);
|
||||
if (picked != null) {
|
||||
controller.startDateTask = picked;
|
||||
controller.update(); // rebuild widget
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
MySpacing.width(12),
|
||||
Expanded(
|
||||
child: _dateButton(
|
||||
label: controller.endDateTask != null
|
||||
? "${controller.endDateTask!.day}/${controller.endDateTask!.month}/${controller.endDateTask!.year}"
|
||||
: "To Date",
|
||||
onTap: () async {
|
||||
final picked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: controller.endDateTask ?? DateTime.now(),
|
||||
firstDate: DateTime(2022),
|
||||
lastDate: DateTime.now(),
|
||||
);
|
||||
if (picked != null) {
|
||||
controller.endDateTask = picked;
|
||||
controller.update();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
MySpacing.height(16),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _dateButton({required String label, required VoidCallback onTap}) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.calendar_today, size: 16, color: Colors.grey),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: MyText(label),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,128 @@
|
||||
class DailyProgressReportFilterResponse {
|
||||
final bool success;
|
||||
final String message;
|
||||
final FilterData? data;
|
||||
final dynamic errors;
|
||||
final int statusCode;
|
||||
final String timestamp;
|
||||
|
||||
DailyProgressReportFilterResponse({
|
||||
required this.success,
|
||||
required this.message,
|
||||
this.data,
|
||||
this.errors,
|
||||
required this.statusCode,
|
||||
required this.timestamp,
|
||||
});
|
||||
|
||||
factory DailyProgressReportFilterResponse.fromJson(Map<String, dynamic> json) {
|
||||
return DailyProgressReportFilterResponse(
|
||||
success: json['success'],
|
||||
message: json['message'],
|
||||
data: json['data'] != null ? FilterData.fromJson(json['data']) : null,
|
||||
errors: json['errors'],
|
||||
statusCode: json['statusCode'],
|
||||
timestamp: json['timestamp'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'success': success,
|
||||
'message': message,
|
||||
'data': data?.toJson(),
|
||||
'errors': errors,
|
||||
'statusCode': statusCode,
|
||||
'timestamp': timestamp,
|
||||
};
|
||||
}
|
||||
|
||||
class FilterData {
|
||||
final List<Building> buildings;
|
||||
final List<Floor> floors;
|
||||
final List<Activity> activities;
|
||||
final List<Service> services;
|
||||
|
||||
FilterData({
|
||||
required this.buildings,
|
||||
required this.floors,
|
||||
required this.activities,
|
||||
required this.services,
|
||||
});
|
||||
|
||||
factory FilterData.fromJson(Map<String, dynamic> json) {
|
||||
return FilterData(
|
||||
buildings: (json['buildings'] as List)
|
||||
.map((e) => Building.fromJson(e))
|
||||
.toList(),
|
||||
floors:
|
||||
(json['floors'] as List).map((e) => Floor.fromJson(e)).toList(),
|
||||
activities:
|
||||
(json['activities'] as List).map((e) => Activity.fromJson(e)).toList(),
|
||||
services:
|
||||
(json['services'] as List).map((e) => Service.fromJson(e)).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'buildings': buildings.map((e) => e.toJson()).toList(),
|
||||
'floors': floors.map((e) => e.toJson()).toList(),
|
||||
'activities': activities.map((e) => e.toJson()).toList(),
|
||||
'services': services.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
class Building {
|
||||
final String id;
|
||||
final String name;
|
||||
|
||||
Building({required this.id, required this.name});
|
||||
|
||||
factory Building.fromJson(Map<String, dynamic> json) =>
|
||||
Building(id: json['id'], name: json['name']);
|
||||
|
||||
Map<String, dynamic> toJson() => {'id': id, 'name': name};
|
||||
}
|
||||
|
||||
class Floor {
|
||||
final String id;
|
||||
final String name;
|
||||
final String buildingId;
|
||||
|
||||
Floor({required this.id, required this.name, required this.buildingId});
|
||||
|
||||
factory Floor.fromJson(Map<String, dynamic> json) => Floor(
|
||||
id: json['id'],
|
||||
name: json['name'],
|
||||
buildingId: json['buildingId'],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'buildingId': buildingId,
|
||||
};
|
||||
}
|
||||
|
||||
class Activity {
|
||||
final String id;
|
||||
final String name;
|
||||
|
||||
Activity({required this.id, required this.name});
|
||||
|
||||
factory Activity.fromJson(Map<String, dynamic> json) =>
|
||||
Activity(id: json['id'], name: json['name']);
|
||||
|
||||
Map<String, dynamic> toJson() => {'id': id, 'name': name};
|
||||
}
|
||||
|
||||
class Service {
|
||||
final String id;
|
||||
final String name;
|
||||
|
||||
Service({required this.id, required this.name});
|
||||
|
||||
factory Service.fromJson(Map<String, dynamic> json) =>
|
||||
Service(id: json['id'], name: json['name']);
|
||||
|
||||
Map<String, dynamic> toJson() => {'id': id, 'name': name};
|
||||
}
|
@ -479,7 +479,7 @@ class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
|
||||
context: context,
|
||||
initialDate: _controller.joiningDate ?? DateTime.now(),
|
||||
firstDate: DateTime(2000),
|
||||
lastDate: DateTime(2100),
|
||||
lastDate: DateTime.now(),
|
||||
);
|
||||
|
||||
if (picked != null) {
|
||||
|
@ -12,7 +12,7 @@ import 'package:marco/view/error_pages/error_500_screen.dart';
|
||||
import 'package:marco/view/dashboard/dashboard_screen.dart';
|
||||
import 'package:marco/view/Attendence/attendance_screen.dart';
|
||||
import 'package:marco/view/taskPlanning/daily_task_planning.dart';
|
||||
import 'package:marco/view/taskPlanning/daily_progress.dart';
|
||||
import 'package:marco/view/taskPlanning/daily_progress_report.dart';
|
||||
import 'package:marco/view/employees/employees_screen.dart';
|
||||
import 'package:marco/view/auth/login_option_screen.dart';
|
||||
import 'package:marco/view/auth/mpin_screen.dart';
|
||||
|
@ -17,6 +17,7 @@ import 'package:marco/helpers/utils/date_time_utils.dart';
|
||||
import 'package:marco/model/directory/add_contact_bottom_sheet.dart';
|
||||
import 'package:marco/helpers/widgets/my_refresh_indicator.dart';
|
||||
import 'package:marco/helpers/widgets/my_confirmation_dialog.dart';
|
||||
import 'package:marco/model/directory/directory_comment_model.dart';
|
||||
|
||||
// HELPER: Delta to HTML conversion
|
||||
String _convertDeltaToHtml(dynamic delta) {
|
||||
@ -344,20 +345,7 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
||||
Widget _buildCommentsTab() {
|
||||
return Obx(() {
|
||||
final contactId = contactRx.value.id;
|
||||
|
||||
// Get active and inactive comments
|
||||
final activeComments = directoryController
|
||||
.getCommentsForContact(contactId)
|
||||
.where((c) => c.isActive)
|
||||
.toList();
|
||||
final inactiveComments = directoryController
|
||||
.getCommentsForContact(contactId)
|
||||
.where((c) => !c.isActive)
|
||||
.toList();
|
||||
|
||||
// Combine both and keep the same sorting (recent first)
|
||||
final comments =
|
||||
[...activeComments, ...inactiveComments].reversed.toList();
|
||||
final comments = directoryController.combinedComments(contactId);
|
||||
final editingId = directoryController.editingCommentId.value;
|
||||
|
||||
return Stack(
|
||||
@ -413,7 +401,8 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildCommentItem(comment, editingId, contactId) {
|
||||
Widget _buildCommentItem(
|
||||
DirectoryComment comment, String? editingId, String contactId) {
|
||||
final isEditing = editingId == comment.id;
|
||||
final initials = comment.createdBy.firstName.isNotEmpty
|
||||
? comment.createdBy.firstName[0].toUpperCase()
|
||||
@ -427,6 +416,8 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
||||
)
|
||||
: null;
|
||||
|
||||
final isInactive = !comment.isActive;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 6),
|
||||
padding: const EdgeInsets.all(12),
|
||||
@ -459,29 +450,32 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Full name on top
|
||||
Text(
|
||||
"${comment.createdBy.firstName} ${comment.createdBy.lastName}",
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 15,
|
||||
color: Colors.black87,
|
||||
color: isInactive ? Colors.grey : Colors.black87,
|
||||
fontStyle:
|
||||
isInactive ? FontStyle.italic : FontStyle.normal,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
// Job Role
|
||||
if (comment.createdBy.jobRoleName?.isNotEmpty ?? false)
|
||||
if (comment.createdBy.jobRoleName.isNotEmpty)
|
||||
Text(
|
||||
comment.createdBy.jobRoleName,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.indigo[600],
|
||||
color:
|
||||
isInactive ? Colors.grey : Colors.indigo[600],
|
||||
fontWeight: FontWeight.w500,
|
||||
fontStyle: isInactive
|
||||
? FontStyle.italic
|
||||
: FontStyle.normal,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
// Timestamp
|
||||
Text(
|
||||
DateTimeUtils.convertUtcToLocal(
|
||||
comment.createdAt.toString(),
|
||||
@ -490,6 +484,8 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
fontStyle:
|
||||
isInactive ? FontStyle.italic : FontStyle.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -497,33 +493,10 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
||||
),
|
||||
|
||||
// ⚡ Action buttons
|
||||
if (!isInactive)
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (!comment.isActive)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.restore,
|
||||
size: 18, color: Colors.green),
|
||||
tooltip: "Restore",
|
||||
splashRadius: 18,
|
||||
onPressed: () async {
|
||||
await Get.dialog(
|
||||
ConfirmDialog(
|
||||
title: "Restore Note",
|
||||
message:
|
||||
"Are you sure you want to restore this note?",
|
||||
confirmText: "Restore",
|
||||
confirmColor: Colors.green,
|
||||
icon: Icons.restore,
|
||||
onConfirm: () async {
|
||||
await directoryController.restoreComment(
|
||||
comment.id, contactId);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (comment.isActive) ...[
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit_outlined,
|
||||
size: 18, color: Colors.indigo),
|
||||
@ -557,7 +530,29 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
)
|
||||
else
|
||||
IconButton(
|
||||
icon: const Icon(Icons.restore,
|
||||
size: 18, color: Colors.green),
|
||||
tooltip: "Restore",
|
||||
splashRadius: 18,
|
||||
onPressed: () async {
|
||||
await Get.dialog(
|
||||
ConfirmDialog(
|
||||
title: "Restore Note",
|
||||
message:
|
||||
"Are you sure you want to restore this note?",
|
||||
confirmText: "Restore",
|
||||
confirmColor: Colors.green,
|
||||
icon: Icons.restore,
|
||||
onConfirm: () async {
|
||||
await directoryController.restoreComment(
|
||||
comment.id, contactId);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -568,13 +563,17 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
||||
if (isEditing && quillController != null)
|
||||
CommentEditorCard(
|
||||
controller: quillController,
|
||||
onCancel: () => directoryController.editingCommentId.value = null,
|
||||
onCancel: () =>
|
||||
directoryController.editingCommentId.value = null,
|
||||
onSave: (ctrl) async {
|
||||
final delta = ctrl.document.toDelta();
|
||||
final htmlOutput = _convertDeltaToHtml(delta);
|
||||
final updated = comment.copyWith(note: htmlOutput);
|
||||
await directoryController.updateComment(updated);
|
||||
await directoryController.fetchCommentsForContact(contactId);
|
||||
await directoryController.fetchCommentsForContact(contactId,
|
||||
active: true);
|
||||
await directoryController.fetchCommentsForContact(contactId,
|
||||
active: false);
|
||||
directoryController.editingCommentId.value = null;
|
||||
},
|
||||
)
|
||||
@ -586,7 +585,8 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
||||
margin: html.Margins.zero,
|
||||
padding: html.HtmlPaddings.zero,
|
||||
fontSize: html.FontSize(14),
|
||||
color: Colors.black87,
|
||||
color: isInactive ? Colors.grey : Colors.black87,
|
||||
fontStyle: isInactive ? FontStyle.italic : FontStyle.normal,
|
||||
),
|
||||
"p": html.Style(
|
||||
margin: html.Margins.only(bottom: 6),
|
||||
@ -594,13 +594,12 @@ class _ContactDetailScreenState extends State<ContactDetailScreen> {
|
||||
),
|
||||
"strong": html.Style(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.black87,
|
||||
color: isInactive ? Colors.grey : Colors.black87,
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
));
|
||||
}
|
||||
|
||||
Widget _iconInfoRow(
|
||||
|
@ -1,572 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:marco/helpers/theme/app_theme.dart';
|
||||
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
|
||||
import 'package:marco/helpers/utils/my_shadow.dart';
|
||||
import 'package:marco/helpers/widgets/my_card.dart';
|
||||
import 'package:marco/helpers/widgets/my_container.dart';
|
||||
import 'package:marco/helpers/widgets/my_spacing.dart';
|
||||
import 'package:marco/helpers/widgets/my_text.dart';
|
||||
import 'package:marco/controller/permission_controller.dart';
|
||||
import 'package:marco/controller/task_planning/daily_task_controller.dart';
|
||||
import 'package:marco/model/dailyTaskPlanning/daily_progress_report_filter.dart';
|
||||
import 'package:marco/helpers/widgets/avatar.dart';
|
||||
import 'package:marco/controller/project_controller.dart';
|
||||
import 'package:marco/model/dailyTaskPlanning/task_action_buttons.dart';
|
||||
import 'package:marco/helpers/widgets/my_custom_skeleton.dart';
|
||||
import 'package:marco/helpers/utils/permission_constants.dart';
|
||||
import 'package:marco/helpers/widgets/my_refresh_indicator.dart';
|
||||
import 'package:marco/controller/tenant/service_controller.dart';
|
||||
import 'package:marco/helpers/widgets/tenant/service_selector.dart';
|
||||
|
||||
class DailyProgressReportScreen extends StatefulWidget {
|
||||
const DailyProgressReportScreen({super.key});
|
||||
|
||||
@override
|
||||
State<DailyProgressReportScreen> createState() =>
|
||||
_DailyProgressReportScreenState();
|
||||
}
|
||||
|
||||
class TaskChartData {
|
||||
final String label;
|
||||
final num value;
|
||||
final Color color;
|
||||
|
||||
TaskChartData(this.label, this.value, this.color);
|
||||
}
|
||||
|
||||
class _DailyProgressReportScreenState extends State<DailyProgressReportScreen>
|
||||
with UIMixin {
|
||||
final DailyTaskController dailyTaskController =
|
||||
Get.put(DailyTaskController());
|
||||
final PermissionController permissionController =
|
||||
Get.put(PermissionController());
|
||||
final ProjectController projectController = Get.find<ProjectController>();
|
||||
final ServiceController serviceController = Get.put(ServiceController());
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_scrollController.addListener(() {
|
||||
if (_scrollController.position.pixels >=
|
||||
_scrollController.position.maxScrollExtent - 100 &&
|
||||
dailyTaskController.hasMore &&
|
||||
!dailyTaskController.isLoadingMore.value) {
|
||||
final projectId = dailyTaskController.selectedProjectId;
|
||||
if (projectId != null && projectId.isNotEmpty) {
|
||||
dailyTaskController.fetchTaskData(
|
||||
projectId,
|
||||
pageNumber: dailyTaskController.currentPage + 1,
|
||||
pageSize: dailyTaskController.pageSize,
|
||||
isLoadMore: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
final initialProjectId = projectController.selectedProjectId.value;
|
||||
if (initialProjectId.isNotEmpty) {
|
||||
dailyTaskController.selectedProjectId = initialProjectId;
|
||||
dailyTaskController.fetchTaskData(initialProjectId);
|
||||
serviceController.fetchServices(initialProjectId);
|
||||
}
|
||||
|
||||
// Update when project changes
|
||||
ever<String>(projectController.selectedProjectId, (newProjectId) async {
|
||||
if (newProjectId.isNotEmpty &&
|
||||
newProjectId != dailyTaskController.selectedProjectId) {
|
||||
dailyTaskController.selectedProjectId = newProjectId;
|
||||
await dailyTaskController.fetchTaskData(newProjectId);
|
||||
await serviceController.fetchServices(newProjectId);
|
||||
dailyTaskController.update(['daily_progress_report_controller']);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(72),
|
||||
child: AppBar(
|
||||
backgroundColor: const Color(0xFFF5F5F5),
|
||||
elevation: 0.5,
|
||||
automaticallyImplyLeading: false,
|
||||
titleSpacing: 0,
|
||||
title: Padding(
|
||||
padding: MySpacing.xy(16, 0),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios_new,
|
||||
color: Colors.black, size: 20),
|
||||
onPressed: () => Get.offNamed('/dashboard'),
|
||||
),
|
||||
MySpacing.width(8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
MyText.titleLarge(
|
||||
'Daily Progress Report',
|
||||
fontWeight: 700,
|
||||
color: Colors.black,
|
||||
),
|
||||
MySpacing.height(2),
|
||||
GetBuilder<ProjectController>(
|
||||
builder: (projectController) {
|
||||
final projectName =
|
||||
projectController.selectedProject?.name ??
|
||||
'Select Project';
|
||||
return Row(
|
||||
children: [
|
||||
const Icon(Icons.work_outline,
|
||||
size: 14, color: Colors.grey),
|
||||
MySpacing.width(4),
|
||||
Expanded(
|
||||
child: MyText.bodySmall(
|
||||
projectName,
|
||||
fontWeight: 600,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: MyRefreshIndicator(
|
||||
onRefresh: _refreshData,
|
||||
child: CustomScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: GetBuilder<DailyTaskController>(
|
||||
init: dailyTaskController,
|
||||
tag: 'daily_progress_report_controller',
|
||||
builder: (controller) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MySpacing.height(flexSpacing),
|
||||
|
||||
// --- ADD SERVICE SELECTOR HERE ---
|
||||
Padding(
|
||||
padding: MySpacing.x(10),
|
||||
child: ServiceSelector(
|
||||
controller: serviceController,
|
||||
height: 40,
|
||||
onSelectionChanged: (service) async {
|
||||
final projectId =
|
||||
dailyTaskController.selectedProjectId;
|
||||
if (projectId?.isNotEmpty ?? false) {
|
||||
await dailyTaskController.fetchTaskData(
|
||||
projectId!,
|
||||
serviceIds:
|
||||
service != null ? [service.id] : null,
|
||||
pageNumber: 1,
|
||||
pageSize: 20,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
_buildActionBar(),
|
||||
Padding(
|
||||
padding: MySpacing.x(8),
|
||||
child: _buildDailyProgressReportTab(),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionBar() {
|
||||
return Padding(
|
||||
padding: MySpacing.x(flexSpacing),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
_buildActionItem(
|
||||
label: "Filter",
|
||||
icon: Icons.tune,
|
||||
tooltip: 'Filter Project',
|
||||
onTap: _openFilterSheet,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionItem({
|
||||
required String label,
|
||||
required IconData icon,
|
||||
required String tooltip,
|
||||
required VoidCallback onTap,
|
||||
Color? color,
|
||||
}) {
|
||||
return Row(
|
||||
children: [
|
||||
MyText.bodyMedium(label, fontWeight: 600),
|
||||
Tooltip(
|
||||
message: tooltip,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(22),
|
||||
onTap: onTap,
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Icon(icon, color: color, size: 22),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _openFilterSheet() async {
|
||||
final result = await showModalBottomSheet<Map<String, dynamic>>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => DailyProgressReportFilter(
|
||||
controller: dailyTaskController,
|
||||
permissionController: permissionController,
|
||||
),
|
||||
);
|
||||
|
||||
if (result != null) {
|
||||
final selectedProjectId = result['projectId'] as String?;
|
||||
if (selectedProjectId != null &&
|
||||
selectedProjectId != dailyTaskController.selectedProjectId) {
|
||||
dailyTaskController.selectedProjectId = selectedProjectId;
|
||||
await dailyTaskController.fetchTaskData(selectedProjectId);
|
||||
dailyTaskController.update(['daily_progress_report_controller']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _refreshData() async {
|
||||
final projectId = dailyTaskController.selectedProjectId;
|
||||
if (projectId != null) {
|
||||
try {
|
||||
await dailyTaskController.fetchTaskData(projectId);
|
||||
} catch (e) {
|
||||
debugPrint('Error refreshing task data: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _showTeamMembersBottomSheet(List<dynamic> members) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
isDismissible: true,
|
||||
enableDrag: true,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
|
||||
),
|
||||
builder: (context) {
|
||||
return GestureDetector(
|
||||
onTap: () {},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius:
|
||||
const BorderRadius.vertical(top: Radius.circular(12)),
|
||||
),
|
||||
padding: const EdgeInsets.fromLTRB(16, 24, 16, 24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.titleMedium(
|
||||
'Team Members',
|
||||
fontWeight: 600,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Divider(thickness: 1),
|
||||
const SizedBox(height: 8),
|
||||
...members.map((member) {
|
||||
final firstName = member.firstName ?? 'Unnamed';
|
||||
final lastName = member.lastName ?? 'User';
|
||||
return ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: Avatar(
|
||||
firstName: firstName,
|
||||
lastName: lastName,
|
||||
size: 31,
|
||||
),
|
||||
title: MyText.bodyMedium(
|
||||
'$firstName $lastName',
|
||||
fontWeight: 600,
|
||||
),
|
||||
);
|
||||
}),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDailyProgressReportTab() {
|
||||
return Obx(() {
|
||||
final isLoading = dailyTaskController.isLoading.value;
|
||||
final groupedTasks = dailyTaskController.groupedDailyTasks;
|
||||
|
||||
// Initial loading skeleton
|
||||
if (isLoading && dailyTaskController.currentPage == 1) {
|
||||
return SkeletonLoaders.dailyProgressReportSkeletonLoader();
|
||||
}
|
||||
|
||||
// No tasks
|
||||
if (groupedTasks.isEmpty) {
|
||||
return Center(
|
||||
child: MyText.bodySmall(
|
||||
"No Progress Report Found",
|
||||
fontWeight: 600,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final sortedDates = groupedTasks.keys.toList()
|
||||
..sort((a, b) => b.compareTo(a));
|
||||
|
||||
// If only one date, make it expanded by default
|
||||
if (sortedDates.length == 1 &&
|
||||
!dailyTaskController.expandedDates.contains(sortedDates[0])) {
|
||||
dailyTaskController.expandedDates.add(sortedDates[0]);
|
||||
}
|
||||
|
||||
return MyCard.bordered(
|
||||
borderRadiusAll: 10,
|
||||
border: Border.all(color: Colors.grey.withOpacity(0.2)),
|
||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||
paddingAll: 8,
|
||||
child: ListView.builder(
|
||||
controller: _scrollController,
|
||||
shrinkWrap: true,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
itemCount: sortedDates.length + 1, // +1 for loading indicator
|
||||
itemBuilder: (context, dateIndex) {
|
||||
// Bottom loading indicator
|
||||
if (dateIndex == sortedDates.length) {
|
||||
return Obx(() => dailyTaskController.isLoadingMore.value
|
||||
? const Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
)
|
||||
: const SizedBox.shrink());
|
||||
}
|
||||
|
||||
final dateKey = sortedDates[dateIndex];
|
||||
final tasksForDate = groupedTasks[dateKey]!;
|
||||
final date = DateTime.tryParse(dateKey);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () => dailyTaskController.toggleDate(dateKey),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
MyText.bodyMedium(
|
||||
date != null
|
||||
? DateFormat('dd MMM yyyy').format(date)
|
||||
: dateKey,
|
||||
fontWeight: 700,
|
||||
),
|
||||
Obx(() => Icon(
|
||||
dailyTaskController.expandedDates.contains(dateKey)
|
||||
? Icons.remove_circle
|
||||
: Icons.add_circle,
|
||||
color: Colors.blueAccent,
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
Obx(() {
|
||||
if (!dailyTaskController.expandedDates.contains(dateKey)) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: tasksForDate.asMap().entries.map((entry) {
|
||||
final task = entry.value;
|
||||
|
||||
final activityName =
|
||||
task.workItem?.activityMaster?.activityName ?? 'N/A';
|
||||
final activityId = task.workItem?.activityMaster?.id;
|
||||
final workAreaId = task.workItem?.workArea?.id;
|
||||
final location = [
|
||||
task.workItem?.workArea?.floor?.building?.name,
|
||||
task.workItem?.workArea?.floor?.floorName,
|
||||
task.workItem?.workArea?.areaName
|
||||
].where((e) => e?.isNotEmpty ?? false).join(' > ');
|
||||
|
||||
final planned = task.plannedTask;
|
||||
final completed = task.completedTask;
|
||||
final progress = (planned != 0)
|
||||
? (completed / planned).clamp(0.0, 1.0)
|
||||
: 0.0;
|
||||
final parentTaskID = task.id;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: MyContainer(
|
||||
paddingAll: 12,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.bodyMedium(activityName, fontWeight: 600),
|
||||
const SizedBox(height: 2),
|
||||
MyText.bodySmall(location, color: Colors.grey),
|
||||
const SizedBox(height: 8),
|
||||
GestureDetector(
|
||||
onTap: () => _showTeamMembersBottomSheet(
|
||||
task.teamMembers),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.group,
|
||||
size: 18, color: Colors.blueAccent),
|
||||
const SizedBox(width: 6),
|
||||
MyText.bodyMedium('Team',
|
||||
color: Colors.blueAccent,
|
||||
fontWeight: 600),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
MyText.bodySmall(
|
||||
"Completed: $completed / $planned",
|
||||
fontWeight: 600,
|
||||
color: Colors.black87,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Stack(
|
||||
children: [
|
||||
Container(
|
||||
height: 5,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[300],
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
),
|
||||
FractionallySizedBox(
|
||||
widthFactor: progress,
|
||||
child: Container(
|
||||
height: 5,
|
||||
decoration: BoxDecoration(
|
||||
color: progress >= 1.0
|
||||
? Colors.green
|
||||
: progress >= 0.5
|
||||
? Colors.amber
|
||||
: Colors.red,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
MyText.bodySmall(
|
||||
"${(progress * 100).toStringAsFixed(1)}%",
|
||||
fontWeight: 500,
|
||||
color: progress >= 1.0
|
||||
? Colors.green[700]
|
||||
: progress >= 0.5
|
||||
? Colors.amber[800]
|
||||
: Colors.red[700],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
if ((task.reportedDate == null ||
|
||||
task.reportedDate
|
||||
.toString()
|
||||
.isEmpty) &&
|
||||
permissionController.hasPermission(
|
||||
Permissions.assignReportTask)) ...[
|
||||
TaskActionButtons.reportButton(
|
||||
context: context,
|
||||
task: task,
|
||||
completed: completed.toInt(),
|
||||
refreshCallback: _refreshData,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
] else if (task.approvedBy == null &&
|
||||
permissionController.hasPermission(
|
||||
Permissions.approveTask)) ...[
|
||||
TaskActionButtons.reportActionButton(
|
||||
context: context,
|
||||
task: task,
|
||||
parentTaskID: parentTaskID,
|
||||
workAreaId: workAreaId.toString(),
|
||||
activityId: activityId.toString(),
|
||||
completed: completed.toInt(),
|
||||
refreshCallback: _refreshData,
|
||||
),
|
||||
const SizedBox(width: 5),
|
||||
],
|
||||
TaskActionButtons.commentButton(
|
||||
context: context,
|
||||
task: task,
|
||||
parentTaskID: parentTaskID,
|
||||
workAreaId: workAreaId.toString(),
|
||||
activityId: activityId.toString(),
|
||||
refreshCallback: _refreshData,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
})
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
562
lib/view/taskPlanning/daily_progress_report.dart
Normal file
562
lib/view/taskPlanning/daily_progress_report.dart
Normal file
@ -0,0 +1,562 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:marco/helpers/theme/app_theme.dart';
|
||||
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
|
||||
import 'package:marco/helpers/utils/my_shadow.dart';
|
||||
import 'package:marco/helpers/widgets/my_card.dart';
|
||||
import 'package:marco/helpers/widgets/my_container.dart';
|
||||
import 'package:marco/helpers/widgets/my_spacing.dart';
|
||||
import 'package:marco/helpers/widgets/my_text.dart';
|
||||
import 'package:marco/controller/permission_controller.dart';
|
||||
import 'package:marco/controller/task_planning/daily_task_controller.dart';
|
||||
import 'package:marco/model/dailyTaskPlanning/daily_progress_report_filter.dart';
|
||||
import 'package:marco/helpers/widgets/avatar.dart';
|
||||
import 'package:marco/controller/project_controller.dart';
|
||||
import 'package:marco/model/dailyTaskPlanning/task_action_buttons.dart';
|
||||
import 'package:marco/helpers/widgets/my_custom_skeleton.dart';
|
||||
import 'package:marco/helpers/utils/permission_constants.dart';
|
||||
import 'package:marco/helpers/widgets/my_refresh_indicator.dart';
|
||||
|
||||
class DailyProgressReportScreen extends StatefulWidget {
|
||||
const DailyProgressReportScreen({super.key});
|
||||
|
||||
@override
|
||||
State<DailyProgressReportScreen> createState() =>
|
||||
_DailyProgressReportScreenState();
|
||||
}
|
||||
|
||||
class TaskChartData {
|
||||
final String label;
|
||||
final num value;
|
||||
final Color color;
|
||||
|
||||
TaskChartData(this.label, this.value, this.color);
|
||||
}
|
||||
|
||||
class _DailyProgressReportScreenState extends State<DailyProgressReportScreen>
|
||||
with UIMixin {
|
||||
final DailyTaskController dailyTaskController =
|
||||
Get.put(DailyTaskController());
|
||||
final PermissionController permissionController =
|
||||
Get.put(PermissionController());
|
||||
final ProjectController projectController = Get.find<ProjectController>();
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_scrollController.addListener(() {
|
||||
if (_scrollController.position.pixels >=
|
||||
_scrollController.position.maxScrollExtent - 100 &&
|
||||
dailyTaskController.hasMore &&
|
||||
!dailyTaskController.isLoadingMore.value) {
|
||||
final projectId = dailyTaskController.selectedProjectId;
|
||||
if (projectId != null && projectId.isNotEmpty) {
|
||||
dailyTaskController.fetchTaskData(
|
||||
projectId,
|
||||
pageNumber: dailyTaskController.currentPage + 1,
|
||||
pageSize: dailyTaskController.pageSize,
|
||||
isLoadMore: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
final initialProjectId = projectController.selectedProjectId.value;
|
||||
if (initialProjectId.isNotEmpty) {
|
||||
dailyTaskController.selectedProjectId = initialProjectId;
|
||||
dailyTaskController.fetchTaskData(initialProjectId);
|
||||
}
|
||||
|
||||
// Update when project changes
|
||||
ever<String>(projectController.selectedProjectId, (newProjectId) async {
|
||||
if (newProjectId.isNotEmpty &&
|
||||
newProjectId != dailyTaskController.selectedProjectId) {
|
||||
dailyTaskController.selectedProjectId = newProjectId;
|
||||
await dailyTaskController.fetchTaskData(newProjectId);
|
||||
dailyTaskController.update(['daily_progress_report_controller']);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(72),
|
||||
child: AppBar(
|
||||
backgroundColor: const Color(0xFFF5F5F5),
|
||||
elevation: 0.5,
|
||||
automaticallyImplyLeading: false,
|
||||
titleSpacing: 0,
|
||||
title: Padding(
|
||||
padding: MySpacing.xy(16, 0),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios_new,
|
||||
color: Colors.black, size: 20),
|
||||
onPressed: () => Get.offNamed('/dashboard'),
|
||||
),
|
||||
MySpacing.width(8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
MyText.titleLarge(
|
||||
'Daily Progress Report',
|
||||
fontWeight: 700,
|
||||
color: Colors.black,
|
||||
),
|
||||
MySpacing.height(2),
|
||||
GetBuilder<ProjectController>(
|
||||
builder: (projectController) {
|
||||
final projectName =
|
||||
projectController.selectedProject?.name ??
|
||||
'Select Project';
|
||||
return Row(
|
||||
children: [
|
||||
const Icon(Icons.work_outline,
|
||||
size: 14, color: Colors.grey),
|
||||
MySpacing.width(4),
|
||||
Expanded(
|
||||
child: MyText.bodySmall(
|
||||
projectName,
|
||||
fontWeight: 600,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: MyRefreshIndicator(
|
||||
onRefresh: _refreshData,
|
||||
child: CustomScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: GetBuilder<DailyTaskController>(
|
||||
init: dailyTaskController,
|
||||
tag: 'daily_progress_report_controller',
|
||||
builder: (controller) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MySpacing.height(flexSpacing),
|
||||
Padding(
|
||||
padding: MySpacing.x(15),
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.end,
|
||||
children: [
|
||||
InkWell(
|
||||
borderRadius: BorderRadius.circular(22),
|
||||
onTap: _openFilterSheet,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8, vertical: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
MyText.bodySmall(
|
||||
"Filter",
|
||||
fontWeight: 600,
|
||||
color: Colors.black,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Icon(Icons.tune,
|
||||
size: 20, color: Colors.black),
|
||||
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
MySpacing.height(8),
|
||||
Padding(
|
||||
padding: MySpacing.x(8),
|
||||
child: _buildDailyProgressReportTab(),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _openFilterSheet() async {
|
||||
// ✅ Fetch filter data first
|
||||
if (dailyTaskController.taskFilterData == null) {
|
||||
await dailyTaskController
|
||||
.fetchTaskFilter(dailyTaskController.selectedProjectId ?? '');
|
||||
}
|
||||
|
||||
final result = await showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => DailyTaskFilterBottomSheet(
|
||||
controller: dailyTaskController,
|
||||
),
|
||||
);
|
||||
|
||||
if (result != null) {
|
||||
final selectedProjectId = result['projectId'] as String?;
|
||||
if (selectedProjectId != null &&
|
||||
selectedProjectId != dailyTaskController.selectedProjectId) {
|
||||
dailyTaskController.selectedProjectId = selectedProjectId;
|
||||
await dailyTaskController.fetchTaskData(selectedProjectId);
|
||||
dailyTaskController.update(['daily_progress_report_controller']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _refreshData() async {
|
||||
final projectId = dailyTaskController.selectedProjectId;
|
||||
if (projectId != null) {
|
||||
try {
|
||||
await dailyTaskController.fetchTaskData(projectId);
|
||||
} catch (e) {
|
||||
debugPrint('Error refreshing task data: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _showTeamMembersBottomSheet(List<dynamic> members) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
isDismissible: true,
|
||||
enableDrag: true,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
|
||||
),
|
||||
builder: (context) {
|
||||
return GestureDetector(
|
||||
onTap: () {},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius:
|
||||
const BorderRadius.vertical(top: Radius.circular(12)),
|
||||
),
|
||||
padding: const EdgeInsets.fromLTRB(16, 24, 16, 24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MyText.titleMedium(
|
||||
'Team Members',
|
||||
fontWeight: 600,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Divider(thickness: 1),
|
||||
const SizedBox(height: 8),
|
||||
...members.map((member) {
|
||||
final firstName = member.firstName ?? 'Unnamed';
|
||||
final lastName = member.lastName ?? 'User';
|
||||
return ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: Avatar(
|
||||
firstName: firstName,
|
||||
lastName: lastName,
|
||||
size: 31,
|
||||
),
|
||||
title: MyText.bodyMedium(
|
||||
'$firstName $lastName',
|
||||
fontWeight: 600,
|
||||
),
|
||||
);
|
||||
}),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDailyProgressReportTab() {
|
||||
return Obx(() {
|
||||
final isLoading = dailyTaskController.isLoading.value;
|
||||
final groupedTasks = dailyTaskController.groupedDailyTasks;
|
||||
|
||||
// 🟡 Show loading skeleton on first load
|
||||
if (isLoading && dailyTaskController.currentPage == 1) {
|
||||
return SkeletonLoaders.dailyProgressReportSkeletonLoader();
|
||||
}
|
||||
|
||||
// ⚪ No data available
|
||||
if (groupedTasks.isEmpty) {
|
||||
return Center(
|
||||
child: MyText.bodySmall(
|
||||
"No Progress Report Found",
|
||||
fontWeight: 600,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 🔽 Sort all date keys by descending (latest first)
|
||||
final sortedDates = groupedTasks.keys.toList()
|
||||
..sort((a, b) => b.compareTo(a));
|
||||
|
||||
// 🔹 Auto expand if only one date present
|
||||
if (sortedDates.length == 1 &&
|
||||
!dailyTaskController.expandedDates.contains(sortedDates[0])) {
|
||||
dailyTaskController.expandedDates.add(sortedDates[0]);
|
||||
}
|
||||
|
||||
// 🧱 Return a scrollable column of cards
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
...sortedDates.map((dateKey) {
|
||||
final tasksForDate = groupedTasks[dateKey]!;
|
||||
final date = DateTime.tryParse(dateKey);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: MyCard.bordered(
|
||||
borderRadiusAll: 10,
|
||||
border: Border.all(color: Colors.grey.withOpacity(0.2)),
|
||||
shadow:
|
||||
MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
||||
paddingAll: 12,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 🗓️ Date Header
|
||||
GestureDetector(
|
||||
onTap: () => dailyTaskController.toggleDate(dateKey),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
MyText.bodyMedium(
|
||||
date != null
|
||||
? DateFormat('dd MMM yyyy').format(date)
|
||||
: dateKey,
|
||||
fontWeight: 700,
|
||||
),
|
||||
Obx(() => Icon(
|
||||
dailyTaskController.expandedDates
|
||||
.contains(dateKey)
|
||||
? Icons.remove_circle
|
||||
: Icons.add_circle,
|
||||
color: Colors.blueAccent,
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 🔽 Task List (expandable)
|
||||
Obx(() {
|
||||
if (!dailyTaskController.expandedDates
|
||||
.contains(dateKey)) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: tasksForDate.map((task) {
|
||||
final activityName =
|
||||
task.workItem?.activityMaster?.activityName ??
|
||||
'N/A';
|
||||
final activityId = task.workItem?.activityMaster?.id;
|
||||
final workAreaId = task.workItem?.workArea?.id;
|
||||
final location = [
|
||||
task.workItem?.workArea?.floor?.building?.name,
|
||||
task.workItem?.workArea?.floor?.floorName,
|
||||
task.workItem?.workArea?.areaName
|
||||
].where((e) => e?.isNotEmpty ?? false).join(' > ');
|
||||
|
||||
final planned = task.plannedTask;
|
||||
final completed = task.completedTask;
|
||||
final progress = (planned != 0)
|
||||
? (completed / planned).clamp(0.0, 1.0)
|
||||
: 0.0;
|
||||
final parentTaskID = task.id;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10),
|
||||
child: MyContainer(
|
||||
paddingAll: 12,
|
||||
borderRadiusAll: 8,
|
||||
border: Border.all(
|
||||
color: Colors.grey.withOpacity(0.2)),
|
||||
color: Colors.white,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 🏗️ Activity name & location
|
||||
MyText.bodyMedium(activityName,
|
||||
fontWeight: 600),
|
||||
const SizedBox(height: 2),
|
||||
MyText.bodySmall(location,
|
||||
color: Colors.grey),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// 👥 Team Members
|
||||
GestureDetector(
|
||||
onTap: () => _showTeamMembersBottomSheet(
|
||||
task.teamMembers),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.group,
|
||||
size: 18, color: Colors.blueAccent),
|
||||
const SizedBox(width: 6),
|
||||
MyText.bodyMedium(
|
||||
'Team',
|
||||
color: Colors.blueAccent,
|
||||
fontWeight: 600,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// 📊 Progress info
|
||||
MyText.bodySmall(
|
||||
"Completed: $completed / $planned",
|
||||
fontWeight: 600,
|
||||
color: Colors.black87,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Stack(
|
||||
children: [
|
||||
Container(
|
||||
height: 5,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[300],
|
||||
borderRadius:
|
||||
BorderRadius.circular(6),
|
||||
),
|
||||
),
|
||||
FractionallySizedBox(
|
||||
widthFactor: progress,
|
||||
child: Container(
|
||||
height: 5,
|
||||
decoration: BoxDecoration(
|
||||
color: progress >= 1.0
|
||||
? Colors.green
|
||||
: progress >= 0.5
|
||||
? Colors.amber
|
||||
: Colors.red,
|
||||
borderRadius:
|
||||
BorderRadius.circular(6),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
MyText.bodySmall(
|
||||
"${(progress * 100).toStringAsFixed(1)}%",
|
||||
fontWeight: 500,
|
||||
color: progress >= 1.0
|
||||
? Colors.green[700]
|
||||
: progress >= 0.5
|
||||
? Colors.amber[800]
|
||||
: Colors.red[700],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// 🎯 Action Buttons
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
physics: const ClampingScrollPhysics(),
|
||||
primary: false,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
if ((task.reportedDate == null ||
|
||||
task.reportedDate
|
||||
.toString()
|
||||
.isEmpty) &&
|
||||
permissionController.hasPermission(
|
||||
Permissions
|
||||
.assignReportTask)) ...[
|
||||
TaskActionButtons.reportButton(
|
||||
context: context,
|
||||
task: task,
|
||||
completed: completed.toInt(),
|
||||
refreshCallback: _refreshData,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
] else if (task.approvedBy == null &&
|
||||
permissionController.hasPermission(
|
||||
Permissions.approveTask)) ...[
|
||||
TaskActionButtons.reportActionButton(
|
||||
context: context,
|
||||
task: task,
|
||||
parentTaskID: parentTaskID,
|
||||
workAreaId: workAreaId.toString(),
|
||||
activityId: activityId.toString(),
|
||||
completed: completed.toInt(),
|
||||
refreshCallback: _refreshData,
|
||||
),
|
||||
const SizedBox(width: 5),
|
||||
],
|
||||
TaskActionButtons.commentButton(
|
||||
context: context,
|
||||
task: task,
|
||||
parentTaskID: parentTaskID,
|
||||
workAreaId: workAreaId.toString(),
|
||||
activityId: activityId.toString(),
|
||||
refreshCallback: _refreshData,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
|
||||
// 🔻 Loading More Indicator
|
||||
Obx(() => dailyTaskController.isLoadingMore.value
|
||||
? const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 16),
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
)
|
||||
: const SizedBox.shrink()),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
@ -48,8 +48,7 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
|
||||
(newProjectId) {
|
||||
if (newProjectId.isNotEmpty) {
|
||||
dailyTaskPlanningController.fetchTaskData(newProjectId);
|
||||
serviceController
|
||||
.fetchServices(newProjectId);
|
||||
serviceController.fetchServices(newProjectId);
|
||||
}
|
||||
},
|
||||
);
|
||||
@ -184,7 +183,7 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
|
||||
|
||||
Widget dailyProgressReportTab() {
|
||||
return Obx(() {
|
||||
final isLoading = dailyTaskPlanningController.isLoading.value;
|
||||
final isLoading = dailyTaskPlanningController.isFetchingTasks.value;
|
||||
final dailyTasks = dailyTaskPlanningController.dailyTasks;
|
||||
|
||||
if (isLoading) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user