import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:intl/intl.dart'; import 'package:marco/controller/service_project/service_project_details_screen_controller.dart'; import 'package:marco/helpers/utils/launcher_utils.dart'; import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/model/service_project/add_service_project_job_bottom_sheet.dart'; import 'package:marco/helpers/utils/date_time_utils.dart'; import 'package:marco/view/service_project/service_project_job_detail_screen.dart'; import 'package:marco/helpers/widgets/custom_app_bar.dart'; import 'package:marco/helpers/widgets/avatar.dart'; class ServiceProjectDetailsScreen extends StatefulWidget { final String projectId; const ServiceProjectDetailsScreen({super.key, required this.projectId}); @override State createState() => _ServiceProjectDetailsScreenState(); } class _ServiceProjectDetailsScreenState extends State with SingleTickerProviderStateMixin, UIMixin { late final TabController _tabController; late final ServiceProjectDetailsController controller; final ScrollController _jobScrollController = ScrollController(); @override void initState() { super.initState(); _tabController = TabController(length: 2, vsync: this); controller = Get.put(ServiceProjectDetailsController()); WidgetsBinding.instance.addPostFrameCallback((_) { controller.setProjectId(widget.projectId); }); _tabController.addListener(() { if (!_tabController.indexIsChanging) { setState(() {}); // rebuild to show/hide FAB if (_tabController.index == 1 && controller.jobList.isEmpty) { controller.fetchProjectJobs(); } } }); _jobScrollController.addListener(() { if (_tabController.index == 1 && _jobScrollController.position.pixels >= _jobScrollController.position.maxScrollExtent - 100) { controller.fetchMoreJobs(); } }); } @override void dispose() { _tabController.dispose(); _jobScrollController.dispose(); super.dispose(); } // ---------------- Helper Widgets ---------------- Widget _buildDetailRow({ required IconData icon, required String label, required String value, VoidCallback? onTap, VoidCallback? onLongPress, bool isActionable = false, }) { return Padding( padding: const EdgeInsets.symmetric(vertical: 12), child: InkWell( onTap: isActionable && value != 'NA' ? onTap : null, onLongPress: isActionable && value != 'NA' ? onLongPress : null, borderRadius: BorderRadius.circular(5), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: const EdgeInsets.all(8), child: Icon( icon, size: 20, ), ), MySpacing.width(16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ MyText.bodySmall( label, fontSize: 12, color: Colors.grey[600], fontWeight: 500, ), MySpacing.height(4), MyText.bodyMedium( value, fontSize: 15, color: isActionable && value != 'NA' ? Colors.blueAccent : Colors.black87, fontWeight: 500, decoration: isActionable && value != 'NA' ? TextDecoration.underline : TextDecoration.none, ), ], ), ), if (isActionable && value != 'NA') Icon(Icons.chevron_right, color: Colors.grey[400], size: 20), ], ), ), ); } Widget _buildSectionCard({ required String title, required IconData titleIcon, required List children, }) { return Card( elevation: 2, shadowColor: Colors.black12, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(titleIcon, size: 20), MySpacing.width(8), MyText.bodyLarge( title, fontSize: 16, fontWeight: 700, color: Colors.black87, ), ], ), MySpacing.height(8), const Divider(), ...children, ], ), ), ); } String _formatDate(DateTime? date) { if (date == null) return 'NA'; try { return DateFormat('d/M/yyyy').format(date); } catch (_) { return 'NA'; } } Widget _buildProfileTab() { final project = controller.projectDetail.value; if (project == null) { return Center(child: MyText.bodyMedium("No project data")); } return Padding( padding: MySpacing.all(12), child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ // Header Card Card( elevation: 2, shadowColor: Colors.black12, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(5)), child: Padding( padding: const EdgeInsets.all(16), child: Row( children: [ const Icon(Icons.work_outline, size: 35), MySpacing.width(16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ MyText.titleMedium(project.name, fontWeight: 700), MySpacing.height(6), MyText.bodySmall(project.client?.name ?? 'N/A', fontWeight: 500), ], ), ), ], ), ), ), MySpacing.height(16), // Project Information _buildSectionCard( title: 'Project Information', titleIcon: Icons.info_outline, children: [ _buildDetailRow( icon: Icons.calendar_today_outlined, label: 'Assigned Date', value: _formatDate(project.assignedDate), ), _buildDetailRow( icon: Icons.location_on_outlined, label: 'Address', value: project.address, ), _buildDetailRow( icon: Icons.people_outline, label: 'Contact Name', value: project.contactName, ), _buildDetailRow( icon: Icons.phone_outlined, label: 'Contact Phone', value: project.contactPhone, isActionable: true, onTap: () => LauncherUtils.launchPhone(project.contactPhone), onLongPress: () => LauncherUtils.copyToClipboard( project.contactPhone, typeLabel: 'Phone'), ), _buildDetailRow( icon: Icons.email_outlined, label: 'Contact Email', value: project.contactEmail, isActionable: true, onTap: () => LauncherUtils.launchEmail(project.contactEmail), onLongPress: () => LauncherUtils.copyToClipboard( project.contactEmail, typeLabel: 'Email'), ), ], ), MySpacing.height(12), // Status if (project.status != null) _buildSectionCard( title: 'Status', titleIcon: Icons.flag_outlined, children: [ _buildDetailRow( icon: Icons.info_outline, label: 'Status', value: project.status!.status, ), ], ), // Services if (project.services != null && project.services!.isNotEmpty) _buildSectionCard( title: 'Services', titleIcon: Icons.miscellaneous_services_outlined, children: project.services!.map((service) { return _buildDetailRow( icon: Icons.build_outlined, label: service.name, value: service.description ?? '-', ); }).toList(), ), MySpacing.height(12), // Client Section if (project.client != null) _buildSectionCard( title: 'Client Information', titleIcon: Icons.business_outlined, children: [ _buildDetailRow( icon: Icons.person_outline, label: 'Client Name', value: project.client!.name, ), _buildDetailRow( icon: Icons.phone_outlined, label: 'Client Phone', value: project.client!.contactNumber ?? 'NA', isActionable: true, onTap: () => LauncherUtils.launchPhone( project.client!.contactNumber ?? ''), onLongPress: () => LauncherUtils.copyToClipboard( project.client!.contactNumber ?? '', typeLabel: 'Phone'), ), ], ), MySpacing.height(40), ], ), ), ); } Widget _buildJobsTab() { return Obx(() { if (controller.isJobLoading.value && controller.jobList.isEmpty) { return const Center(child: CircularProgressIndicator()); } if (controller.jobErrorMessage.value.isNotEmpty && controller.jobList.isEmpty) { return Center( child: MyText.bodyMedium(controller.jobErrorMessage.value)); } if (controller.jobList.isEmpty) { return Center(child: MyText.bodyMedium("No jobs found")); } return ListView.separated( controller: _jobScrollController, padding: const EdgeInsets.fromLTRB(12, 12, 12, 80), itemCount: controller.jobList.length + 1, separatorBuilder: (_, __) => const SizedBox(height: 12), itemBuilder: (context, index) { if (index == controller.jobList.length) { return controller.hasMoreJobs.value ? const Padding( padding: EdgeInsets.symmetric(vertical: 16), child: Center(child: CircularProgressIndicator()), ) : const SizedBox.shrink(); } final job = controller.jobList[index]; return 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: [ // Job Title MyText.titleMedium(job.title, fontWeight: 700), MySpacing.height(6), // Job Description MyText.bodySmall( job.description.isNotEmpty ? job.description : "No description provided", color: Colors.grey[700], ), // Tags 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), // Assignees & Status Row( children: [ if (job.assignees != null && job.assignees!.isNotEmpty) ...job.assignees!.map((assignee) { return Padding( padding: const EdgeInsets.only(right: 6), child: CircleAvatar( radius: 12, backgroundImage: assignee.photo.isNotEmpty ? NetworkImage(assignee.photo) : null, child: assignee.photo.isEmpty ? Text(assignee.firstName[0]) : null, ), ); }).toList(), ], ), MySpacing.height(8), // Date Row with Status Chip Row( children: [ // Dates (same as existing) 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(), // Status Chip 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, ), ), ), ], ), ], ), ), ); }, ); }); } Widget _buildJobsTab() { return Obx(() { if (controller.isJobLoading.value && controller.jobList.isEmpty) { return const Center(child: CircularProgressIndicator()); } if (controller.jobErrorMessage.value.isNotEmpty && controller.jobList.isEmpty) { return Center( child: MyText.bodyMedium(controller.jobErrorMessage.value)); } if (controller.jobList.isEmpty) { return Center(child: MyText.bodyMedium("No jobs found")); } return ListView.separated( controller: _jobScrollController, padding: const EdgeInsets.fromLTRB(12, 12, 12, 80), itemCount: controller.jobList.length + 1, separatorBuilder: (_, __) => const SizedBox(height: 12), itemBuilder: (context, index) { if (index == controller.jobList.length) { return controller.hasMoreJobs.value ? const Padding( padding: EdgeInsets.symmetric(vertical: 16), child: Center(child: CircularProgressIndicator()), ) : const SizedBox.shrink(); } final job = controller.jobList[index]; return InkWell( onTap: () { 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], ), // Tags 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), // Assignees & Status 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), // Date Row with Status Chip Row( children: [ // Dates (same as existing) 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(), // Status Chip 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, ), ), ), ], ), ], ), ), ), ); }, ); }); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: const Color(0xFFF5F5F5), appBar: CustomAppBar( title: "Service Project Details", onBackPressed: () => Get.toNamed('/dashboard/service-projects'), ), body: SafeArea( child: Column( children: [ // TabBar Container( color: Colors.white, child: TabBar( controller: _tabController, labelColor: Colors.black, unselectedLabelColor: Colors.grey, indicatorColor: Colors.red, indicatorWeight: 3, isScrollable: false, tabs: [ Tab(child: MyText.bodyMedium("Profile")), Tab(child: MyText.bodyMedium("Jobs")), ], ), ), // TabBarView Expanded( child: Obx(() { if (controller.isLoading.value && controller.projectDetail.value == null) { return const Center(child: CircularProgressIndicator()); } if (controller.errorMessage.value.isNotEmpty && controller.projectDetail.value == null) { return Center( child: MyText.bodyMedium(controller.errorMessage.value)); } return TabBarView( controller: _tabController, children: [ _buildProfileTab(), _buildJobsTab(), ], ); }), ), ], ), ), floatingActionButton: _tabController.index == 1 ? FloatingActionButton.extended( onPressed: () { showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (_) => AddServiceProjectJobBottomSheet( projectId: widget.projectId, ), ); }, backgroundColor: contentTheme.primary, icon: const Icon(Icons.add), label: MyText.bodyMedium("Add Job", color: Colors.white), ) : null, floatingActionButtonLocation: FloatingActionButtonLocation.endFloat, ); } }