marco.pms.mobileapp/lib/view/taskPlaning/daily_progress.dart
Vaibhav Surve 34100a4d9e -- Enhance layout with floating action button and navigation improvements
- 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.
2025-05-28 17:35:42 +05:30

603 lines
27 KiB
Dart

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_breadcrumb.dart';
import 'package:marco/helpers/widgets/my_breadcrumb_item.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/view/layouts/layout.dart';
import 'package:marco/controller/permission_controller.dart';
import 'package:marco/controller/dashboard/daily_task_controller.dart';
import 'package:marco/model/dailyTaskPlaning/daily_progress_report_filter.dart';
import 'package:marco/helpers/widgets/avatar.dart';
import 'package:marco/model/dailyTaskPlaning/comment_task_bottom_sheet.dart';
import 'package:marco/model/dailyTaskPlaning/report_task_bottom_sheet.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());
@override
Widget build(BuildContext context) {
return Layout(
child: GetBuilder<DailyTaskController>(
init: dailyTaskController,
tag: 'daily_progress_report_controller',
builder: (controller) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
MySpacing.height(flexSpacing),
_buildActionBar(),
Padding(
padding: MySpacing.x(flexSpacing),
child: _buildDailyProgressReportTab(),
),
],
);
},
),
);
}
Widget _buildHeader() {
return Padding(
padding: MySpacing.x(flexSpacing),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
MyText.titleMedium("Daily Progress Report",
fontSize: 18, fontWeight: 600),
MyBreadcrumb(
children: [
MyBreadcrumbItem(name: 'Dashboard'),
MyBreadcrumbItem(name: 'Daily Progress Report', active: true),
],
),
],
),
);
}
Widget _buildActionBar() {
return Padding(
padding: MySpacing.x(flexSpacing),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
_buildActionItem(
label: "Filter",
icon: Icons.filter_list_alt,
tooltip: 'Filter Project',
color: Colors.blueAccent,
onTap: _openFilterSheet,
),
const SizedBox(width: 8),
_buildActionItem(
label: "Refresh",
icon: Icons.refresh,
tooltip: 'Refresh Data',
color: Colors.green,
onTap: _refreshData,
),
],
),
);
}
Widget _buildActionItem({
required String label,
required IconData icon,
required String tooltip,
required VoidCallback onTap,
required Color color,
}) {
return Row(
children: [
MyText.bodyMedium(label, fontWeight: 600),
Tooltip(
message: tooltip,
child: InkWell(
borderRadius: BorderRadius.circular(24),
onTap: onTap,
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(icon, color: color, size: 28),
),
),
),
),
],
);
}
Future<void> _openFilterSheet() async {
final result = await showModalBottomSheet<Map<String, dynamic>>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.white,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
),
builder: (context) => DailyProgressReportFilter(
controller: dailyTaskController,
permissionController: permissionController,
),
);
if (result != null) {
final selectedProjectId = result['projectId'] as String?;
if (selectedProjectId != null &&
selectedProjectId != dailyTaskController.selectedProjectId) {
dailyTaskController.selectedProjectId = selectedProjectId;
try {
await dailyTaskController.fetchProjects();
} catch (e) {
debugPrint('Error fetching projects: $e');
}
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;
if (isLoading) {
return const Center(child: CircularProgressIndicator());
}
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));
return MyCard.bordered(
borderRadiusAll: 4,
border: Border.all(color: Colors.grey.withOpacity(0.2)),
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
paddingAll: 8,
child: ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: sortedDates.length,
separatorBuilder: (_, __) => Column(
children: [
const SizedBox(height: 12),
Divider(color: Colors.grey.withOpacity(0.3), thickness: 1),
const SizedBox(height: 12),
],
),
itemBuilder: (context, dateIndex) {
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 index = entry.key;
final activityName =
task.workItem?.activityMaster?.activityName ?? 'N/A';
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;
return Column(
children: [
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),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (task.reportedDate == null ||
task.reportedDate.toString().isEmpty)
OutlinedButton.icon(
icon: const Icon(Icons.report,
size: 18,
color: Colors.blueAccent),
label: const Text('Report',
style: TextStyle(
color: Colors.blueAccent)),
style: OutlinedButton.styleFrom(
side: const BorderSide(
color: Colors.blueAccent),
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 8),
textStyle:
const TextStyle(fontSize: 14),
),
onPressed: () {
final activityName = task
.workItem
?.activityMaster
?.activityName ??
'N/A';
final assigned =
'${(task.plannedTask - completed)}';
final assignedBy =
"${task.assignedBy.firstName} ${task.assignedBy.lastName ?? ''}";
final assignedOn =
DateFormat('dd-MM-yyyy').format(
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((e) => e.firstName)
.toList();
final taskData = {
'activity': activityName,
'assigned': assigned,
'taskId': taskId,
'assignedBy': assignedBy,
'completed': completed,
'assignedOn': assignedOn,
'location': location,
'teamSize':
task.teamMembers.length,
'teamMembers': teamMembers,
};
showModalBottomSheet(
context: context,
isScrollControlled: true,
shape:
const RoundedRectangleBorder(
borderRadius:
BorderRadius.vertical(
top: Radius.circular(
16)),
),
builder: (_) => Padding(
padding: MediaQuery.of(context)
.viewInsets,
child: ReportTaskBottomSheet(
taskData: taskData),
),
);
},
),
const SizedBox(width: 8),
OutlinedButton.icon(
icon: const Icon(Icons.comment,
size: 18, color: Colors.blueAccent),
label: const Text('Comment',
style: TextStyle(
color: Colors.blueAccent)),
style: OutlinedButton.styleFrom(
side: const BorderSide(
color: Colors.blueAccent),
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 8),
textStyle:
const TextStyle(fontSize: 14),
),
onPressed: () {
final activityName = task
.workItem
?.activityMaster
?.activityName ??
'N/A';
final plannedTask = task.plannedTask;
final completed = task.completedTask;
final assigned =
'${(plannedTask - completed)}';
final plannedWork =
plannedTask.toString();
final completedWork =
completed.toString();
final assignedBy =
"${task.assignedBy.firstName} ${task.assignedBy.lastName ?? ''}";
final assignedOn =
DateFormat('yyyy-MM-dd')
.format(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((e) =>
'${e.firstName} ${e.lastName}')
.toList();
final taskComments =
task.comments.map((comment) {
final isoDate = comment.timestamp
.toIso8601String();
final commenterName = comment
.commentedBy
.firstName
.isNotEmpty
? "${comment.commentedBy.firstName} ${comment.commentedBy.lastName ?? ''}"
.trim()
: "Unknown";
return {
'text': comment.comment,
'date': isoDate,
'commentedBy': commenterName,
};
}).toList();
final taskData = {
'activity': activityName,
'assigned': assigned,
'taskId': taskId,
'assignedBy': assignedBy,
'completedWork': completedWork,
'plannedWork': plannedWork,
'assignedOn': assignedOn,
'location': location,
'teamSize': task.teamMembers.length,
'teamMembers': teamMembers,
'taskComments': taskComments,
};
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (_) =>
CommentTaskBottomSheet(
taskData: taskData,
onCommentSuccess: () {
_refreshData();
Navigator.of(context).pop();
},
),
);
},
),
],
)
],
),
),
),
if (index != tasksForDate.length - 1)
Divider(
color: Colors.grey.withOpacity(0.2),
thickness: 1,
height: 1),
],
);
}).toList(),
);
})
],
);
},
),
);
});
}
}