feat: Implement pagination and service filtering in daily task fetching logic

This commit is contained in:
Vaibhav Surve 2025-09-23 15:02:37 +05:30
parent fc081c779e
commit 7d211e24f8
5 changed files with 251 additions and 204 deletions

View File

@ -24,8 +24,12 @@ class DailyTaskController extends GetxController {
} }
RxBool isLoading = true.obs; RxBool isLoading = true.obs;
RxBool isLoadingMore = false.obs;
Map<String, List<TaskModel>> groupedDailyTasks = {}; Map<String, List<TaskModel>> groupedDailyTasks = {};
// Pagination
int currentPage = 1;
int pageSize = 20;
bool hasMore = true;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
@ -47,48 +51,49 @@ class DailyTaskController extends GetxController {
); );
} }
Future<void> fetchTaskData(String? projectId) async { Future<void> fetchTaskData(
if (projectId == null) { String projectId, {
logSafe("fetchTaskData: Skipped, projectId is null", List<String>? serviceIds,
level: LogLevel.warning); int pageNumber = 1,
return; int pageSize = 20,
bool isLoadMore = false,
}) async {
if (!isLoadMore) {
isLoading.value = true;
currentPage = 1;
hasMore = true;
groupedDailyTasks.clear();
dailyTasks.clear();
} else {
isLoadingMore.value = true;
} }
isLoading.value = true;
final response = await ApiService.getDailyTasks( final response = await ApiService.getDailyTasks(
projectId, projectId,
dateFrom: startDateTask, dateFrom: startDateTask,
dateTo: endDateTask, dateTo: endDateTask,
serviceIds: serviceIds,
pageNumber: pageNumber,
pageSize: pageSize,
); );
isLoading.value = false; if (response != null && response.isNotEmpty) {
if (response != null) {
groupedDailyTasks.clear();
for (var taskJson in response) { for (var taskJson in response) {
final task = TaskModel.fromJson(taskJson); final task = TaskModel.fromJson(taskJson);
final assignmentDateKey = final assignmentDateKey =
task.assignmentDate.toIso8601String().split('T')[0]; task.assignmentDate.toIso8601String().split('T')[0];
groupedDailyTasks.putIfAbsent(assignmentDateKey, () => []).add(task); groupedDailyTasks.putIfAbsent(assignmentDateKey, () => []).add(task);
} }
dailyTasks = groupedDailyTasks.values.expand((list) => list).toList(); dailyTasks = groupedDailyTasks.values.expand((list) => list).toList();
currentPage = pageNumber;
logSafe(
"Daily tasks fetched and grouped: ${dailyTasks.length} for project $projectId",
level: LogLevel.info,
);
update();
} else { } else {
logSafe( hasMore = false;
"Failed to fetch daily tasks for project $projectId",
level: LogLevel.error,
);
} }
isLoading.value = false;
isLoadingMore.value = false;
update();
} }
Future<void> selectDateRangeForTaskData( Future<void> selectDateRangeForTaskData(
@ -119,17 +124,23 @@ class DailyTaskController extends GetxController {
level: LogLevel.info, level: LogLevel.info,
); );
await controller.fetchTaskData(controller.selectedProjectId); // Add null check before calling fetchTaskData
final projectId = controller.selectedProjectId;
if (projectId != null && projectId.isNotEmpty) {
await controller.fetchTaskData(projectId);
} else {
logSafe("Project ID is null or empty, skipping fetchTaskData",
level: LogLevel.warning);
}
} }
void refreshTasksFromNotification({ void refreshTasksFromNotification({
required String projectId, required String projectId,
required String taskAllocationId, required String taskAllocationId,
}) async { }) async {
// re-fetch tasks // re-fetch tasks
await fetchTaskData(projectId); await fetchTaskData(projectId);
update(); // rebuilds UI
}
update(); // rebuilds UI
}
} }

View File

