375 lines
16 KiB
Dart

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<JobsTab> {
final TextEditingController searchController = TextEditingController();
late ServiceProjectDetailsController controller;
@override
void initState() {
super.initState();
controller = Get.find<ServiceProjectDetailsController>();
// 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<bool>(
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,
),
),
),
);
}),
],
);
},
);
}),
),
],
);
}
}