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 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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,28 +88,24 @@ 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 ?? 'Select Project';
|
||||||
projectController.selectedProject?.name ??
|
return Row(
|
||||||
'Select Project';
|
children: [
|
||||||
return Row(
|
const Icon(Icons.work_outline, size: 14, color: Colors.grey),
|
||||||
children: [
|
MySpacing.width(4),
|
||||||
const Icon(Icons.work_outline,
|
Expanded(
|
||||||
size: 14, color: Colors.grey),
|
child: MyText.bodySmall(
|
||||||
MySpacing.width(4),
|
projectName,
|
||||||
Expanded(
|
fontWeight: 600,
|
||||||
child: MyText.bodySmall(
|
overflow: TextOverflow.ellipsis,
|
||||||
projectName,
|
color: Colors.grey[700],
|
||||||
fontWeight: 600,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
color: Colors.grey[700],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
);
|
],
|
||||||
},
|
);
|
||||||
),
|
}),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -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,255 +290,243 @@ 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: [
|
||||||
children: building.floors.expand((floor) {
|
if (buildingLoading)
|
||||||
final validWorkAreas = floor.workAreas
|
Padding(
|
||||||
.where((area) => area.workItems.isNotEmpty);
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Center(
|
||||||
return validWorkAreas.map((area) {
|
child: SizedBox(
|
||||||
final floorWorkAreaKey =
|
height: 24,
|
||||||
"${buildingKey}_${floor.floorName}_${area.areaName}";
|
width: 24,
|
||||||
final isExpanded =
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
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: Icon(
|
|
||||||
isExpanded
|
|
||||||
? Icons.keyboard_arrow_up
|
|
||||||
: Icons.keyboard_arrow_down,
|
|
||||||
size: 28,
|
|
||||||
color: Colors.black54,
|
|
||||||
),
|
),
|
||||||
tilePadding: const EdgeInsets.symmetric(
|
)
|
||||||
horizontal: 16, vertical: 0),
|
else if (!buildingLoaded || building.floors.isEmpty)
|
||||||
title: Row(
|
Padding(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
padding: const EdgeInsets.all(16.0),
|
||||||
children: [
|
child: MyText.bodySmall(
|
||||||
Expanded(
|
"No Progress Report Found",
|
||||||
flex: 3,
|
fontWeight: 600,
|
||||||
child: Column(
|
),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
)
|
||||||
|
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: 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: [
|
children: [
|
||||||
MyText.titleSmall(
|
Expanded(
|
||||||
"Floor: ${floor.floorName}",
|
flex: 3,
|
||||||
fontWeight: 600,
|
child: Column(
|
||||||
color: Colors.teal,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
maxLines: null,
|
children: [
|
||||||
overflow: TextOverflow.visible,
|
MyText.titleSmall(
|
||||||
softWrap: true,
|
"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.height(4),
|
MySpacing.width(12),
|
||||||
MyText.titleSmall(
|
CircularPercentIndicator(
|
||||||
"Work Area: ${area.areaName}",
|
radius: 20.0,
|
||||||
fontWeight: 600,
|
lineWidth: 4.0,
|
||||||
color: Colors.blueGrey,
|
animation: true,
|
||||||
maxLines: null,
|
percent: totalProgress,
|
||||||
overflow: TextOverflow.visible,
|
center: Text(
|
||||||
softWrap: true,
|
"${(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),
|
||||||
MySpacing.width(12),
|
children: area.workItems.map((wItem) {
|
||||||
CircularPercentIndicator(
|
final item = wItem.workItem;
|
||||||
radius: 20.0,
|
final completed = item.completedWork ?? 0;
|
||||||
lineWidth: 4.0,
|
final planned = item.plannedWork ?? 0;
|
||||||
animation: true,
|
final progress = (planned == 0) ? 0.0 : (completed / planned).clamp(0.0, 1.0);
|
||||||
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) {
|
|
||||||
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(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
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,
|
softWrap: true,
|
||||||
softWrap: true,
|
),
|
||||||
),
|
),
|
||||||
),
|
MySpacing.width(8),
|
||||||
MySpacing.width(8),
|
if (item.workCategoryMaster?.name != null)
|
||||||
if (item.workCategoryMaster?.name != null)
|
Container(
|
||||||
Container(
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||||
padding: const EdgeInsets.symmetric(
|
decoration: BoxDecoration(
|
||||||
horizontal: 12, vertical: 4),
|
color: Colors.blue.shade100,
|
||||||
decoration: BoxDecoration(
|
borderRadius: BorderRadius.circular(20),
|
||||||
color: Colors.blue.shade100,
|
),
|
||||||
borderRadius:
|
child: MyText.bodySmall(
|
||||||
BorderRadius.circular(20),
|
item.workCategoryMaster!.name!,
|
||||||
),
|
fontWeight: 500,
|
||||||
child: MyText.bodySmall(
|
color: Colors.blue.shade800,
|
||||||
item.workCategoryMaster!.name!,
|
),
|
||||||
fontWeight: 500,
|
),
|
||||||
color: Colors.blue.shade800,
|
],
|
||||||
),
|
),
|
||||||
),
|
MySpacing.height(4),
|
||||||
],
|
Row(
|
||||||
),
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
MySpacing.height(4),
|
children: [
|
||||||
Row(
|
Expanded(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
flex: 3,
|
||||||
children: [
|
child: Column(
|
||||||
Expanded(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
flex: 3,
|
children: [
|
||||||
child: Column(
|
MySpacing.height(8),
|
||||||
crossAxisAlignment:
|
MyText.bodySmall(
|
||||||
CrossAxisAlignment.start,
|
"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: [
|
children: [
|
||||||
MySpacing.height(8),
|
|
||||||
MyText.bodySmall(
|
MyText.bodySmall(
|
||||||
"Completed: $completed / $planned",
|
"Today's Planned: ${item.todaysAssigned ?? 0}",
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
color: const Color.fromARGB(
|
|
||||||
221, 0, 0, 0),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
MySpacing.height(16),
|
||||||
MySpacing.width(16),
|
Stack(
|
||||||
if (progress < 1.0 &&
|
children: [
|
||||||
permissionController.hasPermission(
|
Container(
|
||||||
Permissions.assignReportTask))
|
height: 5,
|
||||||
IconButton(
|
decoration: BoxDecoration(
|
||||||
icon: const Icon(
|
color: Colors.grey[300],
|
||||||
Icons.person_add_alt_1_rounded,
|
borderRadius: BorderRadius.circular(6),
|
||||||
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(
|
FractionallySizedBox(
|
||||||
buildingName: building.name,
|
widthFactor: progress,
|
||||||
floorName: floor.floorName,
|
child: Container(
|
||||||
workAreaName: area.areaName,
|
height: 5,
|
||||||
workLocation: area.areaName,
|
decoration: BoxDecoration(
|
||||||
activityName:
|
color: progress >= 1.0
|
||||||
item.activityMaster?.name ??
|
? Colors.green
|
||||||
"Unknown Activity",
|
: (progress >= 0.5 ? Colors.amber : Colors.red),
|
||||||
pendingTask: pendingTask,
|
borderRadius: BorderRadius.circular(6),
|
||||||
workItemId: item.id.toString(),
|
),
|
||||||
assignmentDate: DateTime.now(),
|
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
],
|
||||||
),
|
),
|
||||||
],
|
const SizedBox(height: 4),
|
||||||
),
|
MyText.bodySmall(
|
||||||
|
"${(progress * 100).toStringAsFixed(1)}%",
|
||||||
MySpacing.height(6),
|
fontWeight: 500,
|
||||||
Row(
|
color: progress >= 1.0
|
||||||
children: [
|
? Colors.green[700]
|
||||||
MyText.bodySmall(
|
: (progress >= 0.5 ? Colors.amber[800] : Colors.red[700]),
|
||||||
"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(
|
}).toList(),
|
||||||
height: 5,
|
);
|
||||||
decoration: BoxDecoration(
|
}).toList();
|
||||||
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(),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user