marco.pms.mobileapp/lib/view/taskPlanning/daily_task_planning.dart

605 lines
28 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_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_Planning/daily_task_Planning_controller.dart';
import 'package:marco/controller/project_controller.dart';
import 'package:percent_indicator/percent_indicator.dart';
import 'package:marco/model/dailyTaskPlanning/assign_task_bottom_sheet .dart';
import 'package:marco/helpers/widgets/my_custom_skeleton.dart';
import 'package:marco/helpers/utils/permission_constants.dart';
import 'package:marco/helpers/widgets/my_refresh_indicator.dart';
import 'package:marco/controller/tenant/service_controller.dart';
import 'package:marco/helpers/widgets/tenant/service_selector.dart';
class DailyTaskPlanningScreen extends StatefulWidget {
DailyTaskPlanningScreen({super.key});
@override
State<DailyTaskPlanningScreen> createState() =>
_DailyTaskPlanningScreenState();
}
class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
with UIMixin {
final DailyTaskPlanningController dailyTaskPlanningController =
Get.put(DailyTaskPlanningController());
final PermissionController permissionController =
Get.put(PermissionController());
final ProjectController projectController = Get.find<ProjectController>();
final ServiceController serviceController = Get.put(ServiceController());
@override
void initState() {
super.initState();
final projectId = projectController.selectedProjectId.value;
if (projectId.isNotEmpty) {
// Now this will fetch only services + building list (no deep infra)
dailyTaskPlanningController.fetchTaskData(projectId);
serviceController.fetchServices(projectId);
}
// Whenever project changes, fetch buildings & services (still lazy load infra per building)
ever<String>(
projectController.selectedProjectId,
(newProjectId) {
if (newProjectId.isNotEmpty) {
dailyTaskPlanningController.fetchTaskData(newProjectId);
serviceController.fetchServices(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 Planning',
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: MyRefreshIndicator(
onRefresh: () async {
final projectId = projectController.selectedProjectId.value;
if (projectId.isNotEmpty) {
try {
// keep previous behavior but now fetchTaskData is lighter (buildings only)
await dailyTaskPlanningController.fetchTaskData(
projectId,
serviceId: serviceController.selectedService?.id,
);
} catch (e) {
debugPrint('Error refreshing task data: ${e.toString()}');
}
}
},
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
padding: MySpacing.x(0),
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: MediaQuery.of(context).size.height -
kToolbarHeight -
MediaQuery.of(context).padding.top,
),
child: GetBuilder<DailyTaskPlanningController>(
init: dailyTaskPlanningController,
tag: 'daily_task_Planning_controller',
builder: (controller) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MySpacing.height(flexSpacing),
Padding(
padding: MySpacing.x(10),
child: ServiceSelector(
controller: serviceController,
height: 40,
onSelectionChanged: (service) async {
final projectId =
projectController.selectedProjectId.value;
if (projectId.isNotEmpty) {
await dailyTaskPlanningController.fetchTaskData(
projectId,
serviceId:
service?.id, // <-- pass selected service
);
}
},
),
),
MySpacing.height(flexSpacing),
Padding(
padding: MySpacing.x(8),
child: dailyProgressReportTab(),
),
],
);
},
),
),
),
),
),
);
}
Widget dailyProgressReportTab() {
return Obx(() {
final isLoading = dailyTaskPlanningController.isFetchingTasks.value;
final dailyTasks = dailyTaskPlanningController.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) {
// Show all buildings (they may or may not have floors loaded yet)
final buildings = dailyTasks.expand((task) => task.buildings).toList();
if (buildings.isEmpty) {
return Center(
child: MyText.bodySmall(
"No Progress Report Found",
fontWeight: 600,
),
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: buildings.map((building) {
final buildingKey = building.id.toString();
final isBuildingExpanded =
buildingExpansionState[buildingKey] ?? false;
final buildingLoading = dailyTaskPlanningController
.buildingLoadingStates[buildingKey]?.value ??
false;
final buildingLoaded = dailyTaskPlanningController
.buildingsWithDetails
.contains(buildingKey);
return MyCard.bordered(
borderRadiusAll: 10,
paddingAll: 0,
margin: MySpacing.bottom(10),
child: Theme(
data: Theme.of(context)
.copyWith(dividerColor: Colors.transparent),
child: ExpansionTile(
onExpansionChanged: (expanded) async {
setMainState(() {
buildingExpansionState[buildingKey] = expanded;
});
if (expanded && !buildingLoaded && !buildingLoading) {
// fetch infra details for this building lazily
final projectId =
projectController.selectedProjectId.value;
if (projectId.isNotEmpty) {
await dailyTaskPlanningController.fetchBuildingInfra(
building.id.toString(),
projectId,
serviceController.selectedService?.id,
);
setMainState(() {}); // rebuild to reflect loaded data
}
}
},
trailing: buildExpandIcon(isBuildingExpanded),
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: 5, vertical: 0),
children: [
if (buildingLoading)
Padding(
padding: const EdgeInsets.all(0.0),
child: SkeletonLoaders
.dailyProgressPlanningInfraSkeleton(),
)
else if (!buildingLoaded || building.floors.isEmpty)
Padding(
padding: const EdgeInsets.all(16.0),
child: MyText.bodySmall(
"No Progress Report Found",
fontWeight: 600,
),
)
else
// Building is loaded and has floors; render floors -> areas -> items
Column(
children: building.floors.expand((floor) {
final validWorkAreas = floor.workAreas
.where((area) => area.workItems.isNotEmpty);
return validWorkAreas.map((area) {
final floorWorkAreaKey =
"${buildingKey}_${floor.floorName}_${area.areaName}";
final isExpanded =
floorExpansionState[floorWorkAreaKey] ?? false;
final workItems = area.workItems;
final totalPlanned = workItems.fold<double>(
0,
(sum, wi) =>
sum + (wi.workItem.plannedWork ?? 0));
final totalCompleted = workItems.fold<double>(
0,
(sum, wi) =>
sum + (wi.workItem.completedWork ?? 0));
final totalProgress = totalPlanned == 0
? 0.0
: (totalCompleted / totalPlanned)
.clamp(0.0, 1.0);
return ExpansionTile(
onExpansionChanged: (expanded) {
setMainState(() {
floorExpansionState[floorWorkAreaKey] =
expanded;
});
},
trailing: const SizedBox
.shrink(), // ← Add this to remove the default trailing icon
title: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Collapse/Expand icon on the LEFT
Icon(
isExpanded
? Icons.keyboard_arrow_up
: Icons.keyboard_arrow_down,
size: 28,
color: Colors.black54,
),
MySpacing.width(8),
// Floor + Work area text expanded to take available space
Expanded(
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,
),
],
),
),
CircularPercentIndicator(
radius: 20.0,
lineWidth: 4.0,
animation: true,
percent: totalProgress,
center: Text(
"${(totalProgress * 100).toStringAsFixed(0)}%",
style: const 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) {
// keep your existing work item UI as-is
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 &&
permissionController
.hasPermission(Permissions
.assignReportTask))
IconButton(
icon: const Icon(
Icons.person_add_alt_1_rounded,
color: 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(6),
Row(
children: [
MyText.bodySmall(
"Today's Planned: ${item.todaysAssigned ?? 0}",
fontWeight: 600,
),
],
),
MySpacing.height(16),
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]),
),
],
),
);
}).toList(),
);
}).toList();
}).toList(),
)
],
),
),
);
}).toList(),
);
});
});
}
}