Refactor DailyTask and ReportTask screens for improved UI and functionality; update project selection and team member display logic.

This commit is contained in:
Vaibhav Surve 2025-05-16 16:53:09 +05:30
parent 8333910de4
commit 19e705f428
9 changed files with 307 additions and 242 deletions

View File

@ -50,7 +50,7 @@ class DailyTaskController extends GetxController {
projects = response!.map((json) => ProjectModel.fromJson(json)).toList(); projects = response!.map((json) => ProjectModel.fromJson(json)).toList();
selectedProjectId = projects.first.id.toString(); selectedProjectId = projects.first.id.toString();
log.i("Projects fetched: ${projects.length} projects loaded."); log.i("Projects fetched: ${projects.length} projects loaded.");
update();
await fetchTaskData(selectedProjectId); await fetchTaskData(selectedProjectId);
} }

View File

@ -7,11 +7,13 @@ import 'package:get/get.dart';
import 'package:logger/logger.dart'; import 'package:logger/logger.dart';
final Logger logger = Logger(); final Logger logger = Logger();
enum ApiStatus { idle, loading, success, failure }
class ReportTaskController extends MyController { class ReportTaskController extends MyController {
List<PlatformFile> files = []; List<PlatformFile> files = [];
MyFormValidator basicValidator = MyFormValidator(); MyFormValidator basicValidator = MyFormValidator();
RxBool isLoading = false.obs; RxBool isLoading = false.obs;
Rx<ApiStatus> reportStatus = ApiStatus.idle.obs;
Rx<ApiStatus> commentStatus = ApiStatus.idle.obs;
@override @override
void onInit() { void onInit() {
@ -92,12 +94,17 @@ class ReportTaskController extends MyController {
final completedWork = final completedWork =
basicValidator.getController('completed_work')?.text.trim(); basicValidator.getController('completed_work')?.text.trim();
final commentField = basicValidator.getController('comment')?.text.trim();
if (completedWork == null || completedWork.isEmpty) { if (completedWork == null || completedWork.isEmpty) {
Get.snackbar("Error", "Completed work is required."); Get.snackbar("Error", "Completed work is required.");
return; 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) { if (commentField == null || commentField.isEmpty) {
Get.snackbar("Error", "Comment is required."); Get.snackbar("Error", "Comment is required.");
@ -126,7 +133,8 @@ class ReportTaskController extends MyController {
isLoading.value = false; isLoading.value = false;
} }
} }
Future<void> commentTask({
Future<void> commentTask({
required String projectId, required String projectId,
required String comment, required String comment,
required int completedTask, required int completedTask,
@ -158,13 +166,13 @@ class ReportTaskController extends MyController {
); );
if (success) { if (success) {
Get.snackbar("Success", "Task reported successfully!"); Get.snackbar("Success", "Task commented successfully!");
} else { } else {
Get.snackbar("Error", "Failed to report task."); Get.snackbar("Error", "Failed to comment task.");
} }
} catch (e) { } catch (e) {
logger.e("Error reporting task: $e"); logger.e("Error commenting task: $e");
Get.snackbar("Error", "An error occurred while reporting the task."); Get.snackbar("Error", "An error occurred while commenting the task.");
} finally { } finally {
isLoading.value = false; isLoading.value = false;
} }

View File

@ -6,7 +6,7 @@ import 'package:logger/logger.dart';
import 'package:marco/helpers/services/storage/local_storage.dart'; import 'package:marco/helpers/services/storage/local_storage.dart';
import 'package:marco/helpers/services/auth_service.dart'; import 'package:marco/helpers/services/auth_service.dart';
import 'package:marco/helpers/services/api_endpoints.dart'; import 'package:marco/helpers/services/api_endpoints.dart';
import 'package:get/get.dart';
final Logger logger = Logger(); final Logger logger = Logger();
class ApiService { class ApiService {
@ -332,6 +332,7 @@ class ApiService {
final json = jsonDecode(response.body); final json = jsonDecode(response.body);
if (response.statusCode == 200 && json['success'] == true) { if (response.statusCode == 200 && json['success'] == true) {
Get.back();
return true; return true;
} else { } else {
_log("Failed to report task: ${json['message'] ?? 'Unknown error'}"); _log("Failed to report task: ${json['message'] ?? 'Unknown error'}");
@ -358,9 +359,10 @@ class ApiService {
final json = jsonDecode(response.body); final json = jsonDecode(response.body);
if (response.statusCode == 200 && json['success'] == true) { if (response.statusCode == 200 && json['success'] == true) {
Get.back();
return true; return true;
} else { } else {
_log("Failed to report task: ${json['message'] ?? 'Unknown error'}"); _log("Failed to comment task: ${json['message'] ?? 'Unknown error'}");
return false; return false;
} }
} }

View File

@ -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<dynamic> 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),
],
),
);
}
}

View File

@ -16,7 +16,7 @@ import 'package:marco/model/my_paginated_table.dart';
import 'package:marco/controller/dashboard/daily_task_controller.dart'; import 'package:marco/controller/dashboard/daily_task_controller.dart';
import 'package:marco/helpers/widgets/avatar.dart'; import 'package:marco/helpers/widgets/avatar.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:marco/helpers/widgets/my_team_model_sheet.dart';
class DailyTaskScreen extends StatefulWidget { class DailyTaskScreen extends StatefulWidget {
const DailyTaskScreen({super.key}); const DailyTaskScreen({super.key});
@ -63,7 +63,7 @@ class _DailyTaskScreenState extends State<DailyTaskScreen> with UIMixin {
return Padding( return Padding(
padding: MySpacing.x(flexSpacing), padding: MySpacing.x(flexSpacing),
child: MyText.titleMedium( child: MyText.titleMedium(
"Daily Task", "Daily Progress Report",
fontSize: 18, fontSize: 18,
fontWeight: 600, fontWeight: 600,
), ),
@ -76,7 +76,7 @@ class _DailyTaskScreenState extends State<DailyTaskScreen> with UIMixin {
child: MyBreadcrumb( child: MyBreadcrumb(
children: [ children: [
MyBreadcrumbItem(name: 'Dashboard'), MyBreadcrumbItem(name: 'Dashboard'),
MyBreadcrumbItem(name: 'Daily Task', active: true), MyBreadcrumbItem(name: 'Daily Progress Report', active: true),
], ],
), ),
); );
@ -97,61 +97,87 @@ class _DailyTaskScreenState extends State<DailyTaskScreen> with UIMixin {
} }
Widget _buildProjectFilter() { Widget _buildProjectFilter() {
return Expanded( return Expanded(
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all(color: Colors.black, width: 1.5), border: Border.all(color: Colors.black, width: 1.5),
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
),
child: PopupMenuButton<String>(
onSelected: (String value) async {
if (value.isNotEmpty) {
dailyTaskController.selectedProjectId = value;
await dailyTaskController.fetchTaskData(value);
}
dailyTaskController.update();
},
itemBuilder: (BuildContext context) {
return dailyTaskController.projects
.map<PopupMenuItem<String>>((project) {
return PopupMenuItem<String>(
value: project.id,
child: MyText.bodySmall(project.name),
);
}).toList();
},
offset: const Offset(0, 40),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
), ),
child: PopupMenuButton<String>( child: Padding(
onSelected: (String value) async { padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
if (value.isNotEmpty) { child: Row(
dailyTaskController.selectedProjectId = value; mainAxisAlignment: MainAxisAlignment.spaceBetween,
await dailyTaskController.fetchTaskData(value); children: [
} Expanded(
dailyTaskController.update(); child: Text(
}, dailyTaskController.selectedProjectId == null
itemBuilder: (BuildContext context) { ? dailyTaskController.projects.isNotEmpty
return dailyTaskController.projects ? dailyTaskController.projects.first.name
.map<PopupMenuItem<String>>((project) { : 'No Tasks'
return PopupMenuItem<String>( : dailyTaskController.projects
value: project.id, .firstWhere((project) =>
child: MyText.bodySmall(project.name), project.id ==
); dailyTaskController.selectedProjectId)
}).toList(); .name,
}, overflow: TextOverflow.ellipsis,
child: Padding( style: const TextStyle(fontWeight: FontWeight.w600),
padding: ),
const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), ),
child: Text( const Icon(Icons.arrow_drop_down),
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),
),
), ),
), ),
), ),
); ),
);
}
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(
return Padding( padding: const EdgeInsets.all(8.0),
padding: const EdgeInsets.all(8.0), child: TextButton.icon(
child: TextButton.icon( icon: const Icon(Icons.date_range),
icon: const Icon(Icons.date_range), label: Text(dateRangeText),
label: const Text("Select Date Range"), onPressed: () => dailyTaskController.selectDateRangeForTaskData(
onPressed: () => dailyTaskController.selectDateRangeForTaskData( context,
context, dailyTaskController), dailyTaskController,
), ),
); ),
} );
}
Widget _buildTaskList() { Widget _buildTaskList() {
return Padding( return Padding(
@ -334,7 +360,9 @@ class _DailyTaskScreenState extends State<DailyTaskScreen> with UIMixin {
final teamMembers = final teamMembers =
task.teamMembers.map((member) => member.firstName).toList(); 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( Get.toNamed(
'/daily-task/comment-task', '/daily-task/comment-task',
arguments: { arguments: {
@ -364,67 +392,45 @@ class _DailyTaskScreenState extends State<DailyTaskScreen> with UIMixin {
]); ]);
} }
Widget _buildTeamCell(dynamic task) { Widget _buildTeamCell(dynamic task) {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () => TeamBottomSheet.show(
showDialog( context: context,
context: context, teamMembers: task.teamMembers,
builder: (_) => AlertDialog( ),
title: MyText.bodyMedium("Team Members"), child: SizedBox(
content: Column( height: 32,
mainAxisSize: MainAxisSize.min, width: 100,
crossAxisAlignment: CrossAxisAlignment.start, child: Stack(
children: task.teamMembers.map<Widget>((member) { children: [
return ListTile( for (int i = 0; i < task.teamMembers.length.clamp(0, 3); i++)
leading: Avatar( _buildAvatar(task.teamMembers[i], i * 24.0),
firstName: member.firstName, if (task.teamMembers.length > 3)
lastName: '', _buildExtraMembersIndicator(task.teamMembers.length - 3, 48.0),
size: 32, ],
), ),
title: Text(member.firstName), ),
); );
}).toList(), }
),
actions: [ Widget _buildAvatar(dynamic member, double leftPosition) {
TextButton( return Positioned(
onPressed: () => Navigator.pop(context), left: leftPosition,
child: MyText.bodyMedium("Close"), child: Tooltip(
), message: member.firstName,
], child: Avatar(firstName: member.firstName, lastName: '', size: 32),
), ),
); );
}, }
child: SizedBox(
height: 32, Widget _buildExtraMembersIndicator(int extraMembers, double leftPosition) {
width: 100, return Positioned(
child: Stack( left: leftPosition,
children: [ child: CircleAvatar(
for (int i = 0; i < task.teamMembers.length.clamp(0, 3); i++) radius: 16,
Positioned( backgroundColor: Colors.grey.shade300,
left: i * 24.0, child: MyText.bodyMedium('+$extraMembers',
child: Tooltip( style: const TextStyle(fontSize: 12, color: Colors.black87)),
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),
),
),
),
],
),
), ),
); );
} }

