feat: Implement lazy loading for building infrastructure and enhance task fetching logic

This commit is contained in:
Vaibhav Surve 2025-11-04 17:07:54 +05:30
parent 87520d7664
commit 99166801da
2 changed files with 360 additions and 292 deletions

View File

@ -23,6 +23,10 @@ class DailyTaskPlanningController extends GetxController {
RxBool isFetchingProjects = true.obs; RxBool isFetchingProjects = true.obs;
RxBool isFetchingEmployees = true.obs; RxBool isFetchingEmployees = true.obs;
/// New: track per-building loading and loaded state for lazy infra loading
RxMap<String, RxBool> buildingLoadingStates = <String, RxBool>{}.obs;
final Set<String> buildingsWithDetails = <String>{};
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
@ -106,7 +110,7 @@ class DailyTaskPlanningController extends GetxController {
} }
} }
/// Fetch Infra details and then tasks per work area /// Fetch buildings list only (no deep area/workItem calls) for initial load.
Future<void> fetchTaskData(String? projectId, {String? serviceId}) async { Future<void> fetchTaskData(String? projectId, {String? serviceId}) async {
if (projectId == null) return; if (projectId == null) return;
@ -123,24 +127,13 @@ class DailyTaskPlanningController extends GetxController {
return; return;
} }
// Build only building-level data (floors empty). This enables lazy loading later.
dailyTasks = infraData.map((buildingJson) { dailyTasks = infraData.map((buildingJson) {
final building = Building( final building = Building(
id: buildingJson['id'], id: buildingJson['id'],
name: buildingJson['buildingName'], name: buildingJson['buildingName'],
description: buildingJson['description'], description: buildingJson['description'],
floors: (buildingJson['floors'] as List<dynamic>) floors: [], // don't populate floors here - lazy load per-building
.map((floorJson) => Floor(
id: floorJson['id'],
floorName: floorJson['floorName'],
workAreas: (floorJson['workAreas'] as List<dynamic>)
.map((areaJson) => WorkArea(
id: areaJson['id'],
areaName: areaJson['areaName'],
workItems: [],
))
.toList(),
))
.toList(),
); );
return TaskPlanningDetailsModel( return TaskPlanningDetailsModel(
id: building.id, id: building.id,
@ -154,11 +147,65 @@ class DailyTaskPlanningController extends GetxController {
); );
}).toList(); }).toList();
await Future.wait(dailyTasks // Reset building loaded/loading maps because we replaced the list
.expand((task) => task.buildings) buildingLoadingStates.clear();
.expand((b) => b.floors) buildingsWithDetails.clear();
.expand((f) => f.workAreas) } catch (e, stack) {
.map((area) async { logSafe("Error fetching daily task data",
level: LogLevel.error, error: e, stackTrace: stack);
} finally {
isFetchingTasks.value = false;
update();
}
}
/// Fetch full infra for a single building (floors, workAreas, workItems).
/// Called lazily when user expands a building in the UI.
Future<void> fetchBuildingInfra(String buildingId, String projectId, String? serviceId) async {
if (buildingId.isEmpty) return;
// mark loading
buildingLoadingStates.putIfAbsent(buildingId, () => true.obs);
buildingLoadingStates[buildingId]!.value = true;
update();
try {
// Re-use getInfraDetails and find the building entry for the requested buildingId
final infraResponse = await ApiService.getInfraDetails(projectId, serviceId: serviceId);
final infraData = infraResponse?['data'] as List<dynamic>? ?? [];
final buildingJson = infraData.firstWhere(
(b) => b['id'].toString() == buildingId.toString(),
orElse: () => null,
);
if (buildingJson == null) {
logSafe("Building $buildingId not found in infra response", level: LogLevel.warning);
return;
}
// Build floors & workAreas for this building
final building = Building(
id: buildingJson['id'],
name: buildingJson['buildingName'],
description: buildingJson['description'],
floors: (buildingJson['floors'] as List<dynamic>? ?? []).map((floorJson) {
return Floor(
id: floorJson['id'],
floorName: floorJson['floorName'],
workAreas: (floorJson['workAreas'] as List<dynamic>? ?? []).map((areaJson) {
return WorkArea(
id: areaJson['id'],
areaName: areaJson['areaName'],
workItems: [], // will populate below
);
}).toList(),
);
}).toList(),
);
// For each workArea, fetch its work items and populate
await Future.wait(building.floors.expand((f) => f.workAreas).map((area) async {
try { try {
final taskResponse = final taskResponse =
await ApiService.getWorkItemsByWorkArea(area.id, serviceId: serviceId); await ApiService.getWorkItemsByWorkArea(area.id, serviceId: serviceId);
@ -177,9 +224,7 @@ class DailyTaskPlanningController extends GetxController {
completedWork: (taskJson['completedWork'] as num?)?.toDouble(), completedWork: (taskJson['completedWork'] as num?)?.toDouble(),
todaysAssigned: (taskJson['todaysAssigned'] as num?)?.toDouble(), todaysAssigned: (taskJson['todaysAssigned'] as num?)?.toDouble(),
description: taskJson['description'] as String?, description: taskJson['description'] as String?,
taskDate: taskJson['taskDate'] != null taskDate: taskJson['taskDate'] != null ? DateTime.tryParse(taskJson['taskDate']) : null,
? DateTime.tryParse(taskJson['taskDate'])
: null,
), ),
))); )));
} catch (e, stack) { } catch (e, stack) {
@ -187,11 +232,39 @@ class DailyTaskPlanningController extends GetxController {
level: LogLevel.error, error: e, stackTrace: stack); level: LogLevel.error, error: e, stackTrace: stack);
} }
})); }));
// Merge/replace the building into dailyTasks
bool merged = false;
for (var t in dailyTasks) {
final idx = t.buildings.indexWhere((b) => b.id.toString() == building.id.toString());
if (idx != -1) {
t.buildings[idx] = building;
merged = true;
break;
}
}
if (!merged) {
// If not present, add a new TaskPlanningDetailsModel wrapper (fallback)
dailyTasks.add(TaskPlanningDetailsModel(
id: building.id,
name: building.name,
projectAddress: "",
contactPerson: "",
startDate: DateTime.now(),
endDate: DateTime.now(),
projectStatusId: "",
buildings: [building],
));
}
// Mark as loaded
buildingsWithDetails.add(buildingId.toString());
} catch (e, stack) { } catch (e, stack) {
logSafe("Error fetching daily task data", logSafe("Error fetching infra for building $buildingId",
level: LogLevel.error, error: e, stackTrace: stack); level: LogLevel.error, error: e, stackTrace: stack);
} finally { } finally {
isFetchingTasks.value = false; buildingLoadingStates.putIfAbsent(buildingId, () => false.obs);
buildingLoadingStates[buildingId]!.value = false;
update(); update();
} }
} }

