feat: Implement lazy loading for building infrastructure and enhance task fetching logic
This commit is contained in:
parent
87520d7664
commit
99166801da
@ -23,6 +23,10 @@ class DailyTaskPlanningController extends GetxController {
|
||||
RxBool isFetchingProjects = 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
|
||||
void 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 {
|
||||
if (projectId == null) return;
|
||||
|
||||
@ -123,24 +127,13 @@ class DailyTaskPlanningController extends GetxController {
|
||||
return;
|
||||
}
|
||||
|
||||
// Build only building-level data (floors empty). This enables lazy loading later.
|
||||
dailyTasks = infraData.map((buildingJson) {
|
||||
final building = Building(
|
||||
id: buildingJson['id'],
|
||||
name: buildingJson['buildingName'],
|
||||
description: buildingJson['description'],
|
||||
floors: (buildingJson['floors'] as List<dynamic>)
|
||||
.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(),
|
||||
floors: [], // don't populate floors here - lazy load per-building
|
||||
);
|
||||
return TaskPlanningDetailsModel(
|
||||
id: building.id,
|
||||
@ -154,11 +147,65 @@ class DailyTaskPlanningController extends GetxController {
|
||||
);
|
||||
}).toList();
|
||||
|
||||
await Future.wait(dailyTasks
|
||||
.expand((task) => task.buildings)
|
||||
.expand((b) => b.floors)
|
||||
.expand((f) => f.workAreas)
|
||||
.map((area) async {
|
||||
// Reset building loaded/loading maps because we replaced the list
|
||||
buildingLoadingStates.clear();
|
||||
buildingsWithDetails.clear();
|
||||
} catch (e, stack) {
|
||||
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 {
|
||||
final taskResponse =
|
||||
await ApiService.getWorkItemsByWorkArea(area.id, serviceId: serviceId);
|
||||
@ -177,9 +224,7 @@ class DailyTaskPlanningController extends GetxController {
|
||||
completedWork: (taskJson['completedWork'] as num?)?.toDouble(),
|
||||
todaysAssigned: (taskJson['todaysAssigned'] as num?)?.toDouble(),
|
||||
description: taskJson['description'] as String?,
|
||||
taskDate: taskJson['taskDate'] != null
|
||||
? DateTime.tryParse(taskJson['taskDate'])
|
||||
: null,
|
||||
taskDate: taskJson['taskDate'] != null ? DateTime.tryParse(taskJson['taskDate']) : null,
|
||||
),
|
||||
)));
|
||||
} catch (e, stack) {
|
||||
@ -187,11 +232,39 @@ class DailyTaskPlanningController extends GetxController {
|
||||
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) {
|
||||
logSafe("Error fetching daily task data",
|
||||
logSafe("Error fetching infra for building $buildingId",
|
||||
level: LogLevel.error, error: e, stackTrace: stack);
|
||||
} finally {
|
||||
isFetchingTasks.value = false;
|
||||
buildingLoadingStates.putIfAbsent(buildingId, () => false.obs);
|
||||
buildingLoadingStates[buildingId]!.value = false;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,11 +39,12 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
|
||||
|
||||
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 tasks & services
|
||||
// Whenever project changes, fetch buildings & services (still lazy load infra per building)
|
||||
ever<String>(
|
||||
projectController.selectedProjectId,
|
||||
(newProjectId) {
|
||||
@ -87,15 +88,12 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
|
||||
color: Colors.black,
|
||||
),
|
||||
MySpacing.height(2),
|
||||
GetBuilder<ProjectController>(
|
||||
builder: (projectController) {
|
||||
GetBuilder<ProjectController>(builder: (projectController) {
|
||||
final projectName =
|
||||
projectController.selectedProject?.name ??
|
||||
'Select Project';
|
||||
projectController.selectedProject?.name ?? 'Select Project';
|
||||
return Row(
|
||||
children: [
|
||||
const Icon(Icons.work_outline,
|
||||
size: 14, color: Colors.grey),
|
||||
const Icon(Icons.work_outline, size: 14, color: Colors.grey),
|
||||
MySpacing.width(4),
|
||||
Expanded(
|
||||
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;
|
||||
if (projectId.isNotEmpty) {
|
||||
try {
|
||||
// keep previous behavior but now fetchTaskData is lighter (buildings only)
|
||||
await dailyTaskPlanningController.fetchTaskData(
|
||||
projectId,
|
||||
serviceId: serviceController.selectedService?.id,
|
||||
@ -221,14 +219,10 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
|
||||
}
|
||||
|
||||
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();
|
||||
// Show all buildings (they may or may not have floors loaded yet)
|
||||
final buildings = dailyTasks.expand((task) => task.buildings).toList();
|
||||
|
||||
if (filteredBuildings.isEmpty) {
|
||||
if (buildings.isEmpty) {
|
||||
return Center(
|
||||
child: MyText.bodySmall(
|
||||
"No Progress Report Found",
|
||||
@ -239,26 +233,39 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: filteredBuildings.map((building) {
|
||||
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),
|
||||
data: Theme.of(context).copyWith(dividerColor: Colors.transparent),
|
||||
child: ExpansionTile(
|
||||
onExpansionChanged: (expanded) {
|
||||
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(
|
||||
buildingExpansionState[buildingKey] ?? false),
|
||||
tilePadding:
|
||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 0),
|
||||
trailing: buildExpandIcon(isBuildingExpanded),
|
||||
tilePadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 0),
|
||||
collapsedShape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
@ -283,25 +290,41 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
childrenPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 0),
|
||||
childrenPadding: 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) {
|
||||
final validWorkAreas = floor.workAreas
|
||||
.where((area) => area.workItems.isNotEmpty);
|
||||
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 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);
|
||||
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) {
|
||||
@ -310,14 +333,11 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
|
||||
});
|
||||
},
|
||||
trailing: Icon(
|
||||
isExpanded
|
||||
? Icons.keyboard_arrow_up
|
||||
: Icons.keyboard_arrow_down,
|
||||
isExpanded ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down,
|
||||
size: 28,
|
||||
color: Colors.black54,
|
||||
),
|
||||
tilePadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16, vertical: 0),
|
||||
tilePadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 0),
|
||||
title: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
@ -362,22 +382,17 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
|
||||
circularStrokeCap: CircularStrokeCap.round,
|
||||
progressColor: totalProgress >= 1.0
|
||||
? Colors.green
|
||||
: (totalProgress >= 0.5
|
||||
? Colors.amber
|
||||
: Colors.red),
|
||||
: (totalProgress >= 0.5 ? Colors.amber : Colors.red),
|
||||
backgroundColor: Colors.grey[300]!,
|
||||
),
|
||||
],
|
||||
),
|
||||
childrenPadding: const EdgeInsets.only(
|
||||
left: 16, right: 0, bottom: 8),
|
||||
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);
|
||||
final progress = (planned == 0) ? 0.0 : (completed / planned).clamp(0.0, 1.0);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
@ -389,8 +404,7 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
|
||||
children: [
|
||||
Expanded(
|
||||
child: MyText.bodyMedium(
|
||||
item.activityMaster?.name ??
|
||||
"No Activity",
|
||||
item.activityMaster?.name ?? "No Activity",
|
||||
fontWeight: 600,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.visible,
|
||||
@ -400,12 +414,10 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
|
||||
MySpacing.width(8),
|
||||
if (item.workCategoryMaster?.name != null)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 4),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.shade100,
|
||||
borderRadius:
|
||||
BorderRadius.circular(20),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: MyText.bodySmall(
|
||||
item.workCategoryMaster!.name!,
|
||||
@ -422,52 +434,40 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
MySpacing.height(8),
|
||||
MyText.bodySmall(
|
||||
"Completed: $completed / $planned",
|
||||
fontWeight: 600,
|
||||
color: const Color.fromARGB(
|
||||
221, 0, 0, 0),
|
||||
color: const Color.fromARGB(221, 0, 0, 0),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
MySpacing.width(16),
|
||||
if (progress < 1.0 &&
|
||||
permissionController.hasPermission(
|
||||
Permissions.assignReportTask))
|
||||
permissionController.hasPermission(Permissions.assignReportTask))
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
Icons.person_add_alt_1_rounded,
|
||||
color:
|
||||
Color.fromARGB(255, 46, 161, 233),
|
||||
color: Color.fromARGB(255, 46, 161, 233),
|
||||
),
|
||||
onPressed: () {
|
||||
final pendingTask =
|
||||
(planned - completed)
|
||||
.clamp(0, planned)
|
||||
.toInt();
|
||||
final pendingTask = (planned - completed).clamp(0, planned).toInt();
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius:
|
||||
BorderRadius.vertical(
|
||||
top: Radius.circular(16)),
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
builder: (context) =>
|
||||
AssignTaskBottomSheet(
|
||||
builder: (context) => AssignTaskBottomSheet(
|
||||
buildingName: building.name,
|
||||
floorName: floor.floorName,
|
||||
workAreaName: area.areaName,
|
||||
workLocation: area.areaName,
|
||||
activityName:
|
||||
item.activityMaster?.name ??
|
||||
"Unknown Activity",
|
||||
activityName: item.activityMaster?.name ?? "Unknown Activity",
|
||||
pendingTask: pendingTask,
|
||||
workItemId: item.id.toString(),
|
||||
assignmentDate: DateTime.now(),
|
||||
@ -477,7 +477,6 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
MySpacing.height(6),
|
||||
Row(
|
||||
children: [
|
||||
@ -487,7 +486,6 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
MySpacing.height(16),
|
||||
Stack(
|
||||
children: [
|
||||
@ -505,11 +503,8 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
|
||||
decoration: BoxDecoration(
|
||||
color: progress >= 1.0
|
||||
? Colors.green
|
||||
: (progress >= 0.5
|
||||
? Colors.amber
|
||||
: Colors.red),
|
||||
borderRadius:
|
||||
BorderRadius.circular(6),
|
||||
: (progress >= 0.5 ? Colors.amber : Colors.red),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -521,9 +516,7 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
|
||||
fontWeight: 500,
|
||||
color: progress >= 1.0
|
||||
? Colors.green[700]
|
||||
: (progress >= 0.5
|
||||
? Colors.amber[800]
|
||||
: Colors.red[700]),
|
||||
: (progress >= 0.5 ? Colors.amber[800] : Colors.red[700]),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -532,6 +525,8 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
|
||||
);
|
||||
}).toList();
|
||||
}).toList(),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user