diff --git a/lib/controller/dashboard/daily_task_controller.dart b/lib/controller/dashboard/daily_task_controller.dart index 5ea99fc..03ae337 100644 --- a/lib/controller/dashboard/daily_task_controller.dart +++ b/lib/controller/dashboard/daily_task_controller.dart @@ -50,7 +50,7 @@ class DailyTaskController extends GetxController { projects = response!.map((json) => ProjectModel.fromJson(json)).toList(); selectedProjectId = projects.first.id.toString(); log.i("Projects fetched: ${projects.length} projects loaded."); - + update(); await fetchTaskData(selectedProjectId); } diff --git a/lib/controller/task_planing/report_task_controller.dart b/lib/controller/task_planing/report_task_controller.dart index 0141bc5..379cd1d 100644 --- a/lib/controller/task_planing/report_task_controller.dart +++ b/lib/controller/task_planing/report_task_controller.dart @@ -7,11 +7,13 @@ import 'package:get/get.dart'; import 'package:logger/logger.dart'; final Logger logger = Logger(); - +enum ApiStatus { idle, loading, success, failure } class ReportTaskController extends MyController { List files = []; MyFormValidator basicValidator = MyFormValidator(); RxBool isLoading = false.obs; + Rx reportStatus = ApiStatus.idle.obs; + Rx commentStatus = ApiStatus.idle.obs; @override void onInit() { @@ -92,12 +94,17 @@ class ReportTaskController extends MyController { final completedWork = basicValidator.getController('completed_work')?.text.trim(); - final commentField = basicValidator.getController('comment')?.text.trim(); if (completedWork == null || completedWork.isEmpty) { Get.snackbar("Error", "Completed work is required."); return; } + final completedWorkInt = int.tryParse(completedWork); + if (completedWorkInt == null || completedWorkInt <= 0) { + Get.snackbar("Error", "Completed work must be a positive integer."); + return; + } + final commentField = basicValidator.getController('comment')?.text.trim(); if (commentField == null || commentField.isEmpty) { Get.snackbar("Error", "Comment is required."); @@ -126,7 +133,8 @@ class ReportTaskController extends MyController { isLoading.value = false; } } - Future commentTask({ + + Future commentTask({ required String projectId, required String comment, required int completedTask, @@ -158,13 +166,13 @@ class ReportTaskController extends MyController { ); if (success) { - Get.snackbar("Success", "Task reported successfully!"); + Get.snackbar("Success", "Task commented successfully!"); } else { - Get.snackbar("Error", "Failed to report task."); + Get.snackbar("Error", "Failed to comment task."); } } catch (e) { - logger.e("Error reporting task: $e"); - Get.snackbar("Error", "An error occurred while reporting the task."); + logger.e("Error commenting task: $e"); + Get.snackbar("Error", "An error occurred while commenting the task."); } finally { isLoading.value = false; } diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index ce74a51..714b345 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -6,7 +6,7 @@ import 'package:logger/logger.dart'; import 'package:marco/helpers/services/storage/local_storage.dart'; import 'package:marco/helpers/services/auth_service.dart'; import 'package:marco/helpers/services/api_endpoints.dart'; - +import 'package:get/get.dart'; final Logger logger = Logger(); class ApiService { @@ -332,6 +332,7 @@ class ApiService { final json = jsonDecode(response.body); if (response.statusCode == 200 && json['success'] == true) { + Get.back(); return true; } else { _log("Failed to report task: ${json['message'] ?? 'Unknown error'}"); @@ -358,9 +359,10 @@ class ApiService { final json = jsonDecode(response.body); if (response.statusCode == 200 && json['success'] == true) { + Get.back(); return true; } else { - _log("Failed to report task: ${json['message'] ?? 'Unknown error'}"); + _log("Failed to comment task: ${json['message'] ?? 'Unknown error'}"); return false; } } diff --git a/lib/helpers/widgets/my_team_model_sheet.dart b/lib/helpers/widgets/my_team_model_sheet.dart new file mode 100644 index 0000000..b3f7c3c --- /dev/null +++ b/lib/helpers/widgets/my_team_model_sheet.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:marco/helpers/widgets/avatar.dart'; +import 'package:marco/helpers/widgets/my_text.dart'; + +class TeamBottomSheet { + static void show({ + required BuildContext context, + required List teamMembers, + }) { + showModalBottomSheet( + context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(12)), + ), + backgroundColor: Colors.white, + builder: (_) => Padding( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Title and Close Icon + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + MyText.bodyLarge("Team Members", fontWeight: 600), + IconButton( + icon: const Icon(Icons.close, size: 20, color: Colors.black54), + onPressed: () => Navigator.pop(context), + ), + ], + ), + const Divider(thickness: 1.2), + // Team Member Rows + ...teamMembers.map((member) => _buildTeamMemberRow(member)), + ], + ), + ), + ); + } + + static Widget _buildTeamMemberRow(dynamic member) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + children: [ + Avatar(firstName: member.firstName, lastName: '', size: 36), + const SizedBox(width: 10), + MyText.bodyMedium(member.firstName, fontWeight: 500), + ], + ), + ); + } +} diff --git a/lib/view/dashboard/daily_task_screen.dart b/lib/view/dashboard/daily_task_screen.dart index d4de1f2..5f6575e 100644 --- a/lib/view/dashboard/daily_task_screen.dart +++ b/lib/view/dashboard/daily_task_screen.dart @@ -16,7 +16,7 @@ import 'package:marco/model/my_paginated_table.dart'; import 'package:marco/controller/dashboard/daily_task_controller.dart'; import 'package:marco/helpers/widgets/avatar.dart'; import 'package:intl/intl.dart'; - +import 'package:marco/helpers/widgets/my_team_model_sheet.dart'; class DailyTaskScreen extends StatefulWidget { const DailyTaskScreen({super.key}); @@ -63,7 +63,7 @@ class _DailyTaskScreenState extends State with UIMixin { return Padding( padding: MySpacing.x(flexSpacing), child: MyText.titleMedium( - "Daily Task", + "Daily Progress Report", fontSize: 18, fontWeight: 600, ), @@ -76,7 +76,7 @@ class _DailyTaskScreenState extends State with UIMixin { child: MyBreadcrumb( children: [ MyBreadcrumbItem(name: 'Dashboard'), - MyBreadcrumbItem(name: 'Daily Task', active: true), + MyBreadcrumbItem(name: 'Daily Progress Report', active: true), ], ), ); @@ -97,61 +97,87 @@ class _DailyTaskScreenState extends State with UIMixin { } Widget _buildProjectFilter() { - return Expanded( - child: Container( - decoration: BoxDecoration( - border: Border.all(color: Colors.black, width: 1.5), - borderRadius: BorderRadius.circular(4), + return Expanded( + child: Container( + decoration: BoxDecoration( + border: Border.all(color: Colors.black, width: 1.5), + borderRadius: BorderRadius.circular(4), + ), + child: PopupMenuButton( + onSelected: (String value) async { + if (value.isNotEmpty) { + dailyTaskController.selectedProjectId = value; + await dailyTaskController.fetchTaskData(value); + } + dailyTaskController.update(); + }, + itemBuilder: (BuildContext context) { + return dailyTaskController.projects + .map>((project) { + return PopupMenuItem( + value: project.id, + child: MyText.bodySmall(project.name), + ); + }).toList(); + }, + offset: const Offset(0, 40), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), ), - child: PopupMenuButton( - onSelected: (String value) async { - if (value.isNotEmpty) { - dailyTaskController.selectedProjectId = value; - await dailyTaskController.fetchTaskData(value); - } - dailyTaskController.update(); - }, - itemBuilder: (BuildContext context) { - return dailyTaskController.projects - .map>((project) { - return PopupMenuItem( - value: project.id, - child: MyText.bodySmall(project.name), - ); - }).toList(); - }, - child: Padding( - padding: - const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), - child: Text( - dailyTaskController.selectedProjectId == null - ? dailyTaskController.projects.isNotEmpty - ? dailyTaskController.projects.first.name - : 'No Tasks' - : dailyTaskController.projects - .firstWhere((project) => - project.id == dailyTaskController.selectedProjectId) - .name, - overflow: TextOverflow.ellipsis, - style: TextStyle(fontWeight: FontWeight.w600), - ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + dailyTaskController.selectedProjectId == null + ? dailyTaskController.projects.isNotEmpty + ? dailyTaskController.projects.first.name + : 'No Tasks' + : dailyTaskController.projects + .firstWhere((project) => + project.id == + dailyTaskController.selectedProjectId) + .name, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontWeight: FontWeight.w600), + ), + ), + const Icon(Icons.arrow_drop_down), + ], ), ), ), - ); + ), + ); +} + + +Widget _buildDateRangeButton() { + String dateRangeText; + if (dailyTaskController.startDateTask != null && + dailyTaskController.endDateTask != null) { + dateRangeText = + '${DateFormat('dd-MM-yyyy').format(dailyTaskController.startDateTask!)}' + ' to ' + '${DateFormat('dd-MM-yyyy').format(dailyTaskController.endDateTask!)}'; + } else { + dateRangeText = "Select Date Range"; } - Widget _buildDateRangeButton() { - return Padding( - padding: const EdgeInsets.all(8.0), - child: TextButton.icon( - icon: const Icon(Icons.date_range), - label: const Text("Select Date Range"), - onPressed: () => dailyTaskController.selectDateRangeForTaskData( - context, dailyTaskController), + return Padding( + padding: const EdgeInsets.all(8.0), + child: TextButton.icon( + icon: const Icon(Icons.date_range), + label: Text(dateRangeText), + onPressed: () => dailyTaskController.selectDateRangeForTaskData( + context, + dailyTaskController, ), - ); - } + ), + ); +} Widget _buildTaskList() { return Padding( @@ -334,7 +360,9 @@ class _DailyTaskScreenState extends State with UIMixin { final teamMembers = task.teamMembers.map((member) => member.firstName).toList(); - final taskComments = task.comments.map((comment) => comment.comment ?? 'No Content').toList(); + final taskComments = task.comments + .map((comment) => comment.comment ?? 'No Content') + .toList(); Get.toNamed( '/daily-task/comment-task', arguments: { @@ -364,67 +392,45 @@ class _DailyTaskScreenState extends State with UIMixin { ]); } - Widget _buildTeamCell(dynamic task) { - return GestureDetector( - onTap: () { - showDialog( - context: context, - builder: (_) => AlertDialog( - title: MyText.bodyMedium("Team Members"), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: task.teamMembers.map((member) { - return ListTile( - leading: Avatar( - firstName: member.firstName, - lastName: '', - size: 32, - ), - title: Text(member.firstName), - ); - }).toList(), - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: MyText.bodyMedium("Close"), - ), - ], - ), - ); - }, - child: SizedBox( - height: 32, - width: 100, - child: Stack( - children: [ - for (int i = 0; i < task.teamMembers.length.clamp(0, 3); i++) - Positioned( - left: i * 24.0, - child: Tooltip( - message: task.teamMembers[i].firstName, - child: Avatar( - firstName: task.teamMembers[i].firstName, - lastName: '', - size: 32, - ), - ), - ), - if (task.teamMembers.length > 3) - Positioned( - left: 2 * 24.0, - child: CircleAvatar( - radius: 16, - backgroundColor: Colors.grey.shade300, - child: MyText.bodyMedium( - '+${task.teamMembers.length - 3}', - style: const TextStyle(fontSize: 12, color: Colors.black87), - ), - ), - ), - ], - ), +Widget _buildTeamCell(dynamic task) { + return GestureDetector( + onTap: () => TeamBottomSheet.show( + context: context, + teamMembers: task.teamMembers, + ), + child: SizedBox( + height: 32, + width: 100, + child: Stack( + children: [ + for (int i = 0; i < task.teamMembers.length.clamp(0, 3); i++) + _buildAvatar(task.teamMembers[i], i * 24.0), + if (task.teamMembers.length > 3) + _buildExtraMembersIndicator(task.teamMembers.length - 3, 48.0), + ], + ), + ), + ); +} + + Widget _buildAvatar(dynamic member, double leftPosition) { + return Positioned( + left: leftPosition, + child: Tooltip( + message: member.firstName, + child: Avatar(firstName: member.firstName, lastName: '', size: 32), + ), + ); + } + + Widget _buildExtraMembersIndicator(int extraMembers, double leftPosition) { + return Positioned( + left: leftPosition, + child: CircleAvatar( + radius: 16, + backgroundColor: Colors.grey.shade300, + child: MyText.bodyMedium('+$extraMembers', + style: const TextStyle(fontSize: 12, color: Colors.black87)), ), ); } diff --git a/lib/view/dashboard/dashboard_screen.dart b/lib/view/dashboard/dashboard_screen.dart index cc66b20..cc658e3 100644 --- a/lib/view/dashboard/dashboard_screen.dart +++ b/lib/view/dashboard/dashboard_screen.dart @@ -51,12 +51,18 @@ class DashboardScreen extends StatelessWidget with UIMixin { List _buildDashboardStats() { final stats = [ - _StatItem(LucideIcons.gauge, "Dashboard", contentTheme.primary, dashboardRoute), - _StatItem(LucideIcons.scan_face, "Attendance", contentTheme.success, attendanceRoute), - _StatItem( LucideIcons.users, "Employees", contentTheme.warning, employeesRoute), - _StatItem(LucideIcons.logs, "Daily Task", contentTheme.info, tasksRoute), - _StatItem(LucideIcons.logs, "Daily Task Planing", contentTheme.info, tasksRoute), - _StatItem(LucideIcons.folder, "Projects", contentTheme.secondary, projectsRoute), + _StatItem( + LucideIcons.gauge, "Dashboard", contentTheme.primary, dashboardRoute), + _StatItem(LucideIcons.scan_face, "Attendance", contentTheme.success, + attendanceRoute), + _StatItem( + LucideIcons.users, "Employees", contentTheme.warning, employeesRoute), + _StatItem(LucideIcons.logs, "Daily Progress Report", contentTheme.info, + tasksRoute), + _StatItem(LucideIcons.logs, "Daily Task Planing", contentTheme.info, + tasksRoute), + _StatItem(LucideIcons.folder, "Projects", contentTheme.secondary, + projectsRoute), ]; return List.generate( diff --git a/lib/view/layouts/left_bar.dart b/lib/view/layouts/left_bar.dart index 9747f75..0782d62 100644 --- a/lib/view/layouts/left_bar.dart +++ b/lib/view/layouts/left_bar.dart @@ -126,7 +126,7 @@ class _LeftBarState extends State route: '/dashboard/employees'), NavigationItem( iconData: LucideIcons.list, - title: "Daily Task", + title: "Daily Progress Report", isCondensed: isCondensed, route: '/dashboard/daily-task'), ], diff --git a/lib/view/taskPlaning/comment_task_screen.dart b/lib/view/taskPlaning/comment_task_screen.dart index 4cfacc7..5047629 100644 --- a/lib/view/taskPlaning/comment_task_screen.dart +++ b/lib/view/taskPlaning/comment_task_screen.dart @@ -16,6 +16,7 @@ import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/helpers/widgets/my_text_style.dart'; import 'package:marco/view/layouts/layout.dart'; import 'package:marco/helpers/widgets/avatar.dart'; +import 'package:marco/helpers/widgets/my_team_model_sheet.dart'; class CommentTaskScreen extends StatefulWidget { const CommentTaskScreen({super.key}); @@ -23,6 +24,10 @@ class CommentTaskScreen extends StatefulWidget { @override State createState() => _CommentTaskScreenState(); } +class _Member { + final String firstName; + _Member(this.firstName); +} class _CommentTaskScreenState extends State with UIMixin { final ReportTaskController controller = Get.put(ReportTaskController()); @@ -67,7 +72,7 @@ class _CommentTaskScreenState extends State with UIMixin { fontSize: 18, fontWeight: 600), MyBreadcrumb( children: [ - MyBreadcrumbItem(name: 'Daily Task'), + MyBreadcrumbItem(name: 'Daily Progress Report'), MyBreadcrumbItem(name: 'Comment Task'), ], ), @@ -217,91 +222,65 @@ class _CommentTaskScreenState extends State with UIMixin { ), ); } -Widget buildTeamMembers() { - final teamMembersText = - controller.basicValidator.getController('team_members')?.text ?? ''; - final members = teamMembersText - .split(',') - .map((e) => e.trim()) - .where((e) => e.isNotEmpty) - .toList(); + Widget buildTeamMembers() { + final teamMembersText = + controller.basicValidator.getController('team_members')?.text ?? ''; + final members = teamMembersText + .split(',') + .map((e) => e.trim()) + .where((e) => e.isNotEmpty) + .toList(); - return Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - MyText.labelMedium("Team Members:"), - MySpacing.width(12), - GestureDetector( - onTap: () => showTeamMembersDialog(members), - child: SizedBox( - height: 32, - width: 100, - child: Stack( - children: [ - for (int i = 0; i < members.length.clamp(0, 3); i++) - Positioned( - left: i * 24.0, - child: Tooltip( - message: members[i], - child: Avatar( - firstName: members[i], - lastName: '', - size: 32, + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + MyText.labelMedium("Team Members:"), + MySpacing.width(12), + GestureDetector( + onTap: () { + TeamBottomSheet.show( + context: context, + teamMembers: members.map((name) => _Member(name)).toList(), + ); + }, + child: SizedBox( + height: 32, + width: 100, + child: Stack( + children: [ + for (int i = 0; i < members.length.clamp(0, 3); i++) + Positioned( + left: i * 24.0, + child: Tooltip( + message: members[i], + child: Avatar( + firstName: members[i], + lastName: '', + size: 32, + ), ), ), - ), - if (members.length > 3) - Positioned( - left: 2 * 24.0, - child: CircleAvatar( - radius: 16, - backgroundColor: Colors.grey.shade300, - child: MyText.bodyMedium( - '+${members.length - 3}', - style: const TextStyle( - fontSize: 12, color: Colors.black87), + if (members.length > 3) + Positioned( + left: 2 * 24.0, + child: CircleAvatar( + radius: 16, + backgroundColor: Colors.grey.shade300, + child: MyText.bodyMedium( + '+${members.length - 3}', + style: const TextStyle( + fontSize: 12, color: Colors.black87), + ), ), ), - ), - ], + ], + ), ), ), - ), - ], - ), - ); -} - - void showTeamMembersDialog(List members) { - showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: MyText.titleMedium('Team Members'), - content: SizedBox( - width: 300, - child: ListView.builder( - shrinkWrap: true, - itemCount: members.length, - itemBuilder: (context, index) { - final name = members[index]; - return ListTile( - leading: Avatar(firstName: name, lastName: "", size: 32), - title: MyText.bodyMedium(name), - ); - }, - ), - ), - actions: [ - TextButton( - onPressed: () => Get.back(), - child: MyText.bodySmall("Close"), - ), - ], - ); - }, + ], + ), ); } diff --git a/lib/view/taskPlaning/report_task_screen.dart b/lib/view/taskPlaning/report_task_screen.dart index 2edab4f..37eb76e 100644 --- a/lib/view/taskPlaning/report_task_screen.dart +++ b/lib/view/taskPlaning/report_task_screen.dart @@ -62,7 +62,7 @@ class _ReportTaskScreenState extends State with UIMixin { fontSize: 18, fontWeight: 600), MyBreadcrumb( children: [ - MyBreadcrumbItem(name: 'Daily Task'), + MyBreadcrumbItem(name: 'Daily Progress Report'), MyBreadcrumbItem(name: 'Report Task'), ], ), @@ -195,45 +195,55 @@ class _ReportTaskScreenState extends State with UIMixin { ), MySpacing.width(12), MyButton( - onPressed: () async { - if (controller.basicValidator.validateForm()) { - await controller.reportTask( - projectId: controller.basicValidator - .getController('task_id') - ?.text ?? - '', // Replace with actual ID - comment: controller.basicValidator - .getController('comment') - ?.text ?? - '', - completedTask: int.tryParse(controller.basicValidator - .getController('completed_work') - ?.text ?? - '') ?? - 0, - checklist: [], - reportedDate: DateTime.now(), - ); - } - }, + onPressed: controller.reportStatus.value == ApiStatus.loading + ? null + : () async { + if (controller.basicValidator.validateForm()) { + await controller.reportTask( + projectId: controller.basicValidator + .getController('task_id') + ?.text ?? + '', + comment: controller.basicValidator + .getController('comment') + ?.text ?? + '', + completedTask: int.tryParse(controller + .basicValidator + .getController('completed_work') + ?.text ?? + '') ?? + 0, + checklist: [], + reportedDate: DateTime.now(), + ); + } + }, elevation: 0, padding: MySpacing.xy(20, 16), backgroundColor: contentTheme.primary, borderRadiusAll: AppStyle.buttonRadius.medium, - child: MyText.bodySmall( - 'Save', - color: contentTheme.onPrimary, - ), + child: Obx(() { + if (controller.reportStatus.value == ApiStatus.loading) { + return SizedBox( + height: 16, + width: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation( + contentTheme.onPrimary), + ), + ); + } else { + return MyText.bodySmall( + 'Save', + color: contentTheme.onPrimary, + ); + } + }), ), ], ), - - // Loading spinner - Obx(() { - return controller.isLoading.value - ? Center(child: CircularProgressIndicator()) - : SizedBox.shrink(); - }), ], ), ),