- Added a floating action button to the Layout widget for better accessibility. - Updated the left bar navigation items for clarity and consistency. - Introduced Daily Progress Report and Daily Task Planning screens with comprehensive UI. - Implemented filtering and refreshing functionalities in task planning. - Improved user experience with better spacing and layout adjustments. - Updated pubspec.yaml to include new dependencies for image handling and path management.
438 lines
15 KiB
Dart
438 lines
15 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:marco/helpers/theme/app_theme.dart';
|
|
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
|
|
import 'package:marco/helpers/widgets/my_breadcrumb.dart';
|
|
import 'package:marco/helpers/widgets/my_breadcrumb_item.dart';
|
|
import 'package:marco/helpers/widgets/my_flex.dart';
|
|
import 'package:marco/helpers/widgets/my_flex_item.dart';
|
|
import 'package:marco/helpers/widgets/my_spacing.dart';
|
|
import 'package:marco/helpers/widgets/my_text.dart';
|
|
import 'package:marco/view/layouts/layout.dart';
|
|
import 'package:marco/controller/permission_controller.dart';
|
|
import 'package:marco/helpers/widgets/my_loading_component.dart';
|
|
import 'package:marco/helpers/widgets/my_refresh_wrapper.dart';
|
|
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});
|
|
|
|
@override
|
|
State<DailyTaskScreen> createState() => _DailyTaskScreenState();
|
|
}
|
|
|
|
class _DailyTaskScreenState extends State<DailyTaskScreen> with UIMixin {
|
|
final DailyTaskController dailyTaskController =
|
|
Get.put(DailyTaskController());
|
|
final PermissionController permissionController =
|
|
Get.put(PermissionController());
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Layout(
|
|
child: Obx(() {
|
|
return LoadingComponent(
|
|
isLoading: dailyTaskController.isLoading.value,
|
|
loadingText: 'Loading Tasks...',
|
|
child: GetBuilder<DailyTaskController>(
|
|
init: dailyTaskController,
|
|
builder: (controller) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_buildHeader(),
|
|
MySpacing.height(flexSpacing),
|
|
_buildBreadcrumb(),
|
|
MySpacing.height(flexSpacing),
|
|
_buildFilterSection(),
|
|
MySpacing.height(flexSpacing),
|
|
_buildTaskList(),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}),
|
|
);
|
|
}
|
|
|
|
Widget _buildHeader() {
|
|
return Padding(
|
|
padding: MySpacing.x(flexSpacing),
|
|
child: MyText.titleMedium(
|
|
"Daily Progress Report",
|
|
fontSize: 18,
|
|
fontWeight: 600,
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildBreadcrumb() {
|
|
return Padding(
|
|
padding: MySpacing.x(flexSpacing),
|
|
child: MyBreadcrumb(
|
|
children: [
|
|
MyBreadcrumbItem(name: 'Dashboard'),
|
|
MyBreadcrumbItem(name: 'Daily Progress Report', active: true),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildFilterSection() {
|
|
return Padding(
|
|
padding: MySpacing.x(flexSpacing),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
_buildProjectFilter(),
|
|
const SizedBox(width: 10),
|
|
_buildDateRangeButton(),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildProjectFilter() {
|
|
return Expanded(
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
border: Border.all(color: Colors.black, width: 1.5),
|
|
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: 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";
|
|
}
|
|
|
|
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(
|
|
padding: MySpacing.x(flexSpacing / 2),
|
|
child: MyFlex(
|
|
children: [
|
|
MyFlexItem(sizes: 'lg-6', child: employeeListTab()),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget employeeListTab() {
|
|
if (dailyTaskController.dailyTasks.isEmpty) {
|
|
return Center(
|
|
child: MyText.bodySmall("No Tasks Assigned to This Project",
|
|
fontWeight: 600),
|
|
);
|
|
}
|
|
Map<String, List<dynamic>> groupedTasks = {};
|
|
for (var task in dailyTaskController.dailyTasks) {
|
|
String dateKey =
|
|
DateFormat('dd-MM-yyyy').format(task.assignmentDate);
|
|
groupedTasks.putIfAbsent(dateKey, () => []).add(task);
|
|
}
|
|
|
|
// Sort dates descending (latest first)
|
|
final sortedEntries = groupedTasks.entries.toList()
|
|
..sort((a, b) => DateFormat('dd-MM-yyyy')
|
|
.parse(b.key)
|
|
.compareTo(DateFormat('dd-MM-yyyy').parse(a.key)));
|
|
|
|
// Flatten grouped data into one list with optional visual separators
|
|
List<DataRow> allRows = [];
|
|
|
|
for (var entry in sortedEntries) {
|
|
allRows.add(
|
|
DataRow(
|
|
color: WidgetStateProperty.all(Colors.grey.shade200),
|
|
cells: [
|
|
DataCell(MyText.titleSmall('Date: ${entry.key}')),
|
|
DataCell(MyText.titleSmall('')),
|
|
DataCell(MyText.titleSmall('')),
|
|
DataCell(MyText.titleSmall('')),
|
|
DataCell(MyText.titleSmall('')),
|
|
DataCell(MyText.titleSmall('')),
|
|
],
|
|
),
|
|
);
|
|
|
|
allRows.addAll(entry.value.map((task) => _buildRow(task)));
|
|
}
|
|
|
|
return MyRefreshableContent(
|
|
onRefresh: () async {
|
|
if (dailyTaskController.selectedProjectId != null) {
|
|
await dailyTaskController
|
|
.fetchTaskData(dailyTaskController.selectedProjectId!);
|
|
}
|
|
},
|
|
child: MyPaginatedTable(
|
|
columns: _buildColumns(),
|
|
rows: allRows,
|
|
),
|
|
);
|
|
}
|
|
|
|
List<DataColumn> _buildColumns() {
|
|
return [
|
|
DataColumn(
|
|
label: MyText.labelLarge('Activity', color: contentTheme.primary)),
|
|
DataColumn(
|
|
label: MyText.labelLarge('Assigned', color: contentTheme.primary)),
|
|
DataColumn(
|
|
label: MyText.labelLarge('Completed', color: contentTheme.primary)),
|
|
DataColumn(
|
|
label: MyText.labelLarge('Assigned On', color: contentTheme.primary)),
|
|
DataColumn(label: MyText.labelLarge('Team', color: contentTheme.primary)),
|
|
DataColumn(
|
|
label: MyText.labelLarge('Actions', color: contentTheme.primary)),
|
|
];
|
|
}
|
|
|
|
DataRow _buildRow(dynamic task) {
|
|
final workItem = task.workItem;
|
|
final location = [
|
|
workItem?.workArea?.floor?.building?.name,
|
|
workItem?.workArea?.floor?.floorName,
|
|
workItem?.workArea?.areaName
|
|
].where((e) => e != null && e.isNotEmpty).join(' > ');
|
|
|
|
return DataRow(cells: [
|
|
DataCell(
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
MyText.bodyMedium(workItem?.activityMaster?.activityName ?? 'N/A',
|
|
fontWeight: 600),
|
|
SizedBox(height: 2),
|
|
MyText.bodySmall(location, color: Colors.grey),
|
|
],
|
|
),
|
|
),
|
|
DataCell(
|
|
MyText.bodyMedium(
|
|
'${task.plannedTask ?? "NA"} / '
|
|
'${(workItem?.plannedWork != null && workItem?.completedWork != null) ? (workItem!.plannedWork! - workItem.completedWork!) : "NA"}',
|
|
),
|
|
),
|
|
DataCell(MyText.bodyMedium(task.completedTask.toString())),
|
|
DataCell(MyText.bodyMedium(DateFormat('dd-MM-yyyy')
|
|
.format(DateTime.parse(task.assignmentDate)))),
|
|
DataCell(_buildTeamCell(task)),
|
|
DataCell(Row(
|
|
children: [
|
|
ElevatedButton(
|
|
onPressed: () {
|
|
final activityName =
|
|
task.workItem?.activityMaster?.activityName ?? 'N/A';
|
|
final assigned = '${task.plannedTask ?? "NA"} / '
|
|
'${(task.workItem?.plannedWork != null && task.workItem?.completedWork != null) ? (task.workItem!.plannedWork! - task.workItem.completedWork!) : "NA"}';
|
|
final assignedBy =
|
|
"${task.assignedBy.firstName} ${task.assignedBy.lastName ?? ''}";
|
|
final completed = task.completedTask.toString();
|
|
final assignedOn = DateFormat('dd-MM-yyyy')
|
|
.format(DateTime.parse(task.assignmentDate));
|
|
final taskId = task.id;
|
|
final location = [
|
|
task.workItem?.workArea?.floor?.building?.name,
|
|
task.workItem?.workArea?.floor?.floorName,
|
|
task.workItem?.workArea?.areaName
|
|
].where((e) => e != null && e.isNotEmpty).join(' > ');
|
|
|
|
final teamMembers =
|
|
task.teamMembers.map((member) => member.firstName).toList();
|
|
|
|
// Navigate with detailed values
|
|
Get.toNamed(
|
|
'/daily-task/report-task',
|
|
arguments: {
|
|
'activity': activityName,
|
|
'assigned': assigned,
|
|
'taskId': taskId,
|
|
'assignedBy': assignedBy,
|
|
'completed': completed,
|
|
'assignedOn': assignedOn,
|
|
'location': location,
|
|
'teamSize': task.teamMembers.length,
|
|
'teamMembers': teamMembers,
|
|
},
|
|
);
|
|
},
|
|
style: ElevatedButton.styleFrom(
|
|
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 6),
|
|
minimumSize: const Size(60, 20),
|
|
textStyle: const TextStyle(fontSize: 12),
|
|
),
|
|
child: const Text("Report"),
|
|
),
|
|
const SizedBox(width: 8),
|
|
ElevatedButton(
|
|
onPressed: () {
|
|
final activityName =
|
|
task.workItem?.activityMaster?.activityName ?? 'N/A';
|
|
final assigned = '${task.plannedTask ?? "NA"} / '
|
|
'${(task.workItem?.plannedWork != null && task.workItem?.completedWork != null) ? (task.workItem!.plannedWork! - task.workItem.completedWork!) : "NA"}';
|
|
final plannedWork = '${(task.plannedTask.toString())}';
|
|
final assignedBy =
|
|
"${task.assignedBy.firstName} ${task.assignedBy.lastName ?? ''}";
|
|
final completedWork = '${(task.completedTask.toString())}';
|
|
final assignedOn = DateFormat('dd-MM-yyyy')
|
|
.format(DateTime.parse(task.assignmentDate));
|
|
final taskId = task.id;
|
|
final location = [
|
|
task.workItem?.workArea?.floor?.building?.name,
|
|
task.workItem?.workArea?.floor?.floorName,
|
|
task.workItem?.workArea?.areaName
|
|
].where((e) => e != null && e.isNotEmpty).join(' > ');
|
|
|
|
final teamMembers =
|
|
task.teamMembers.map((member) => member.firstName).toList();
|
|
final taskComments = task.comments
|
|
.map((comment) => comment.comment ?? 'No Content')
|
|
.toList();
|
|
Get.toNamed(
|
|
'/daily-task/comment-task',
|
|
arguments: {
|
|
'activity': activityName,
|
|
'assigned': assigned,
|
|
'taskId': taskId,
|
|
'assignedBy': assignedBy,
|
|
'completedWork': completedWork,
|
|
'plannedWork': plannedWork,
|
|
'assignedOn': assignedOn,
|
|
'location': location,
|
|
'teamSize': task.teamMembers.length,
|
|
'teamMembers': teamMembers,
|
|
'taskComments': taskComments
|
|
},
|
|
);
|
|
},
|
|
style: ElevatedButton.styleFrom(
|
|
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 6),
|
|
minimumSize: const Size(60, 20),
|
|
textStyle: const TextStyle(fontSize: 12),
|
|
),
|
|
child: const Text("Comment"),
|
|
),
|
|
],
|
|
)),
|
|
]);
|
|
}
|
|
|
|
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)),
|
|
),
|
|
);
|
|
}
|
|
}
|