import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:marco/helpers/utils/date_time_utils.dart'; import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/avatar.dart'; import 'package:marco/controller/service_project/service_project_details_screen_controller.dart'; import 'package:marco/view/service_project/service_project_job_detail_screen.dart'; import 'package:marco/helpers/widgets/my_confirmation_dialog.dart'; import 'package:marco/helpers/services/api_service.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:marco/helpers/utils/permission_constants.dart'; class JobsTab extends StatefulWidget { final ScrollController scrollController; const JobsTab({super.key, required this.scrollController}); @override _JobsTabState createState() => _JobsTabState(); } class _JobsTabState extends State { final TextEditingController searchController = TextEditingController(); late ServiceProjectDetailsController controller; @override void initState() { super.initState(); controller = Get.find(); // Ensure only active jobs are displayed initially controller.showArchivedJobs.value = false; controller.fetchProjectJobs(refresh: true); searchController.addListener(() { controller.updateJobSearch(searchController.text); }); } @override void dispose() { searchController.dispose(); super.dispose(); } Widget _buildSearchBar() { return Padding( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), child: Row( children: [ Expanded( child: SizedBox( height: 35, child: TextField( controller: searchController, decoration: InputDecoration( contentPadding: const EdgeInsets.symmetric(horizontal: 12), prefixIcon: const Icon(Icons.search, size: 20, color: Colors.grey), suffixIcon: ValueListenableBuilder( valueListenable: searchController, builder: (context, value, _) { if (value.text.isEmpty) return const SizedBox.shrink(); return IconButton( icon: const Icon(Icons.clear, size: 20, color: Colors.grey), onPressed: () { searchController.clear(); controller.updateJobSearch(''); }, ); }, ), hintText: 'Search jobs...', filled: true, fillColor: Colors.white, border: OutlineInputBorder( borderRadius: BorderRadius.circular(5), borderSide: BorderSide(color: Colors.grey.shade300), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(5), borderSide: BorderSide(color: Colors.grey.shade300), ), ), ), ), ), const SizedBox(width: 10), Container( height: 35, padding: const EdgeInsets.symmetric(horizontal: 5), decoration: BoxDecoration( color: Colors.white, border: Border.all(color: Colors.grey.shade300), borderRadius: BorderRadius.circular(5), ), child: Obx(() { return Row( mainAxisSize: MainAxisSize.min, children: [ const Text( "Archived", style: TextStyle(fontSize: 14, color: Colors.black87), ), Switch( value: controller.showArchivedJobs.value, onChanged: (val) { controller.showArchivedJobs.value = val; controller.fetchProjectJobs(refresh: true); }, activeColor: Colors.blue, inactiveThumbColor: Colors.grey, ), ], ); }), ), ], ), ); } @override Widget build(BuildContext context) { return Column( children: [ _buildSearchBar(), Expanded( child: Obx(() { if (controller.isJobLoading.value && controller.filteredJobList.isEmpty) { return const Center(child: CircularProgressIndicator()); } if (controller.jobErrorMessage.value.isNotEmpty && controller.filteredJobList.isEmpty) { return Center( child: MyText.bodyMedium(controller.jobErrorMessage.value)); } if (controller.filteredJobList.isEmpty) { return Center(child: MyText.bodyMedium("No jobs found")); } return ListView.separated( controller: widget.scrollController, padding: const EdgeInsets.fromLTRB(12, 0, 12, 12), itemCount: controller.filteredJobList.length + 1, separatorBuilder: (_, __) => const SizedBox(height: 12), itemBuilder: (context, index) { if (index == controller.filteredJobList.length) { return controller.hasMoreJobs.value ? const Padding( padding: EdgeInsets.symmetric(vertical: 16), child: Center(child: CircularProgressIndicator()), ) : const SizedBox.shrink(); } final job = controller.filteredJobList[index]; return Stack( children: [ AbsorbPointer( absorbing: controller.showArchivedJobs .value, // Disable interactions if archived child: InkWell( onTap: () { if (!controller.showArchivedJobs.value) { Get.to(() => JobDetailsScreen(jobId: job.id)); } }, child: Card( elevation: 3, shadowColor: Colors.black26, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(5), ), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ MyText.titleMedium(job.title, fontWeight: 700), MySpacing.height(6), MyText.bodySmall( job.description.isNotEmpty ? job.description : "No description provided", color: Colors.grey[700], ), if (job.tags != null && job.tags!.isNotEmpty) Padding( padding: const EdgeInsets.only(top: 2), child: Wrap( spacing: 2, runSpacing: 4, children: job.tags!.map((tag) { return Chip( label: Text( tag.name, style: const TextStyle(fontSize: 12), ), backgroundColor: Colors.grey[200], shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(5), ), ); }).toList(), ), ), MySpacing.height(8), Row( children: [ if (job.assignees != null && job.assignees!.isNotEmpty) ...job.assignees!.map((assignee) { return Padding( padding: const EdgeInsets.only(right: 6), child: Avatar( firstName: assignee.firstName, lastName: assignee.lastName, size: 24, imageUrl: assignee.photo.isNotEmpty ? assignee.photo : null, ), ); }).toList(), ], ), MySpacing.height(8), Row( children: [ const Icon(Icons.calendar_today_outlined, size: 14, color: Colors.grey), MySpacing.width(4), Text( "${DateTimeUtils.convertUtcToLocal(job.startDate, format: 'dd MMM yyyy')} to " "${DateTimeUtils.convertUtcToLocal(job.dueDate, format: 'dd MMM yyyy')}", style: const TextStyle( fontSize: 12, color: Colors.grey), ), const Spacer(), Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4), decoration: BoxDecoration( color: job.status.name.toLowerCase() == 'completed' ? Colors.green[100] : Colors.orange[100], borderRadius: BorderRadius.circular(5), ), child: Text( job.status.displayName, style: TextStyle( fontSize: 12, color: job.status.name.toLowerCase() == 'completed' ? Colors.green[800] : Colors.orange[800], fontWeight: FontWeight.w600, ), ), ), ], ), ], ), ), ), ), ), Obx(() { final isArchivedJob = controller.showArchivedJobs.value; if (job.status.id != JobStatus.closed && job.status.id != JobStatus.reviewDone) { return const SizedBox.shrink(); } return Positioned( top: 10, right: 10, child: GestureDetector( onTap: () async { final confirmed = await showDialog( context: context, builder: (_) => ConfirmDialog( title: isArchivedJob ? "Restore Job?" : "Archive Job?", message: isArchivedJob ? "Are you sure you want to restore this job?" : "Are you sure you want to archive this job?", confirmText: "Yes", cancelText: "Cancel", icon: isArchivedJob ? Icons.restore : Icons.archive_outlined, confirmColor: Colors.green, confirmIcon: Icons.check, onConfirm: () async { final operations = [ { "op": "replace", "path": "/isArchive", "value": isArchivedJob ? false : true } ]; final success = await ApiService.editServiceProjectJobApi( jobId: job.id , operations: operations, ); if (success) { showAppSnackbar( title: "Success", message: isArchivedJob ? "Job restored successfully" : "Job archived successfully", type: SnackbarType.success, ); controller.fetchProjectJobs(refresh: true); } else { showAppSnackbar( title: "Error", message: isArchivedJob ? "Failed to restore job. Please try again." : "Failed to archive job. Please try again.", type: SnackbarType.error, ); } }, ), ); if (confirmed != true) return; }, child: Container( padding: const EdgeInsets.all(6), decoration: BoxDecoration( color: isArchivedJob ? Colors.blue : Colors.red, shape: BoxShape.circle, ), child: Icon( isArchivedJob ? Icons.restore : Icons.archive_outlined, size: 20, color: Colors.white, ), ), ), ); }), ], ); }, ); }), ), ], ); } }