feat: Add daily progress planning skeleton loader and improve UI formatting

This commit is contained in:
Vaibhav Surve 2025-11-04 17:41:29 +05:30
parent 99166801da
commit 80d7ef96cb
3 changed files with 218 additions and 62 deletions

View File

@ -117,8 +117,8 @@ class PermissionController extends GetxController {
bool hasPermission(String permissionId) { bool hasPermission(String permissionId) {
final hasPerm = permissions.any((p) => p.id == permissionId); final hasPerm = permissions.any((p) => p.id == permissionId);
logSafe("Checking permission $permissionId: $hasPerm", // logSafe("Checking permission $permissionId: $hasPerm",
level: LogLevel.debug); // level: LogLevel.debug);
return hasPerm; return hasPerm;
} }

View File

@ -33,6 +33,96 @@ class SkeletonLoaders {
); );
} }
// Daily Progress Planning - Infra (Expanded) Skeleton Loader
static Widget dailyProgressPlanningInfraSkeleton() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: List.generate(3, (floorIndex) {
return MyCard(
borderRadiusAll: 8,
paddingAll: 5,
margin: MySpacing.bottom(10),
shadow: MyShadow(elevation: 1.5),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Floor header placeholder
Container(
height: 14,
width: 160,
decoration: BoxDecoration(
color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(6),
),
),
MySpacing.height(10),
// Divider
Divider(color: Colors.grey.withOpacity(0.3)),
// Work areas skeleton
Column(
children: List.generate(2, (areaIndex) {
return Padding(
padding: const EdgeInsets.only(top: 8, bottom: 6),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Area title
Container(
height: 12,
width: 120,
decoration: BoxDecoration(
color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(4),
),
),
MySpacing.height(8),
// Work items skeleton rows
Column(
children: List.generate(2, (itemIndex) {
return Padding(
padding: const EdgeInsets.only(top: 4),
child: Row(
children: [
// Bullet / icon
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: Colors.grey.shade300,
shape: BoxShape.circle,
),
),
MySpacing.width(8),
// Item text placeholder
Expanded(
child: Container(
height: 10,
decoration: BoxDecoration(
color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(4),
),
),
),
],
),
);
}),
),
],
),
);
}),
),
],
),
);
}),
);
}
// Chart Skeleton Loader (Donut Chart) // Chart Skeleton Loader (Donut Chart)
static Widget chartSkeletonLoader() { static Widget chartSkeletonLoader() {
return MyCard.bordered( return MyCard.bordered(

View File

@ -88,12 +88,15 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
color: Colors.black, color: Colors.black,
), ),
MySpacing.height(2), MySpacing.height(2),
GetBuilder<ProjectController>(builder: (projectController) { GetBuilder<ProjectController>(
builder: (projectController) {
final projectName = final projectName =
projectController.selectedProject?.name ?? 'Select Project'; projectController.selectedProject?.name ??
'Select Project';
return Row( return Row(
children: [ children: [
const Icon(Icons.work_outline, size: 14, color: Colors.grey), const Icon(Icons.work_outline,
size: 14, color: Colors.grey),
MySpacing.width(4), MySpacing.width(4),
Expanded( Expanded(
child: MyText.bodySmall( child: MyText.bodySmall(
@ -235,16 +238,22 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: buildings.map((building) { children: buildings.map((building) {
final buildingKey = building.id.toString(); final buildingKey = building.id.toString();
final isBuildingExpanded = buildingExpansionState[buildingKey] ?? false; final isBuildingExpanded =
final buildingLoading = dailyTaskPlanningController.buildingLoadingStates[buildingKey]?.value ?? false; buildingExpansionState[buildingKey] ?? false;
final buildingLoaded = dailyTaskPlanningController.buildingsWithDetails.contains(buildingKey); 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).copyWith(dividerColor: Colors.transparent), data: Theme.of(context)
.copyWith(dividerColor: Colors.transparent),
child: ExpansionTile( child: ExpansionTile(
onExpansionChanged: (expanded) async { onExpansionChanged: (expanded) async {
setMainState(() { setMainState(() {
@ -253,7 +262,8 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
if (expanded && !buildingLoaded && !buildingLoading) { if (expanded && !buildingLoaded && !buildingLoading) {
// fetch infra details for this building lazily // fetch infra details for this building lazily
final projectId = projectController.selectedProjectId.value; final projectId =
projectController.selectedProjectId.value;
if (projectId.isNotEmpty) { if (projectId.isNotEmpty) {
await dailyTaskPlanningController.fetchBuildingInfra( await dailyTaskPlanningController.fetchBuildingInfra(
building.id.toString(), building.id.toString(),
@ -265,7 +275,8 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
} }
}, },
trailing: buildExpandIcon(isBuildingExpanded), trailing: buildExpandIcon(isBuildingExpanded),
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),
), ),
@ -290,18 +301,14 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
childrenPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 0), childrenPadding:
const EdgeInsets.symmetric(horizontal: 5, vertical: 0),
children: [ children: [
if (buildingLoading) if (buildingLoading)
Padding( Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(0.0),
child: Center( child: SkeletonLoaders
child: SizedBox( .dailyProgressPlanningInfraSkeleton(),
height: 24,
width: 24,
child: CircularProgressIndicator(strokeWidth: 2),
),
),
) )
else if (!buildingLoaded || building.floors.isEmpty) else if (!buildingLoaded || building.floors.isEmpty)
Padding( Padding(
@ -315,36 +322,55 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
// Building is loaded and has floors; render floors -> areas -> items // Building is loaded and has floors; render floors -> areas -> items
Column( Column(
children: building.floors.expand((floor) { 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) { return validWorkAreas.map((area) {
final floorWorkAreaKey = final floorWorkAreaKey =
"${buildingKey}_${floor.floorName}_${area.areaName}"; "${buildingKey}_${floor.floorName}_${area.areaName}";
final isExpanded = floorExpansionState[floorWorkAreaKey] ?? false; final isExpanded =
floorExpansionState[floorWorkAreaKey] ?? false;
final workItems = area.workItems; final workItems = area.workItems;
final totalPlanned = workItems.fold<double>(0, (sum, wi) => sum + (wi.workItem.plannedWork ?? 0)); final totalPlanned = workItems.fold<double>(
final totalCompleted = workItems.fold<double>(0, (sum, wi) => sum + (wi.workItem.completedWork ?? 0)); 0,
final totalProgress = totalPlanned == 0 ? 0.0 : (totalCompleted / totalPlanned).clamp(0.0, 1.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( return ExpansionTile(
onExpansionChanged: (expanded) { onExpansionChanged: (expanded) {
setMainState(() { setMainState(() {
floorExpansionState[floorWorkAreaKey] = expanded; floorExpansionState[floorWorkAreaKey] =
expanded;
}); });
}, },
trailing: Icon( trailing: const SizedBox
isExpanded ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down, .shrink(), // Add this to remove the default trailing icon
size: 28,
color: Colors.black54,
),
tilePadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 0),
title: Row( title: Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ 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( Expanded(
flex: 3,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment:
CrossAxisAlignment.start,
children: [ children: [
MyText.titleSmall( MyText.titleSmall(
"Floor: ${floor.floorName}", "Floor: ${floor.floorName}",
@ -366,7 +392,6 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
], ],
), ),
), ),
MySpacing.width(12),
CircularPercentIndicator( CircularPercentIndicator(
radius: 20.0, radius: 20.0,
lineWidth: 4.0, lineWidth: 4.0,
@ -382,29 +407,39 @@ 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 ? Colors.amber : Colors.red), : (totalProgress >= 0.5
? Colors.amber
: Colors.red),
backgroundColor: Colors.grey[300]!, 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) { children: area.workItems.map((wItem) {
// keep your existing work item UI as-is
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) ? 0.0 : (completed / planned).clamp(0.0, 1.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 ?? "No Activity", item.activityMaster?.name ??
"No Activity",
fontWeight: 600, fontWeight: 600,
maxLines: 2, maxLines: 2,
overflow: TextOverflow.visible, overflow: TextOverflow.visible,
@ -412,12 +447,17 @@ 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(horizontal: 12, vertical: 4), padding:
const EdgeInsets.symmetric(
horizontal: 12,
vertical: 4),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.blue.shade100, color: Colors.blue.shade100,
borderRadius: BorderRadius.circular(20), borderRadius:
BorderRadius.circular(20),
), ),
child: MyText.bodySmall( child: MyText.bodySmall(
item.workCategoryMaster!.name!, item.workCategoryMaster!.name!,
@ -429,48 +469,68 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
), ),
MySpacing.height(4), MySpacing.height(4),
Row( Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment:
CrossAxisAlignment.center,
children: [ children: [
Expanded( Expanded(
flex: 3, flex: 3,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment:
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(221, 0, 0, 0), color: const Color.fromARGB(
221, 0, 0, 0),
), ),
], ],
), ),
), ),
MySpacing.width(16), MySpacing.width(16),
if (progress < 1.0 && if (progress < 1.0 &&
permissionController.hasPermission(Permissions.assignReportTask)) permissionController
.hasPermission(Permissions
.assignReportTask))
IconButton( IconButton(
icon: const Icon( icon: const Icon(
Icons.person_add_alt_1_rounded, Icons.person_add_alt_1_rounded,
color: Color.fromARGB(255, 46, 161, 233), color: Color.fromARGB(
255, 46, 161, 233),
), ),
onPressed: () { onPressed: () {
final pendingTask = (planned - completed).clamp(0, planned).toInt(); final pendingTask =
(planned - completed)
.clamp(0, planned)
.toInt();
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
shape: const RoundedRectangleBorder( shape:
borderRadius: BorderRadius.vertical(top: Radius.circular(16)), const RoundedRectangleBorder(
borderRadius:
BorderRadius.vertical(
top:
Radius.circular(
16)),
), ),
builder: (context) => AssignTaskBottomSheet( builder: (context) =>
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: item.activityMaster?.name ?? "Unknown Activity", activityName: item
.activityMaster
?.name ??
"Unknown Activity",
pendingTask: pendingTask, pendingTask: pendingTask,
workItemId: item.id.toString(), workItemId:
assignmentDate: DateTime.now(), item.id.toString(),
assignmentDate:
DateTime.now(),
), ),
); );
}, },
@ -493,7 +553,8 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
height: 5, height: 5,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey[300], color: Colors.grey[300],
borderRadius: BorderRadius.circular(6), borderRadius:
BorderRadius.circular(6),
), ),
), ),
FractionallySizedBox( FractionallySizedBox(
@ -503,8 +564,11 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
decoration: BoxDecoration( decoration: BoxDecoration(
color: progress >= 1.0 color: progress >= 1.0
? Colors.green ? Colors.green
: (progress >= 0.5 ? Colors.amber : Colors.red), : (progress >= 0.5
borderRadius: BorderRadius.circular(6), ? Colors.amber
: Colors.red),
borderRadius:
BorderRadius.circular(6),
), ),
), ),
), ),
@ -516,7 +580,9 @@ 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 ? Colors.amber[800] : Colors.red[700]), : (progress >= 0.5
? Colors.amber[800]
: Colors.red[700]),
), ),
], ],
), ),