View File

@ -39,11 +39,12 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
final projectId = projectController.selectedProjectId.value; final projectId = projectController.selectedProjectId.value;
if (projectId.isNotEmpty) { if (projectId.isNotEmpty) {
// Now this will fetch only services + building list (no deep infra)
dailyTaskPlanningController.fetchTaskData(projectId); dailyTaskPlanningController.fetchTaskData(projectId);
serviceController.fetchServices(projectId); serviceController.fetchServices(projectId);
} }
// Whenever project changes, fetch tasks & services // Whenever project changes, fetch buildings & services (still lazy load infra per building)
ever<String>( ever<String>(
projectController.selectedProjectId, projectController.selectedProjectId,
(newProjectId) { (newProjectId) {
@ -87,15 +88,12 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
color: Colors.black, color: Colors.black,
), ),
MySpacing.height(2), MySpacing.height(2),
GetBuilder<ProjectController>( GetBuilder<ProjectController>(builder: (projectController) {
builder: (projectController) {
final projectName = final projectName =
projectController.selectedProject?.name ?? projectController.selectedProject?.name ?? 'Select Project';
'Select Project';
return Row( return Row(
children: [ children: [
const Icon(Icons.work_outline, const Icon(Icons.work_outline, size: 14, color: Colors.grey),
size: 14, color: Colors.grey),
MySpacing.width(4), MySpacing.width(4),
Expanded( Expanded(
child: MyText.bodySmall( child: MyText.bodySmall(
@ -107,8 +105,7 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
), ),
], ],
); );
}, }),
),
], ],
), ),
), ),
@ -123,6 +120,7 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
final projectId = projectController.selectedProjectId.value; final projectId = projectController.selectedProjectId.value;
if (projectId.isNotEmpty) { if (projectId.isNotEmpty) {
try { try {
// keep previous behavior but now fetchTaskData is lighter (buildings only)
await dailyTaskPlanningController.fetchTaskData( await dailyTaskPlanningController.fetchTaskData(
projectId, projectId,
serviceId: serviceController.selectedService?.id, serviceId: serviceController.selectedService?.id,
@ -221,14 +219,10 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
} }
return StatefulBuilder(builder: (context, setMainState) { return StatefulBuilder(builder: (context, setMainState) {
final filteredBuildings = dailyTasks.expand((task) { // Show all buildings (they may or may not have floors loaded yet)
return task.buildings.where((building) { final buildings = dailyTasks.expand((task) => task.buildings).toList();
return building.floors.any((floor) =>
floor.workAreas.any((area) => area.workItems.isNotEmpty));
});
}).toList();
if (filteredBuildings.isEmpty) { if (buildings.isEmpty) {
return Center( return Center(
child: MyText.bodySmall( child: MyText.bodySmall(
"No Progress Report Found", "No Progress Report Found",
@ -239,26 +233,39 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: filteredBuildings.map((building) { children: buildings.map((building) {
final buildingKey = building.id.toString(); 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( return MyCard.bordered(
borderRadiusAll: 10, borderRadiusAll: 10,
paddingAll: 0, paddingAll: 0,
margin: MySpacing.bottom(10), margin: MySpacing.bottom(10),
child: Theme( child: Theme(
data: Theme.of(context) data: Theme.of(context).copyWith(dividerColor: Colors.transparent),
.copyWith(dividerColor: Colors.transparent),
child: ExpansionTile( child: ExpansionTile(
onExpansionChanged: (expanded) { onExpansionChanged: (expanded) async {
setMainState(() { setMainState(() {
buildingExpansionState[buildingKey] = expanded; 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( trailing: buildExpandIcon(isBuildingExpanded),
buildingExpansionState[buildingKey] ?? false), tilePadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 0),
tilePadding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 0),
collapsedShape: RoundedRectangleBorder( collapsedShape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
@ -283,25 +290,41 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
childrenPadding: childrenPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 0),
const EdgeInsets.symmetric(horizontal: 16, vertical: 0), children: [
if (buildingLoading)
Padding(
padding: const EdgeInsets.all(16.0),
child: Center(
child: SizedBox(
height: 24,
width: 24,
child: CircularProgressIndicator(strokeWidth: 2),
),
),
)
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) { children: building.floors.expand((floor) {
final validWorkAreas = floor.workAreas final validWorkAreas = floor.workAreas.where((area) => area.workItems.isNotEmpty);
.where((area) => area.workItems.isNotEmpty);
return validWorkAreas.map((area) { return validWorkAreas.map((area) {
final floorWorkAreaKey = final floorWorkAreaKey =
"${buildingKey}_${floor.floorName}_${area.areaName}"; "${buildingKey}_${floor.floorName}_${area.areaName}";
final isExpanded = final isExpanded = floorExpansionState[floorWorkAreaKey] ?? false;
floorExpansionState[floorWorkAreaKey] ?? false;
final workItems = area.workItems; final workItems = area.workItems;
final totalPlanned = workItems.fold<double>( final totalPlanned = workItems.fold<double>(0, (sum, wi) => sum + (wi.workItem.plannedWork ?? 0));
0, (sum, wi) => sum + (wi.workItem.plannedWork ?? 0)); final totalCompleted = workItems.fold<double>(0, (sum, wi) => sum + (wi.workItem.completedWork ?? 0));
final totalCompleted = workItems.fold<double>(0, final totalProgress = totalPlanned == 0 ? 0.0 : (totalCompleted / totalPlanned).clamp(0.0, 1.0);
(sum, wi) => sum + (wi.workItem.completedWork ?? 0));
final totalProgress = totalPlanned == 0
? 0.0
: (totalCompleted / totalPlanned).clamp(0.0, 1.0);
return ExpansionTile( return ExpansionTile(
onExpansionChanged: (expanded) { onExpansionChanged: (expanded) {
@ -310,14 +333,11 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
}); });
}, },
trailing: Icon( trailing: Icon(
isExpanded isExpanded ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down,
? Icons.keyboard_arrow_up
: Icons.keyboard_arrow_down,
size: 28, size: 28,
color: Colors.black54, color: Colors.black54,
), ),
tilePadding: const EdgeInsets.symmetric( tilePadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 0),
horizontal: 16, vertical: 0),
title: Row( title: Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
@ -362,22 +382,17 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
circularStrokeCap: CircularStrokeCap.round, circularStrokeCap: CircularStrokeCap.round,
progressColor: totalProgress >= 1.0 progressColor: totalProgress >= 1.0
? Colors.green ? Colors.green
: (totalProgress >= 0.5 : (totalProgress >= 0.5 ? Colors.amber : Colors.red),
? Colors.amber
: Colors.red),
backgroundColor: Colors.grey[300]!, backgroundColor: Colors.grey[300]!,
), ),
], ],
), ),
childrenPadding: const EdgeInsets.only( childrenPadding: const EdgeInsets.only(left: 16, right: 0, bottom: 8),
left: 16, right: 0, bottom: 8),
children: area.workItems.map((wItem) { children: area.workItems.map((wItem) {
final item = wItem.workItem; final item = wItem.workItem;
final completed = item.completedWork ?? 0; final completed = item.completedWork ?? 0;
final planned = item.plannedWork ?? 0; final planned = item.plannedWork ?? 0;
final progress = (planned == 0) final progress = (planned == 0) ? 0.0 : (completed / planned).clamp(0.0, 1.0);
? 0.0
: (completed / planned).clamp(0.0, 1.0);
return Padding( return Padding(
padding: const EdgeInsets.symmetric(vertical: 8), padding: const EdgeInsets.symmetric(vertical: 8),
@ -389,8 +404,7 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
children: [ children: [
Expanded( Expanded(
child: MyText.bodyMedium( child: MyText.bodyMedium(
item.activityMaster?.name ?? item.activityMaster?.name ?? "No Activity",
"No Activity",
fontWeight: 600, fontWeight: 600,
maxLines: 2, maxLines: 2,
overflow: TextOverflow.visible, overflow: TextOverflow.visible,
@ -400,12 +414,10 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
MySpacing.width(8), MySpacing.width(8),
if (item.workCategoryMaster?.name != null) if (item.workCategoryMaster?.name != null)
Container( Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
horizontal: 12, vertical: 4),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.blue.shade100, color: Colors.blue.shade100,
borderRadius: borderRadius: BorderRadius.circular(20),
BorderRadius.circular(20),
), ),
child: MyText.bodySmall( child: MyText.bodySmall(
item.workCategoryMaster!.name!, item.workCategoryMaster!.name!,
@ -422,52 +434,40 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
Expanded( Expanded(
flex: 3, flex: 3,
child: Column( child: Column(
crossAxisAlignment: crossAxisAlignment: CrossAxisAlignment.start,
CrossAxisAlignment.start,
children: [ children: [
MySpacing.height(8), MySpacing.height(8),
MyText.bodySmall( MyText.bodySmall(
"Completed: $completed / $planned", "Completed: $completed / $planned",
fontWeight: 600, fontWeight: 600,
color: const Color.fromARGB( color: const Color.fromARGB(221, 0, 0, 0),
221, 0, 0, 0),
), ),
], ],
), ),
), ),
MySpacing.width(16), MySpacing.width(16),
if (progress < 1.0 && if (progress < 1.0 &&
permissionController.hasPermission( permissionController.hasPermission(Permissions.assignReportTask))
Permissions.assignReportTask))
IconButton( IconButton(
icon: const Icon( icon: const Icon(
Icons.person_add_alt_1_rounded, Icons.person_add_alt_1_rounded,
color: color: Color.fromARGB(255, 46, 161, 233),
Color.fromARGB(255, 46, 161, 233),
), ),
onPressed: () { onPressed: () {
final pendingTask = final pendingTask = (planned - completed).clamp(0, planned).toInt();
(planned - completed)
.clamp(0, planned)
.toInt();
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
BorderRadius.vertical(
top: Radius.circular(16)),
), ),
builder: (context) => builder: (context) => AssignTaskBottomSheet(
AssignTaskBottomSheet(
buildingName: building.name, buildingName: building.name,
floorName: floor.floorName, floorName: floor.floorName,
workAreaName: area.areaName, workAreaName: area.areaName,
workLocation: area.areaName, workLocation: area.areaName,
activityName: activityName: item.activityMaster?.name ?? "Unknown Activity",
item.activityMaster?.name ??
"Unknown Activity",
pendingTask: pendingTask, pendingTask: pendingTask,
workItemId: item.id.toString(), workItemId: item.id.toString(),
assignmentDate: DateTime.now(), assignmentDate: DateTime.now(),
@ -477,7 +477,6 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
), ),
], ],
), ),
MySpacing.height(6), MySpacing.height(6),
Row( Row(
children: [ children: [
@ -487,7 +486,6 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
), ),
], ],
), ),
MySpacing.height(16), MySpacing.height(16),
Stack( Stack(
children: [ children: [
@ -505,11 +503,8 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
decoration: BoxDecoration( decoration: BoxDecoration(
color: progress >= 1.0 color: progress >= 1.0
? Colors.green ? Colors.green
: (progress >= 0.5 : (progress >= 0.5 ? Colors.amber : Colors.red),
? Colors.amber borderRadius: BorderRadius.circular(6),
: Colors.red),
borderRadius:
BorderRadius.circular(6),
), ),
), ),
), ),
@ -521,9 +516,7 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
fontWeight: 500, fontWeight: 500,
color: progress >= 1.0 color: progress >= 1.0
? Colors.green[700] ? Colors.green[700]
: (progress >= 0.5 : (progress >= 0.5 ? Colors.amber[800] : Colors.red[700]),
? Colors.amber[800]
: Colors.red[700]),
), ),
], ],
), ),
@ -532,6 +525,8 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
); );
}).toList(); }).toList();
}).toList(), }).toList(),
)
],
), ),
), ),
); );