522 lines
23 KiB
Dart
522 lines
23 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_card.dart';
|
|
import 'package:marco/helpers/widgets/my_spacing.dart';
|
|
import 'package:marco/helpers/widgets/my_text.dart';
|
|
import 'package:marco/controller/permission_controller.dart';
|
|
import 'package:marco/controller/task_planing/daily_task_planing_controller.dart';
|
|
import 'package:marco/controller/project_controller.dart';
|
|
import 'package:percent_indicator/percent_indicator.dart';
|
|
import 'package:marco/model/dailyTaskPlaning/assign_task_bottom_sheet .dart';
|
|
import 'package:marco/helpers/widgets/my_custom_skeleton.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());
|
|
final ProjectController projectController = Get.find<ProjectController>();
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
|
|
// Initial fetch if a project is already selected
|
|
final projectId = projectController.selectedProjectId.value;
|
|
if (projectId.isNotEmpty) {
|
|
dailyTaskPlaningController.fetchTaskData(projectId);
|
|
}
|
|
|
|
// Reactive fetch on project ID change
|
|
ever<String>(
|
|
projectController.selectedProjectId,
|
|
(newProjectId) {
|
|
if (newProjectId.isNotEmpty) {
|
|
dailyTaskPlaningController.fetchTaskData(newProjectId);
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: PreferredSize(
|
|
preferredSize: const Size.fromHeight(72),
|
|
child: AppBar(
|
|
backgroundColor: const Color(0xFFF5F5F5),
|
|
elevation: 0.5,
|
|
automaticallyImplyLeading: false,
|
|
titleSpacing: 0,
|
|
title: Padding(
|
|
padding: MySpacing.xy(16, 0),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
IconButton(
|
|
icon: const Icon(Icons.arrow_back_ios_new,
|
|
color: Colors.black, size: 20),
|
|
onPressed: () => Get.offNamed('/dashboard'),
|
|
),
|
|
MySpacing.width(8),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
MyText.titleLarge(
|
|
'Daily Task Planing',
|
|
fontWeight: 700,
|
|
color: Colors.black,
|
|
),
|
|
MySpacing.height(2),
|
|
GetBuilder<ProjectController>(
|
|
builder: (projectController) {
|
|
final projectName =
|
|
projectController.selectedProject?.name ??
|
|
'Select Project';
|
|
return Row(
|
|
children: [
|
|
const Icon(Icons.work_outline,
|
|
size: 14, color: Colors.grey),
|
|
MySpacing.width(4),
|
|
Expanded(
|
|
child: MyText.bodySmall(
|
|
projectName,
|
|
fontWeight: 600,
|
|
overflow: TextOverflow.ellipsis,
|
|
color: Colors.grey[700],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
body: SafeArea(
|
|
child: SingleChildScrollView(
|
|
padding: MySpacing.x(0),
|
|
child: GetBuilder<DailyTaskPlaningController>(
|
|
init: dailyTaskPlaningController,
|
|
tag: 'daily_task_planing_controller',
|
|
builder: (controller) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
MySpacing.height(flexSpacing),
|
|
Padding(
|
|
padding: MySpacing.x(flexSpacing),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
|
children: [
|
|
const SizedBox(width: 8),
|
|
MyText.bodyMedium("Refresh", fontWeight: 600),
|
|
Tooltip(
|
|
message: 'Refresh Data',
|
|
child: InkWell(
|
|
borderRadius: BorderRadius.circular(24),
|
|
onTap: () async {
|
|
final projectId =
|
|
projectController.selectedProjectId.value;
|
|
if (projectId.isNotEmpty) {
|
|
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 SkeletonLoaders.dailyProgressPlanningSkeletonCollapsedOnly();
|
|
}
|
|
|
|
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;
|
|
final totalPlanned = area.workItems
|
|
.map((wi) => wi.workItem.plannedWork ?? 0)
|
|
.fold<double>(0, (prev, curr) => prev + curr);
|
|
final totalCompleted = area.workItems
|
|
.map((wi) => wi.workItem.completedWork ?? 0)
|
|
.fold<double>(0, (prev, curr) => prev + curr);
|
|
final totalProgress = totalPlanned == 0
|
|
? 0.0
|
|
: (totalCompleted / totalPlanned).clamp(0.0, 1.0);
|
|
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: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
Expanded(
|
|
flex: 3,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
MyText.titleSmall(
|
|
"Floor: ${floor.floorName}",
|
|
fontWeight: 600,
|
|
color: Colors.teal,
|
|
maxLines: null,
|
|
overflow: TextOverflow.visible,
|
|
softWrap: true,
|
|
),
|
|
MySpacing.height(4),
|
|
MyText.titleSmall(
|
|
"Work Area: ${area.areaName}",
|
|
fontWeight: 600,
|
|
color: Colors.blueGrey,
|
|
maxLines: null,
|
|
overflow: TextOverflow.visible,
|
|
softWrap: true,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
MySpacing.width(12),
|
|
CircularPercentIndicator(
|
|
radius: 20.0,
|
|
lineWidth: 4.0,
|
|
animation: true,
|
|
percent: totalProgress,
|
|
center: Text(
|
|
"${(totalProgress * 100).toStringAsFixed(0)}%",
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 10.0,
|
|
),
|
|
),
|
|
circularStrokeCap: CircularStrokeCap.round,
|
|
progressColor: totalProgress >= 1.0
|
|
? Colors.green
|
|
: (totalProgress >= 0.5
|
|
? Colors.amber
|
|
: Colors.red),
|
|
backgroundColor: Colors.grey[300]!,
|
|
),
|
|
],
|
|
),
|
|
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)
|
|
.toInt();
|
|
|
|
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(),
|
|
);
|
|
});
|
|
});
|
|
}
|
|
}
|