@ -2022,21 +2022,35 @@ class ApiService {
// === Daily Task APIs === // === Daily Task APIs ===
static Future<List<dynamic>?> getDailyTasks( static Future<List<dynamic>?> getDailyTasks(
String projectId, { String projectId, {
DateTime? dateFrom, DateTime? dateFrom,
DateTime? dateTo, DateTime? dateTo,
}) async { List<String>? serviceIds,
final query = { int pageNumber = 1,
"projectId": projectId, int pageSize = 20,
if (dateFrom != null) }) async {
"dateFrom": DateFormat('yyyy-MM-dd').format(dateFrom), final filterBody = {
if (dateTo != null) "dateTo": DateFormat('yyyy-MM-dd').format(dateTo), "serviceIds": serviceIds ?? [],
}; };
return _getRequest(ApiEndpoints.getDailyTask, queryParams: query).then(
(res) => final query = {
res != null ? _parseResponse(res, label: 'Daily Tasks') : null); "projectId": projectId,
} "pageNumber": pageNumber.toString(),
"pageSize": pageSize.toString(),
if (dateFrom != null)
"dateFrom": DateFormat('yyyy-MM-dd').format(dateFrom),
if (dateTo != null) "dateTo": DateFormat('yyyy-MM-dd').format(dateTo),
"filter": jsonEncode(filterBody),
};
final uri = Uri.parse(ApiEndpoints.getDailyTask).replace(queryParameters: query);
final response = await _getRequest(uri.toString());
return response != null ? _parseResponse(response, label: 'Daily Tasks') : null;
}
static Future<bool> reportTask({ static Future<bool> reportTask({
required String id, required String id,

View File

@ -25,17 +25,15 @@ class OrganizationSelector extends StatelessWidget {
required List<String> items, required List<String> items,
}) { }) {
return PopupMenuButton<String>( return PopupMenuButton<String>(
color: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
onSelected: (name) async { onSelected: (name) async {
// Determine the selected organization
Organization? org = name == "All Organizations" Organization? org = name == "All Organizations"
? null ? null
: controller.organizations.firstWhere((e) => e.name == name); : controller.organizations.firstWhere((e) => e.name == name);
// Update controller state
controller.selectOrganization(org); controller.selectOrganization(org);
// Trigger callback for post-selection logic
if (onSelectionChanged != null) { if (onSelectionChanged != null) {
await onSelectionChanged!(org); await onSelectionChanged!(org);
} }
@ -45,9 +43,10 @@ class OrganizationSelector extends StatelessWidget {
.toList(), .toList(),
child: Container( child: Container(
height: height, height: height,
padding: EdgeInsets.symmetric(horizontal: 12), padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey.shade100, color:
Colors.white,
border: Border.all(color: Colors.grey.shade300), border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),

View File

@ -44,28 +44,50 @@ class _DailyProgressReportScreenState extends State<DailyProgressReportScreen>
Get.find<PermissionController>(); Get.find<PermissionController>();
final ProjectController projectController = Get.find<ProjectController>(); final ProjectController projectController = Get.find<ProjectController>();
final ServiceController serviceController = Get.put(ServiceController()); final ServiceController serviceController = Get.put(ServiceController());
final ScrollController _scrollController = ScrollController();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_scrollController.addListener(() {
if (_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent - 100 &&
dailyTaskController.hasMore &&
!dailyTaskController.isLoadingMore.value) {
final projectId = dailyTaskController.selectedProjectId;
if (projectId != null && projectId.isNotEmpty) {
dailyTaskController.fetchTaskData(
projectId,
pageNumber: dailyTaskController.currentPage + 1,
pageSize: dailyTaskController.pageSize,
isLoadMore: true,
);
}
}
});
final initialProjectId = projectController.selectedProjectId.value; final initialProjectId = projectController.selectedProjectId.value;
if (initialProjectId.isNotEmpty) { if (initialProjectId.isNotEmpty) {
dailyTaskController.selectedProjectId = initialProjectId; dailyTaskController.selectedProjectId = initialProjectId;
dailyTaskController.fetchTaskData(initialProjectId); dailyTaskController.fetchTaskData(initialProjectId);
serviceController.fetchServices(initialProjectId);
} }
ever<String>( // Update when project changes
projectController.selectedProjectId, ever<String>(projectController.selectedProjectId, (newProjectId) async {
(newProjectId) async { if (newProjectId.isNotEmpty &&
if (newProjectId.isNotEmpty && newProjectId != dailyTaskController.selectedProjectId) {
newProjectId != dailyTaskController.selectedProjectId) { dailyTaskController.selectedProjectId = newProjectId;
dailyTaskController.selectedProjectId = newProjectId; await dailyTaskController.fetchTaskData(newProjectId);
await dailyTaskController.fetchTaskData(newProjectId); await serviceController.fetchServices(newProjectId);
dailyTaskController.update(['daily_progress_report_controller']); dailyTaskController.update(['daily_progress_report_controller']);
} }
}, });
); }
@override
void dispose() {
_scrollController.dispose();
super.dispose();
} }
@override @override
@ -158,7 +180,10 @@ class _DailyProgressReportScreenState extends State<DailyProgressReportScreen>
if (projectId?.isNotEmpty ?? false) { if (projectId?.isNotEmpty ?? false) {
await dailyTaskController.fetchTaskData( await dailyTaskController.fetchTaskData(
projectId!, projectId!,
// serviceId: service?.id, serviceIds:
service != null ? [service.id] : null,
pageNumber: 1,
pageSize: 20,
); );
} }
}, },
@ -321,10 +346,12 @@ class _DailyProgressReportScreenState extends State<DailyProgressReportScreen>
final isLoading = dailyTaskController.isLoading.value; final isLoading = dailyTaskController.isLoading.value;
final groupedTasks = dailyTaskController.groupedDailyTasks; final groupedTasks = dailyTaskController.groupedDailyTasks;
if (isLoading) { // Initial loading skeleton
if (isLoading && dailyTaskController.currentPage == 1) {
return SkeletonLoaders.dailyProgressReportSkeletonLoader(); return SkeletonLoaders.dailyProgressReportSkeletonLoader();
} }
// No tasks
if (groupedTasks.isEmpty) { if (groupedTasks.isEmpty) {
return Center( return Center(
child: MyText.bodySmall( child: MyText.bodySmall(
@ -337,23 +364,33 @@ class _DailyProgressReportScreenState extends State<DailyProgressReportScreen>
final sortedDates = groupedTasks.keys.toList() final sortedDates = groupedTasks.keys.toList()
..sort((a, b) => b.compareTo(a)); ..sort((a, b) => b.compareTo(a));
// If only one date, make it expanded by default
if (sortedDates.length == 1 &&
!dailyTaskController.expandedDates.contains(sortedDates[0])) {
dailyTaskController.expandedDates.add(sortedDates[0]);
}
return MyCard.bordered( return MyCard.bordered(
borderRadiusAll: 10, borderRadiusAll: 10,
border: Border.all(color: Colors.grey.withOpacity(0.2)), border: Border.all(color: Colors.grey.withOpacity(0.2)),
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom), shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
paddingAll: 8, paddingAll: 8,
child: ListView.separated( child: ListView.builder(
controller: _scrollController,
shrinkWrap: true, shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
itemCount: sortedDates.length, itemCount: sortedDates.length + 1, // +1 for loading indicator
separatorBuilder: (_, __) => Column(
children: [
const SizedBox(height: 12),
Divider(color: Colors.grey.withOpacity(0.3), thickness: 1),
const SizedBox(height: 12),
],
),
itemBuilder: (context, dateIndex) { itemBuilder: (context, dateIndex) {
// Bottom loading indicator
if (dateIndex == sortedDates.length) {
return Obx(() => dailyTaskController.isLoadingMore.value
? const Padding(
padding: EdgeInsets.all(16.0),
child: Center(child: CircularProgressIndicator()),
)
: const SizedBox.shrink());
}
final dateKey = sortedDates[dateIndex]; final dateKey = sortedDates[dateIndex];
final tasksForDate = groupedTasks[dateKey]!; final tasksForDate = groupedTasks[dateKey]!;
final date = DateTime.tryParse(dateKey); final date = DateTime.tryParse(dateKey);
@ -389,7 +426,6 @@ class _DailyProgressReportScreenState extends State<DailyProgressReportScreen>
return Column( return Column(
children: tasksForDate.asMap().entries.map((entry) { children: tasksForDate.asMap().entries.map((entry) {
final task = entry.value; final task = entry.value;
final index = entry.key;
final activityName = final activityName =
task.workItem?.activityMaster?.activityName ?? 'N/A'; task.workItem?.activityMaster?.activityName ?? 'N/A';
@ -407,134 +443,121 @@ class _DailyProgressReportScreenState extends State<DailyProgressReportScreen>
? (completed / planned).clamp(0.0, 1.0) ? (completed / planned).clamp(0.0, 1.0)
: 0.0; : 0.0;
final parentTaskID = task.id; final parentTaskID = task.id;
return Column(
children: [ return Padding(
Padding( padding: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.only(bottom: 8), child: MyContainer(
child: MyContainer( paddingAll: 12,
paddingAll: 12, child: Column(
child: Column( crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, children: [
MyText.bodyMedium(activityName, fontWeight: 600),
const SizedBox(height: 2),
MyText.bodySmall(location, color: Colors.grey),
const SizedBox(height: 8),
GestureDetector(
onTap: () => _showTeamMembersBottomSheet(
task.teamMembers),
child: Row(
children: [
const Icon(Icons.group,
size: 18, color: Colors.blueAccent),
const SizedBox(width: 6),
MyText.bodyMedium('Team',
color: Colors.blueAccent,
fontWeight: 600),
],
),
),
const SizedBox(height: 8),
MyText.bodySmall(
"Completed: $completed / $planned",
fontWeight: 600,
color: Colors.black87,
),
const SizedBox(height: 6),
Stack(
children: [ children: [
MyText.bodyMedium(activityName, Container(
fontWeight: 600), height: 5,
const SizedBox(height: 2), decoration: BoxDecoration(
MyText.bodySmall(location, color: Colors.grey[300],
color: Colors.grey), borderRadius: BorderRadius.circular(6),
const SizedBox(height: 8),
GestureDetector(
onTap: () => _showTeamMembersBottomSheet(
task.teamMembers),
child: Row(
children: [
const Icon(Icons.group,
size: 18, color: Colors.blueAccent),
const SizedBox(width: 6),
MyText.bodyMedium('Team',
color: Colors.blueAccent,
fontWeight: 600),
],
), ),
), ),
const SizedBox(height: 8), FractionallySizedBox(
MyText.bodySmall( widthFactor: progress,
"Completed: $completed / $planned", child: Container(
fontWeight: 600, height: 5,
color: Colors.black87, decoration: BoxDecoration(
), color: progress >= 1.0
const SizedBox(height: 6), ? Colors.green
Stack( : progress >= 0.5
children: [ ? Colors.amber
Container( : Colors.red,
height: 5, borderRadius: BorderRadius.circular(6),
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius:
BorderRadius.circular(6),
),
), ),
FractionallySizedBox(
widthFactor: progress,
child: Container(
height: 5,
decoration: BoxDecoration(
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],
),
const SizedBox(height: 12),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if ((task.reportedDate == null ||
task.reportedDate
.toString()
.isEmpty) &&
permissionController.hasPermission(
Permissions
.assignReportTask)) ...[
TaskActionButtons.reportButton(
context: context,
task: task,
completed: completed.toInt(),
refreshCallback: _refreshData,
),
const SizedBox(width: 4),
] else if (task.approvedBy == null &&
permissionController.hasPermission(
Permissions.approveTask)) ...[
TaskActionButtons.reportActionButton(
context: context,
task: task,
parentTaskID: parentTaskID,
workAreaId: workAreaId.toString(),
activityId: activityId.toString(),
completed: completed.toInt(),
refreshCallback: _refreshData,
),
const SizedBox(width: 5),
],
TaskActionButtons.commentButton(
context: context,
task: task,
parentTaskID: parentTaskID,
workAreaId: workAreaId.toString(),
activityId: activityId.toString(),
refreshCallback: _refreshData,
),
],
), ),
), ),
], ],
), ),
), 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],
),
const SizedBox(height: 12),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if ((task.reportedDate == null ||
task.reportedDate
.toString()
.isEmpty) &&
permissionController.hasPermission(
Permissions.assignReportTask)) ...[
TaskActionButtons.reportButton(
context: context,
task: task,
completed: completed.toInt(),
refreshCallback: _refreshData,
),
const SizedBox(width: 4),
] else if (task.approvedBy == null &&
permissionController.hasPermission(
Permissions.approveTask)) ...[
TaskActionButtons.reportActionButton(
context: context,
task: task,
parentTaskID: parentTaskID,
workAreaId: workAreaId.toString(),
activityId: activityId.toString(),
completed: completed.toInt(),
refreshCallback: _refreshData,
),
const SizedBox(width: 5),
],
TaskActionButtons.commentButton(
context: context,
task: task,
parentTaskID: parentTaskID,
workAreaId: workAreaId.toString(),
activityId: activityId.toString(),
refreshCallback: _refreshData,
),
],
),
),
],
), ),
if (index != tasksForDate.length - 1) ),
Divider(
color: Colors.grey.withOpacity(0.2),
thickness: 1,
height: 1),
],
); );
}).toList(), }).toList(),
); );

View File

@ -37,18 +37,19 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
void initState() { void initState() {
super.initState(); super.initState();
// Initial fetch if a project is already selected
final projectId = projectController.selectedProjectId.value; final projectId = projectController.selectedProjectId.value;
if (projectId.isNotEmpty) { if (projectId.isNotEmpty) {
dailyTaskPlanningController.fetchTaskData(projectId); dailyTaskPlanningController.fetchTaskData(projectId);
serviceController.fetchServices(projectId); // <-- Fetch services here
} }
// Reactive fetch on project ID change
ever<String>( ever<String>(
projectController.selectedProjectId, projectController.selectedProjectId,
(newProjectId) { (newProjectId) {
if (newProjectId.isNotEmpty) { if (newProjectId.isNotEmpty) {
dailyTaskPlanningController.fetchTaskData(newProjectId); dailyTaskPlanningController.fetchTaskData(newProjectId);
serviceController
.fetchServices(newProjectId);
} }
}, },
); );
@ -150,8 +151,7 @@ class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
Padding( Padding(
padding: MySpacing.x(10), padding: MySpacing.x(10),
child: ServiceSelector( child: ServiceSelector(
controller: controller: serviceController,
serviceController,
height: 40, height: 40,
onSelectionChanged: (service) async { onSelectionChanged: (service) async {
final projectId = final projectId =