View File

@ -51,12 +51,18 @@ class DashboardScreen extends StatelessWidget with UIMixin {
List<Widget> _buildDashboardStats() { List<Widget> _buildDashboardStats() {
final stats = [ final stats = [
_StatItem(LucideIcons.gauge, "Dashboard", contentTheme.primary, dashboardRoute), _StatItem(
_StatItem(LucideIcons.scan_face, "Attendance", contentTheme.success, attendanceRoute), LucideIcons.gauge, "Dashboard", contentTheme.primary, dashboardRoute),
_StatItem( LucideIcons.users, "Employees", contentTheme.warning, employeesRoute), _StatItem(LucideIcons.scan_face, "Attendance", contentTheme.success,
_StatItem(LucideIcons.logs, "Daily Task", contentTheme.info, tasksRoute), attendanceRoute),
_StatItem(LucideIcons.logs, "Daily Task Planing", contentTheme.info, tasksRoute), _StatItem(
_StatItem(LucideIcons.folder, "Projects", contentTheme.secondary, projectsRoute), 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( return List.generate(

View File

@ -126,7 +126,7 @@ class _LeftBarState extends State<LeftBar>
route: '/dashboard/employees'), route: '/dashboard/employees'),
NavigationItem( NavigationItem(
iconData: LucideIcons.list, iconData: LucideIcons.list,
title: "Daily Task", title: "Daily Progress Report",
isCondensed: isCondensed, isCondensed: isCondensed,
route: '/dashboard/daily-task'), route: '/dashboard/daily-task'),
], ],

View File

@ -16,6 +16,7 @@ import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/helpers/widgets/my_text_style.dart'; import 'package:marco/helpers/widgets/my_text_style.dart';
import 'package:marco/view/layouts/layout.dart'; import 'package:marco/view/layouts/layout.dart';
import 'package:marco/helpers/widgets/avatar.dart'; import 'package:marco/helpers/widgets/avatar.dart';
import 'package:marco/helpers/widgets/my_team_model_sheet.dart';
class CommentTaskScreen extends StatefulWidget { class CommentTaskScreen extends StatefulWidget {
const CommentTaskScreen({super.key}); const CommentTaskScreen({super.key});
@ -23,6 +24,10 @@ class CommentTaskScreen extends StatefulWidget {
@override @override
State<CommentTaskScreen> createState() => _CommentTaskScreenState(); State<CommentTaskScreen> createState() => _CommentTaskScreenState();
} }
class _Member {
final String firstName;
_Member(this.firstName);
}
class _CommentTaskScreenState extends State<CommentTaskScreen> with UIMixin { class _CommentTaskScreenState extends State<CommentTaskScreen> with UIMixin {
final ReportTaskController controller = Get.put(ReportTaskController()); final ReportTaskController controller = Get.put(ReportTaskController());
@ -67,7 +72,7 @@ class _CommentTaskScreenState extends State<CommentTaskScreen> with UIMixin {
fontSize: 18, fontWeight: 600), fontSize: 18, fontWeight: 600),
MyBreadcrumb( MyBreadcrumb(
children: [ children: [
MyBreadcrumbItem(name: 'Daily Task'), MyBreadcrumbItem(name: 'Daily Progress Report'),
MyBreadcrumbItem(name: 'Comment Task'), MyBreadcrumbItem(name: 'Comment Task'),
], ],
), ),
@ -217,91 +222,65 @@ class _CommentTaskScreenState extends State<CommentTaskScreen> with UIMixin {
), ),
); );
} }
Widget buildTeamMembers() { Widget buildTeamMembers() {
final teamMembersText = final teamMembersText =
controller.basicValidator.getController('team_members')?.text ?? ''; controller.basicValidator.getController('team_members')?.text ?? '';
final members = teamMembersText final members = teamMembersText
.split(',') .split(',')
.map((e) => e.trim()) .map((e) => e.trim())
.where((e) => e.isNotEmpty) .where((e) => e.isNotEmpty)
.toList(); .toList();
return Padding( return Padding(
padding: const EdgeInsets.only(bottom: 16), padding: const EdgeInsets.only(bottom: 16),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
MyText.labelMedium("Team Members:"), MyText.labelMedium("Team Members:"),
MySpacing.width(12), MySpacing.width(12),
GestureDetector( GestureDetector(
onTap: () => showTeamMembersDialog(members), onTap: () {
child: SizedBox( TeamBottomSheet.show(
height: 32, context: context,
width: 100, teamMembers: members.map((name) => _Member(name)).toList(),
child: Stack( );
children: [ },
for (int i = 0; i < members.length.clamp(0, 3); i++) child: SizedBox(
Positioned( height: 32,
left: i * 24.0, width: 100,
child: Tooltip( child: Stack(
message: members[i], children: [
child: Avatar( for (int i = 0; i < members.length.clamp(0, 3); i++)
firstName: members[i], Positioned(
lastName: '', left: i * 24.0,
size: 32, child: Tooltip(
message: members[i],
child: Avatar(
firstName: members[i],
lastName: '',
size: 32,
),
), ),
), ),
), if (members.length > 3)
if (members.length > 3) Positioned(
Positioned( left: 2 * 24.0,
left: 2 * 24.0, child: CircleAvatar(
child: CircleAvatar( radius: 16,
radius: 16, backgroundColor: Colors.grey.shade300,
backgroundColor: Colors.grey.shade300, child: MyText.bodyMedium(
child: MyText.bodyMedium( '+${members.length - 3}',
'+${members.length - 3}', style: const TextStyle(
style: const TextStyle( fontSize: 12, color: Colors.black87),
fontSize: 12, color: Colors.black87), ),
), ),
), ),
), ],
], ),
), ),
), ),
), ],
], ),
),
);
}
void showTeamMembersDialog(List<String> 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"),
),
],
);
},
); );
} }

