- 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.
486 lines
21 KiB
Dart
486 lines
21 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/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_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/model/dailyTaskPlaning/daily_task_planing_filter.dart';
|
|
import 'package:marco/controller/task_planing/daily_task_planing_controller.dart';
|
|
import 'package:marco/model/dailyTaskPlaning/assign_task_bottom_sheet .dart';
|
|
|
|
class DailyTaskPlaningScreen extends StatefulWidget {
|
|
DailyTaskPlaningScreen({super.key});
|
|
|
|
@override
|
|
State<DailyTaskPlaningScreen> createState() => _DailyTaskPlaningScreenState();
|
|
}
|
|
|
|
class _DailyTaskPlaningScreenState extends State<DailyTaskPlaningScreen>
|
|
with UIMixin {
|
|
final DailyTaskPlaningController dailyTaskPlaningController =
|
|
Get.put(DailyTaskPlaningController());
|
|
final PermissionController permissionController =
|
|
Get.put(PermissionController());
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Layout(
|
|
child: GetBuilder<DailyTaskPlaningController>(
|
|
init: dailyTaskPlaningController,
|
|
tag: 'daily_task_planing_controller',
|
|
builder: (controller) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Padding(
|
|
padding: MySpacing.x(flexSpacing),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
MyText.titleMedium("Daily Task Planning",
|
|
fontSize: 18, fontWeight: 600),
|
|
MyBreadcrumb(
|
|
children: [
|
|
MyBreadcrumbItem(name: 'Dashboard'),
|
|
MyBreadcrumbItem(
|
|
name: 'Daily Task Planning', active: true),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
MySpacing.height(flexSpacing),
|
|
Padding(
|
|
padding: MySpacing.x(flexSpacing),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
|
children: [
|
|
MyText.bodyMedium(
|
|
"Filter",
|
|
fontWeight: 600,
|
|
),
|
|
Tooltip(
|
|
message: 'Project',
|
|
child: InkWell(
|
|
borderRadius: BorderRadius.circular(24),
|
|
onTap: () 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) => DailyTaskPlaningFilter(
|
|
controller: dailyTaskPlaningController,
|
|
permissionController: permissionController,
|
|
),
|
|
);
|
|
|
|
if (result != null) {
|
|
final selectedProjectId =
|
|
result['projectId'] as String?;
|
|
|
|
if (selectedProjectId != null &&
|
|
selectedProjectId !=
|
|
dailyTaskPlaningController
|
|
.selectedProjectId) {
|
|
// Update the controller's selected project ID
|
|
dailyTaskPlaningController.selectedProjectId =
|
|
selectedProjectId;
|
|
|
|
try {
|
|
// Fetch tasks for the new project
|
|
await dailyTaskPlaningController
|
|
.fetchTaskData(selectedProjectId);
|
|
} catch (e) {
|
|
debugPrint(
|
|
'Error fetching task data: ${e.toString()}');
|
|
}
|
|
|
|
// Update the UI
|
|
dailyTaskPlaningController
|
|
.update(['daily_task_planing_controller']);
|
|
}
|
|
}
|
|
},
|
|
child: MouseRegion(
|
|
cursor: SystemMouseCursors.click,
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: Icon(
|
|
Icons.filter_list_alt,
|
|
color: Colors.blueAccent,
|
|
size: 28,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
MyText.bodyMedium(
|
|
"Refresh",
|
|
fontWeight: 600,
|
|
),
|
|
Tooltip(
|
|
message: 'Refresh Data',
|
|
child: InkWell(
|
|
borderRadius: BorderRadius.circular(24),
|
|
onTap: () async {
|
|
final projectId =
|
|
dailyTaskPlaningController.selectedProjectId;
|
|
if (projectId != null) {
|
|
try {
|
|
await dailyTaskPlaningController
|
|
.fetchTaskData(projectId);
|
|
} catch (e) {
|
|
debugPrint(
|
|
'Error refreshing task data: ${e.toString()}');
|
|
}
|
|
}
|
|
},
|
|
child: MouseRegion(
|
|
cursor: SystemMouseCursors.click,
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: Icon(
|
|
Icons.refresh,
|
|
color: Colors.green,
|
|
size: 28,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Padding(
|
|
padding: MySpacing.x(flexSpacing),
|
|
child: dailyProgressReportTab(),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget dailyProgressReportTab() {
|
|
return Obx(() {
|
|
final isLoading = dailyTaskPlaningController.isLoading.value;
|
|
final dailyTasks = dailyTaskPlaningController.dailyTasks;
|
|
|
|
if (isLoading) {
|
|
return Center(child: CircularProgressIndicator());
|
|
}
|
|
|
|
if (dailyTasks.isEmpty) {
|
|
return Center(
|
|
child: MyText.bodySmall(
|
|
"No Progress Report Found",
|
|
fontWeight: 600,
|
|
),
|
|
);
|
|
}
|
|
|
|
final buildingExpansionState = <String, bool>{};
|
|
final floorExpansionState = <String, bool>{};
|
|
|
|
Widget buildExpandIcon(bool isExpanded) {
|
|
return Container(
|
|
width: 32,
|
|
height: 32,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
color: Colors.grey.shade200,
|
|
),
|
|
child: Icon(
|
|
isExpanded ? Icons.remove : Icons.add,
|
|
size: 20,
|
|
color: Colors.black87,
|
|
),
|
|
);
|
|
}
|
|
|
|
return StatefulBuilder(builder: (context, setMainState) {
|
|
final filteredBuildings = dailyTasks.expand((task) {
|
|
return task.buildings.where((building) {
|
|
return building.floors.any((floor) =>
|
|
floor.workAreas.any((area) => area.workItems.isNotEmpty));
|
|
});
|
|
}).toList();
|
|
|
|
if (filteredBuildings.isEmpty) {
|
|
return Center(
|
|
child: MyText.bodySmall(
|
|
"No Progress Report Found",
|
|
fontWeight: 600,
|
|
),
|
|
);
|
|
}
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: filteredBuildings.map((building) {
|
|
final buildingKey = building.id.toString();
|
|
|
|
return MyCard.bordered(
|
|
borderRadiusAll: 12,
|
|
paddingAll: 0,
|
|
margin: MySpacing.bottom(12),
|
|
shadow: MyShadow(elevation: 3),
|
|
child: Theme(
|
|
data: Theme.of(context)
|
|
.copyWith(dividerColor: Colors.transparent),
|
|
child: ExpansionTile(
|
|
onExpansionChanged: (expanded) {
|
|
setMainState(() {
|
|
buildingExpansionState[buildingKey] = expanded;
|
|
});
|
|
},
|
|
trailing: buildExpandIcon(
|
|
buildingExpansionState[buildingKey] ?? false),
|
|
tilePadding:
|
|
const EdgeInsets.symmetric(horizontal: 16, vertical: 0),
|
|
collapsedShape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
leading: Container(
|
|
decoration: BoxDecoration(
|
|
color: Colors.blueAccent.withOpacity(0.1),
|
|
shape: BoxShape.circle,
|
|
),
|
|
padding: const EdgeInsets.all(8),
|
|
child: Icon(
|
|
Icons.location_city_rounded,
|
|
color: Colors.blueAccent,
|
|
size: 24,
|
|
),
|
|
),
|
|
title: MyText.titleMedium(
|
|
building.name,
|
|
fontWeight: 700,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
childrenPadding:
|
|
const EdgeInsets.symmetric(horizontal: 16, vertical: 0),
|
|
children: building.floors.expand((floor) {
|
|
final validWorkAreas = floor.workAreas
|
|
.where((area) => area.workItems.isNotEmpty);
|
|
|
|
// For each valid work area, return a Floor+WorkArea ExpansionTile
|
|
return validWorkAreas.map((area) {
|
|
final floorWorkAreaKey =
|
|
"${buildingKey}_${floor.floorName}_${area.areaName}";
|
|
final isExpanded =
|
|
floorExpansionState[floorWorkAreaKey] ?? false;
|
|
|
|
return ExpansionTile(
|
|
onExpansionChanged: (expanded) {
|
|
setMainState(() {
|
|
floorExpansionState[floorWorkAreaKey] = expanded;
|
|
});
|
|
},
|
|
trailing: Icon(
|
|
isExpanded
|
|
? Icons.keyboard_arrow_up
|
|
: Icons.keyboard_arrow_down,
|
|
size: 28,
|
|
color: Colors.black54,
|
|
),
|
|
tilePadding: const EdgeInsets.symmetric(
|
|
horizontal: 16, vertical: 0),
|
|
title: Wrap(
|
|
spacing: 16,
|
|
crossAxisAlignment: WrapCrossAlignment.center,
|
|
children: [
|
|
MyText.titleSmall(
|
|
"Floor: ${floor.floorName}",
|
|
fontWeight: 600,
|
|
color: Colors.teal,
|
|
maxLines: null,
|
|
overflow: TextOverflow.visible,
|
|
softWrap: true,
|
|
),
|
|
MyText.titleSmall(
|
|
"Work Area: ${area.areaName}",
|
|
fontWeight: 600,
|
|
color: Colors.blueGrey,
|
|
maxLines: null,
|
|
overflow: TextOverflow.visible,
|
|
softWrap: true,
|
|
),
|
|
],
|
|
),
|
|
childrenPadding: const EdgeInsets.only(
|
|
left: 16, right: 0, bottom: 8),
|
|
children: area.workItems.map((wItem) {
|
|
final item = wItem.workItem;
|
|
final completed = item.completedWork ?? 0;
|
|
final planned = item.plannedWork ?? 0;
|
|
final progress = (planned == 0)
|
|
? 0.0
|
|
: (completed / planned).clamp(0.0, 1.0);
|
|
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Expanded(
|
|
child: MyText.bodyMedium(
|
|
item.activityMaster?.name ??
|
|
"No Activity",
|
|
fontWeight: 600,
|
|
maxLines: 2,
|
|
overflow: TextOverflow.visible,
|
|
softWrap: true,
|
|
),
|
|
),
|
|
MySpacing.width(8),
|
|
if (item.workCategoryMaster?.name != null)
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 12, vertical: 4),
|
|
decoration: BoxDecoration(
|
|
color: Colors.blue.shade100,
|
|
borderRadius:
|
|
BorderRadius.circular(20),
|
|
),
|
|
child: MyText.bodySmall(
|
|
item.workCategoryMaster!.name!,
|
|
fontWeight: 500,
|
|
color: Colors.blue.shade800,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
MySpacing.height(4),
|
|
Row(
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
Expanded(
|
|
flex: 3,
|
|
child: Column(
|
|
crossAxisAlignment:
|
|
CrossAxisAlignment.start,
|
|
children: [
|
|
MySpacing.height(8),
|
|
MyText.bodySmall(
|
|
"Completed: $completed / $planned",
|
|
fontWeight: 600,
|
|
color: const Color.fromARGB(
|
|
221, 0, 0, 0),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
MySpacing.width(16),
|
|
if (progress < 1.0)
|
|
IconButton(
|
|
icon: Icon(
|
|
Icons.person_add_alt_1_rounded,
|
|
color: const Color.fromARGB(
|
|
255, 46, 161, 233),
|
|
),
|
|
onPressed: () {
|
|
final pendingTask =
|
|
(planned - completed)
|
|
.clamp(0, planned);
|
|
|
|
showModalBottomSheet(
|
|
context: context,
|
|
isScrollControlled: true,
|
|
shape: const RoundedRectangleBorder(
|
|
borderRadius:
|
|
BorderRadius.vertical(
|
|
top: Radius.circular(16)),
|
|
),
|
|
builder: (context) =>
|
|
AssignTaskBottomSheet(
|
|
buildingName: building.name,
|
|
floorName: floor.floorName,
|
|
workAreaName: area.areaName,
|
|
workLocation: area.areaName,
|
|
activityName:
|
|
item.activityMaster?.name ??
|
|
"Unknown Activity",
|
|
pendingTask: pendingTask,
|
|
workItemId: item.id.toString(),
|
|
assignmentDate: DateTime.now(),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
MySpacing.height(8),
|
|
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),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
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]),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}).toList(),
|
|
);
|
|
}).toList();
|
|
}).toList(),
|
|
),
|
|
),
|
|
);
|
|
}).toList(),
|
|
);
|
|
});
|
|
});
|
|
}
|
|
}
|