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) {
final hasPerm = permissions.any((p) => p.id == permissionId);
logSafe("Checking permission $permissionId: $hasPerm",
level: LogLevel.debug);
// logSafe("Checking permission $permissionId: $hasPerm",
// level: LogLevel.debug);
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)
static Widget chartSkeletonLoader() {
return MyCard.bordered(

View File

@ -88,12 +88,15 @@ 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(
@ -235,16 +238,22 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
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);
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) async {
setMainState(() {
@ -253,7 +262,8 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
if (expanded && !buildingLoaded && !buildingLoading) {
// fetch infra details for this building lazily
final projectId = projectController.selectedProjectId.value;
final projectId =
projectController.selectedProjectId.value;
if (projectId.isNotEmpty) {
await dailyTaskPlanningController.fetchBuildingInfra(
building.id.toString(),
@ -265,7 +275,8 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
}
},
trailing: buildExpandIcon(isBuildingExpanded),
tilePadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 0),
tilePadding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 0),
collapsedShape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
@ -290,18 +301,14 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
childrenPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 0),
childrenPadding:
const EdgeInsets.symmetric(horizontal: 5, vertical: 0),
children: [
if (buildingLoading)
Padding(
padding: const EdgeInsets.all(16.0),
child: Center(
child: SizedBox(
height: 24,
width: 24,
child: CircularProgressIndicator(strokeWidth: 2),
),
),
padding: const EdgeInsets.all(0.0),
child: SkeletonLoaders
.dailyProgressPlanningInfraSkeleton(),
)
else if (!buildingLoaded || building.floors.isEmpty)
Padding(
@ -315,36 +322,55 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
// 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) {
setMainState(() {
floorExpansionState[floorWorkAreaKey] = expanded;
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),
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(
flex: 3,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
MyText.titleSmall(
"Floor: ${floor.floorName}",
@ -366,7 +392,6 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
],
),
),
MySpacing.width(12),
CircularPercentIndicator(
radius: 20.0,
lineWidth: 4.0,
@ -382,29 +407,39 @@ 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) {
// 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);
final progress = (planned == 0)
? 0.0
: (completed / planned).clamp(0.0, 1.0);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
padding:
const EdgeInsets.symmetric(vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Expanded(
child: MyText.bodyMedium(
item.activityMaster?.name ?? "No Activity",
item.activityMaster?.name ??
"No Activity",
fontWeight: 600,
maxLines: 2,
overflow: TextOverflow.visible,
@ -412,12 +447,17 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
),
),
MySpacing.width(8),
if (item.workCategoryMaster?.name != null)
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!,
@ -429,48 +469,68 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
),
MySpacing.height(4),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
crossAxisAlignment:
CrossAxisAlignment.center,
children: [
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)),
shape:
const RoundedRectangleBorder(
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(),
workItemId:
item.id.toString(),
assignmentDate:
DateTime.now(),
),
);
},
@ -493,7 +553,8 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
height: 5,
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(6),
borderRadius:
BorderRadius.circular(6),
),
),
FractionallySizedBox(
@ -503,8 +564,11 @@ 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),
),
),
),
@ -516,7 +580,9 @@ 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]),
),
],
),