View File

@ -62,7 +62,7 @@ class _ReportTaskScreenState extends State<ReportTaskScreen> with UIMixin {
fontSize: 18, fontWeight: 600), fontSize: 18, fontWeight: 600),
MyBreadcrumb( MyBreadcrumb(
children: [ children: [
MyBreadcrumbItem(name: 'Daily Task'), MyBreadcrumbItem(name: 'Daily Progress Report'),
MyBreadcrumbItem(name: 'Report Task'), MyBreadcrumbItem(name: 'Report Task'),
], ],
), ),
@ -195,45 +195,55 @@ class _ReportTaskScreenState extends State<ReportTaskScreen> with UIMixin {
), ),
MySpacing.width(12), MySpacing.width(12),
MyButton( MyButton(
onPressed: () async { onPressed: controller.reportStatus.value == ApiStatus.loading
if (controller.basicValidator.validateForm()) { ? null
await controller.reportTask( : () async {
projectId: controller.basicValidator if (controller.basicValidator.validateForm()) {
.getController('task_id') await controller.reportTask(
?.text ?? projectId: controller.basicValidator
'', // Replace with actual ID .getController('task_id')
comment: controller.basicValidator ?.text ??
.getController('comment') '',
?.text ?? comment: controller.basicValidator
'', .getController('comment')
completedTask: int.tryParse(controller.basicValidator ?.text ??
.getController('completed_work') '',
?.text ?? completedTask: int.tryParse(controller
'') ?? .basicValidator
0, .getController('completed_work')
checklist: [], ?.text ??
reportedDate: DateTime.now(), '') ??
); 0,
} checklist: [],
}, reportedDate: DateTime.now(),
);
}
},
elevation: 0, elevation: 0,
padding: MySpacing.xy(20, 16), padding: MySpacing.xy(20, 16),
backgroundColor: contentTheme.primary, backgroundColor: contentTheme.primary,
borderRadiusAll: AppStyle.buttonRadius.medium, borderRadiusAll: AppStyle.buttonRadius.medium,
child: MyText.bodySmall( child: Obx(() {
'Save', if (controller.reportStatus.value == ApiStatus.loading) {
color: contentTheme.onPrimary, return SizedBox(
), height: 16,
width: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
contentTheme.onPrimary),
),
);
} else {
return MyText.bodySmall(
'Save',
color: contentTheme.onPrimary,
);
}
}),
), ),
], ],
), ),
// Loading spinner
Obx(() {
return controller.isLoading.value
? Center(child: CircularProgressIndicator())
: SizedBox.shrink();
}),
], ],
), ),
), ),