import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:intl/intl.dart'; import 'package:on_field_work/controller/service_project/service_project_details_screen_controller.dart'; import 'package:on_field_work/helpers/utils/launcher_utils.dart'; import 'package:on_field_work/helpers/widgets/my_spacing.dart'; import 'package:on_field_work/helpers/utils/mixins/ui_mixin.dart'; import 'package:on_field_work/helpers/widgets/my_text.dart'; import 'package:on_field_work/model/service_project/add_service_project_job_bottom_sheet.dart'; import 'package:on_field_work/helpers/widgets/custom_app_bar.dart'; import 'package:on_field_work/helpers/widgets/avatar.dart'; import 'package:on_field_work/model/service_project/service_project_allocation_bottomsheet.dart'; import 'package:on_field_work/model/employees/employee_model.dart'; import 'package:on_field_work/view/service_project/jobs_tab.dart'; import 'package:on_field_work/helpers/widgets/my_refresh_indicator.dart'; class ServiceProjectDetailsScreen extends StatefulWidget { final String projectId; final String? projectName; const ServiceProjectDetailsScreen( {super.key, required this.projectId, this.projectName}); @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: 3, vsync: this); controller = Get.put(ServiceProjectDetailsController()); WidgetsBinding.instance.addPostFrameCallback((_) { controller.setProjectId(widget.projectId); }); _tabController.addListener(() { if (!_tabController.indexIsChanging) { setState(() {}); if (_tabController.index == 1 && controller.jobList.isEmpty) { controller.fetchProjectJobs(); } else if (_tabController.index == 2 && controller.teamList.isEmpty) { controller.fetchProjectTeams(); } } }); _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 MyRefreshIndicator( onRefresh: () async { await controller.fetchProjectDetail(); }, backgroundColor: Colors.indigo, color: Colors.white, child: SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), padding: MySpacing.all(12), 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 _buildTeamsTab() { return Obx(() { if (controller.isTeamLoading.value) { return const Center(child: CircularProgressIndicator()); } if (controller.teamErrorMessage.value.isNotEmpty && controller.teamList.isEmpty) { return Center( child: MyText.bodyMedium(controller.teamErrorMessage.value)); } if (controller.teamList.isEmpty) { return Center(child: MyText.bodyMedium("No team members found")); } // Group team members by role final Map roleGroups = {}; for (var team in controller.teamList) { roleGroups.putIfAbsent(team.teamRole.id, () => []).add(team); } return MyRefreshIndicator( onRefresh: () async { await controller.fetchProjectTeams(); }, backgroundColor: Colors.indigo, color: Colors.white, child: ListView.separated( padding: const EdgeInsets.all(12), itemCount: roleGroups.keys.length, separatorBuilder: (_, __) => const SizedBox(height: 12), itemBuilder: (context, index) { final roleId = roleGroups.keys.elementAt(index); final teamMembers = roleGroups[roleId]!; final roleName = teamMembers.first.teamRole.name; return Card( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8)), elevation: 3, shadowColor: Colors.black26, child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Role header MyText.bodyLarge( roleName, fontWeight: 700, color: Colors.black87, ), const Divider(height: 20, thickness: 1), // List of team members inside this role card ...teamMembers.map((team) { return Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Row( children: [ Avatar( firstName: team.employee.firstName, lastName: team.employee.lastName, size: 32, imageUrl: (team.employee.photo?.isNotEmpty ?? false) ? team.employee.photo : null, ), MySpacing.width(12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ MyText.titleMedium( "${team.employee.firstName} ${team.employee.lastName}", fontWeight: 600, ), MyText.bodySmall( "Status: ${team.isActive ? 'Active' : 'Inactive'}", color: Colors.grey[700], ), ], ), ), ], ), ); }).toList(), ], ), ), ); }, ), ); }); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: const Color(0xFFF5F5F5), appBar: CustomAppBar( title: "Service Project Details", projectName: widget.projectName, 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")), Tab(child: MyText.bodyMedium("Teams")), ], ), ), // 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(), JobsTab( scrollController: _jobScrollController, projectName: widget.projectName ?? '', ), _buildTeamsTab(), ], ); }), ), ], ), ), 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), ) : _tabController.index == 2 ? FloatingActionButton.extended( onPressed: () async { // Prepare existing allocations grouped by role Map> allocationsMap = {}; for (var team in controller.teamList) { if (!allocationsMap.containsKey(team.teamRole.id)) { allocationsMap[team.teamRole.id] = []; } allocationsMap[team.teamRole.id]!.add(EmployeeModel( id: team.employee.id, jobRoleID: team.teamRole.id, employeeId: team.employee.id, name: "${team.employee.firstName} ${team.employee.lastName}", designation: team.teamRole.name, firstName: team.employee.firstName, lastName: team.employee.lastName, activity: 0, action: 0, jobRole: team.teamRole.name, email: team.employee.email ?? '', phoneNumber: '', )); } final existingAllocations = allocationsMap.entries.map((entry) { final role = controller.teamList .firstWhere((team) => team.teamRole.id == entry.key) .teamRole; return RoleEmployeeAllocation( role: role, employees: entry.value); }).toList(); final result = await showModalBottomSheet( context: context, isScrollControlled: true, builder: (_) => SimpleProjectAllocationBottomSheet( projectId: widget.projectId, existingAllocations: existingAllocations, ), ); if (result == true) { controller.fetchProjectTeams(); } }, backgroundColor: contentTheme.primary, icon: const Icon(Icons.group_add), label: MyText.bodyMedium("Manage Team", color: Colors.white), ) : null, floatingActionButtonLocation: FloatingActionButtonLocation.endFloat, ); } }