feat: Add daily progress planning skeleton loader and improve UI formatting
This commit is contained in:
parent
99166801da
commit
80d7ef96cb
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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]),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user