From 1de5e7fae712891661dc46901b03831c9e27b45b Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Wed, 12 Nov 2025 15:36:10 +0530 Subject: [PATCH] added service projet screen --- ...ice_project_details_screen_controller.dart | 53 +++ .../service_project_screen_controller.dart | 36 ++ lib/helpers/services/api_endpoints.dart | 8 +- lib/helpers/services/api_service.dart | 76 +++- .../service_projects_details_model.dart | 241 +++++++++++ .../service_projects_list_model.dart | 127 ++++++ lib/routes.dart | 6 - .../service_project_details_screen.dart | 405 ++++++++++++++++++ .../service_project_screen.dart | 364 ++++++++++++++++ .../taskPlanning/daily_task_planning.dart | 4 +- 10 files changed, 1309 insertions(+), 11 deletions(-) create mode 100644 lib/controller/service_project/service_project_details_screen_controller.dart create mode 100644 lib/controller/service_project/service_project_screen_controller.dart create mode 100644 lib/model/service_project/service_projects_details_model.dart create mode 100644 lib/model/service_project/service_projects_list_model.dart create mode 100644 lib/view/service_project/service_project_details_screen.dart create mode 100644 lib/view/service_project/service_project_screen.dart diff --git a/lib/controller/service_project/service_project_details_screen_controller.dart b/lib/controller/service_project/service_project_details_screen_controller.dart new file mode 100644 index 0000000..65b9f38 --- /dev/null +++ b/lib/controller/service_project/service_project_details_screen_controller.dart @@ -0,0 +1,53 @@ +import 'package:get/get.dart'; +import 'package:marco/helpers/services/api_service.dart'; +import 'package:marco/model/service_project/service_projects_details_model.dart'; + +class ServiceProjectDetailsController extends GetxController { + // Selected project id + var projectId = ''.obs; + + // Project details + var projectDetail = Rxn(); + + // Loading state + var isLoading = false.obs; + + // Error message + var errorMessage = ''.obs; + + /// Set project id and fetch its details + void setProjectId(String id) { + projectId.value = id; + fetchProjectDetail(); + } + + /// Fetch project detail from API + Future fetchProjectDetail() async { + if (projectId.value.isEmpty) { + errorMessage.value = "Invalid project ID"; + return; + } + + isLoading.value = true; + errorMessage.value = ''; + + try { + final result = await ApiService.getServiceProjectDetailApi(projectId.value); + + if (result != null && result.data != null) { + projectDetail.value = result.data!; + } else { + errorMessage.value = result?.message ?? "Failed to fetch project details"; + } + } catch (e) { + errorMessage.value = "Error: $e"; + } finally { + isLoading.value = false; + } + } + + /// Refresh project details manually + Future refresh() async { + await fetchProjectDetail(); + } +} diff --git a/lib/controller/service_project/service_project_screen_controller.dart b/lib/controller/service_project/service_project_screen_controller.dart new file mode 100644 index 0000000..ae0b423 --- /dev/null +++ b/lib/controller/service_project/service_project_screen_controller.dart @@ -0,0 +1,36 @@ +import 'package:get/get.dart'; +import 'package:marco/helpers/services/api_service.dart'; +import 'package:marco/model/service_project/service_projects_list_model.dart'; + +class ServiceProjectController extends GetxController { + var projects = [].obs; + var isLoading = false.obs; + var searchQuery = ''.obs; + + RxList get filteredProjects { + if (searchQuery.value.isEmpty) return projects; + return projects + .where((p) => + p.name.toLowerCase().contains(searchQuery.value.toLowerCase()) || + p.contactPerson.toLowerCase().contains(searchQuery.value.toLowerCase())) + .toList() + .obs; + } + + Future fetchProjects({int pageNumber = 1, int pageSize = 20}) async { + try { + isLoading.value = true; + final result = await ApiService.getServiceProjectsListApi( + pageNumber: pageNumber, pageSize: pageSize); + if (result != null && result.data != null) { + projects.assignAll(result.data!.data ?? []); + } + } finally { + isLoading.value = false; + } + } + + void updateSearch(String query) { + searchQuery.value = query; + } +} diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index de12e95..e73e150 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -1,7 +1,9 @@ class ApiEndpoints { - static const String baseUrl = "https://stageapi.marcoaiot.com/api"; + // static const String baseUrl = "https://stageapi.marcoaiot.com/api"; // static const String baseUrl = "https://api.marcoaiot.com/api"; // static const String baseUrl = "https://devapi.marcoaiot.com/api"; + static const String baseUrl = "https://mapi.marcoaiot.com/api"; + static const String getMasterCurrencies = "/Master/currencies/list"; static const String getMasterExpensesCategories = @@ -127,4 +129,8 @@ class ApiEndpoints { static const String getAssignedServices = "/Project/get/assigned/services"; static const String getAdvancePayments = '/Expense/get/transactions'; + + // Service Project Module API Endpoints + static const String getServiceProjectsList = "/serviceproject/list"; + static const String getServiceProjectDetail = "/serviceproject/details"; } diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index d0d942d..48e5179 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -33,6 +33,8 @@ import 'package:marco/model/finance/payment_request_list_model.dart'; import 'package:marco/model/finance/payment_request_filter.dart'; import 'package:marco/model/finance/payment_request_details_model.dart'; import 'package:marco/model/finance/advance_payment_model.dart'; +import 'package:marco/model/service_project/service_projects_list_model.dart'; +import 'package:marco/model/service_project/service_projects_details_model.dart'; class ApiService { static const bool enableLogs = true; @@ -302,6 +304,76 @@ class ApiService { } } + // Service Project Module APIs + + /// Get details of a single service project +static Future getServiceProjectDetailApi(String projectId) async { + final endpoint = "${ApiEndpoints.getServiceProjectDetail}/$projectId"; + logSafe("Fetching details for Service Project ID: $projectId"); + + try { + final response = await _getRequest(endpoint); + + if (response == null) { + logSafe("Service Project Detail request failed: null response", level: LogLevel.error); + return null; + } + + final jsonResponse = _parseResponseForAllData( + response, + label: "Service Project Detail", + ); + + if (jsonResponse != null) { + return ServiceProjectDetailModel.fromJson(jsonResponse); + } + } catch (e, stack) { + logSafe("Exception during getServiceProjectDetailApi: $e", level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } + + return null; +} + + /// Get Service Project List + static Future getServiceProjectsListApi({ + int pageNumber = 1, + int pageSize = 20, + }) async { + const endpoint = ApiEndpoints.getServiceProjectsList; + logSafe("Fetching Service Project List"); + + try { + final queryParams = { + 'pageNumber': pageNumber.toString(), + 'pageSize': pageSize.toString(), + }; + + final response = await _getRequest(endpoint, queryParams: queryParams); + + if (response == null) { + logSafe("Service Project List request failed: null response", + level: LogLevel.error); + return null; + } + + final jsonResponse = _parseResponseForAllData( + response, + label: "Service Project List", + ); + + if (jsonResponse != null) { + return ServiceProjectListModel.fromJson(jsonResponse); + } + } catch (e, stack) { + logSafe("Exception during getServiceProjectsListApi: $e", + level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } + + return null; + } + /// Edit Expense Payment Request static Future editExpensePaymentRequestApi({ required String id, @@ -1707,8 +1779,8 @@ class ApiService { String? reimbursedById, double? baseAmount, double? taxAmount, - double? tdsPercent, - double? netPayable, + double? tdsPercent, + double? netPayable, }) async { final Map payload = { "expenseId": expenseId, diff --git a/lib/model/service_project/service_projects_details_model.dart b/lib/model/service_project/service_projects_details_model.dart new file mode 100644 index 0000000..47cad69 --- /dev/null +++ b/lib/model/service_project/service_projects_details_model.dart @@ -0,0 +1,241 @@ +class ServiceProjectDetailModel { + final bool success; + final String message; + final ProjectDetail? data; + final dynamic errors; + final int statusCode; + final DateTime timestamp; + + ServiceProjectDetailModel({ + required this.success, + required this.message, + this.data, + this.errors, + required this.statusCode, + required this.timestamp, + }); + + factory ServiceProjectDetailModel.fromJson(Map json) { + return ServiceProjectDetailModel( + success: json['success'] ?? false, + message: json['message'] ?? '', + data: json['data'] != null ? ProjectDetail.fromJson(json['data']) : null, + errors: json['errors'], + statusCode: json['statusCode'] ?? 0, + timestamp: DateTime.parse(json['timestamp'] ?? DateTime.now().toIso8601String()), + ); + } + + Map toJson() => { + 'success': success, + 'message': message, + 'data': data?.toJson(), + 'errors': errors, + 'statusCode': statusCode, + 'timestamp': timestamp.toIso8601String(), + }; +} + +class ProjectDetail { + final String id; + final String name; + final String shortName; + final String address; + final DateTime assignedDate; + final Status? status; + final Client? client; + final List? services; + final int numberOfJobs; + final String contactName; + final String contactPhone; + final String contactEmail; + final DateTime createdAt; + final User? createdBy; + final DateTime updatedAt; + final User? updatedBy; + + ProjectDetail({ + required this.id, + required this.name, + required this.shortName, + required this.address, + required this.assignedDate, + this.status, + this.client, + this.services, + required this.numberOfJobs, + required this.contactName, + required this.contactPhone, + required this.contactEmail, + required this.createdAt, + this.createdBy, + required this.updatedAt, + this.updatedBy, + }); + + factory ProjectDetail.fromJson(Map json) { + return ProjectDetail( + id: json['id'] ?? '', + name: json['name'] ?? '', + shortName: json['shortName'] ?? '', + address: json['address'] ?? '', + assignedDate: DateTime.parse(json['assignedDate'] ?? DateTime.now().toIso8601String()), + status: json['status'] != null ? Status.fromJson(json['status']) : null, + client: json['client'] != null ? Client.fromJson(json['client']) : null, + services: json['services'] != null + ? List.from(json['services'].map((x) => Service.fromJson(x))) + : [], + numberOfJobs: json['numberOfJobs'] ?? 0, + contactName: json['contactName'] ?? '', + contactPhone: json['contactPhone'] ?? '', + contactEmail: json['contactEmail'] ?? '', + createdAt: DateTime.parse(json['createdAt'] ?? DateTime.now().toIso8601String()), + createdBy: json['createdBy'] != null ? User.fromJson(json['createdBy']) : null, + updatedAt: DateTime.parse(json['updatedAt'] ?? DateTime.now().toIso8601String()), + updatedBy: json['updatedBy'] != null ? User.fromJson(json['updatedBy']) : null, + ); + } + + Map toJson() => { + 'id': id, + 'name': name, + 'shortName': shortName, + 'address': address, + 'assignedDate': assignedDate.toIso8601String(), + 'status': status?.toJson(), + 'client': client?.toJson(), + 'services': services?.map((x) => x.toJson()).toList(), + 'numberOfJobs': numberOfJobs, + 'contactName': contactName, + 'contactPhone': contactPhone, + 'contactEmail': contactEmail, + 'createdAt': createdAt.toIso8601String(), + 'createdBy': createdBy?.toJson(), + 'updatedAt': updatedAt.toIso8601String(), + 'updatedBy': updatedBy?.toJson(), + }; +} + +class Status { + final String id; + final String status; + + Status({required this.id, required this.status}); + + factory Status.fromJson(Map json) => + Status(id: json['id'] ?? '', status: json['status'] ?? ''); + + Map toJson() => {'id': id, 'status': status}; +} + +class Client { + final String id; + final String name; + final String? email; + final String? contactPerson; + final String? address; + final String? contactNumber; + final int? sprid; + + Client({ + required this.id, + required this.name, + this.email, + this.contactPerson, + this.address, + this.contactNumber, + this.sprid, + }); + + factory Client.fromJson(Map json) => Client( + id: json['id'] ?? '', + name: json['name'] ?? '', + email: json['email'], + contactPerson: json['contactPerson'], + address: json['address'], + contactNumber: json['contactNumber'], + sprid: json['sprid'], + ); + + Map toJson() => { + 'id': id, + 'name': name, + 'email': email, + 'contactPerson': contactPerson, + 'address': address, + 'contactNumber': contactNumber, + 'sprid': sprid, + }; +} + +class Service { + final String id; + final String name; + final String? description; + final bool isSystem; + final bool isActive; + + Service({ + required this.id, + required this.name, + this.description, + required this.isSystem, + required this.isActive, + }); + + factory Service.fromJson(Map json) => Service( + id: json['id'] ?? '', + name: json['name'] ?? '', + description: json['description'], + isSystem: json['isSystem'] ?? false, + isActive: json['isActive'] ?? false, + ); + + Map toJson() => { + 'id': id, + 'name': name, + 'description': description, + 'isSystem': isSystem, + 'isActive': isActive, + }; +} + +class User { + final String id; + final String firstName; + final String lastName; + final String email; + final String? photo; + final String? jobRoleId; + final String? jobRoleName; + + User({ + required this.id, + required this.firstName, + required this.lastName, + required this.email, + this.photo, + this.jobRoleId, + this.jobRoleName, + }); + + factory User.fromJson(Map json) => User( + id: json['id'] ?? '', + firstName: json['firstName'] ?? '', + lastName: json['lastName'] ?? '', + email: json['email'] ?? '', + photo: json['photo'], + jobRoleId: json['jobRoleId'], + jobRoleName: json['jobRoleName'], + ); + + Map toJson() => { + 'id': id, + 'firstName': firstName, + 'lastName': lastName, + 'email': email, + 'photo': photo, + 'jobRoleId': jobRoleId, + 'jobRoleName': jobRoleName, + }; +} diff --git a/lib/model/service_project/service_projects_list_model.dart b/lib/model/service_project/service_projects_list_model.dart new file mode 100644 index 0000000..5d9da34 --- /dev/null +++ b/lib/model/service_project/service_projects_list_model.dart @@ -0,0 +1,127 @@ +class ServiceProjectListModel { + final bool success; + final String message; + final ProjectData? data; + final dynamic errors; + final int statusCode; + final DateTime timestamp; + + ServiceProjectListModel({ + required this.success, + required this.message, + this.data, + this.errors, + required this.statusCode, + required this.timestamp, + }); + + factory ServiceProjectListModel.fromJson(Map json) { + return ServiceProjectListModel( + success: json['success'] ?? false, + message: json['message'] ?? '', + data: json['data'] != null ? ProjectData.fromJson(json['data']) : null, + errors: json['errors'], + statusCode: json['statusCode'] ?? 0, + timestamp: DateTime.parse(json['timestamp'] ?? DateTime.now().toIso8601String()), + ); + } + + Map toJson() => { + 'success': success, + 'message': message, + 'data': data?.toJson(), + 'errors': errors, + 'statusCode': statusCode, + 'timestamp': timestamp.toIso8601String(), + }; +} + +class ProjectData { + final int currentPage; + final int totalPages; + final int totalEntities; + final List? data; + + ProjectData({ + required this.currentPage, + required this.totalPages, + required this.totalEntities, + this.data, + }); + + factory ProjectData.fromJson(Map json) { + return ProjectData( + currentPage: json['currentPage'] ?? 1, + totalPages: json['totalPages'] ?? 1, + totalEntities: json['totalEntites'] ?? 0, + data: json['data'] != null + ? List.from(json['data'].map((x) => ProjectItem.fromJson(x))) + : [], + ); + } + + Map toJson() => { + 'currentPage': currentPage, + 'totalPages': totalPages, + 'totalEntites': totalEntities, + 'data': data?.map((x) => x.toJson()).toList(), + }; +} + +class ProjectItem { + final String id; + final String name; + final String shortName; + final String projectAddress; + final String contactPerson; + final DateTime startDate; + final DateTime endDate; + final String projectStatusId; + final int teamSize; + final double completedWork; + final double plannedWork; + + ProjectItem({ + required this.id, + required this.name, + required this.shortName, + required this.projectAddress, + required this.contactPerson, + required this.startDate, + required this.endDate, + required this.projectStatusId, + required this.teamSize, + required this.completedWork, + required this.plannedWork, + }); + + factory ProjectItem.fromJson(Map json) { + return ProjectItem( + id: json['id'] ?? '', + name: json['name'] ?? '', + shortName: json['shortName'] ?? '', + projectAddress: json['projectAddress'] ?? '', + contactPerson: json['contactPerson'] ?? '', + startDate: DateTime.parse(json['startDate'] ?? DateTime.now().toIso8601String()), + endDate: DateTime.parse(json['endDate'] ?? DateTime.now().toIso8601String()), + projectStatusId: json['projectStatusId'] ?? '', + teamSize: json['teamSize'] ?? 0, + completedWork: (json['completedWork']?.toDouble() ?? 0.0), + plannedWork: (json['plannedWork']?.toDouble() ?? 0.0), + ); + } + + Map toJson() => { + 'id': id, + 'name': name, + 'shortName': shortName, + 'projectAddress': projectAddress, + 'contactPerson': contactPerson, + 'startDate': startDate.toIso8601String(), + 'endDate': endDate.toIso8601String(), + 'projectStatusId': projectStatusId, + 'teamSize': teamSize, + 'completedWork': completedWork, + 'plannedWork': plannedWork, + }; +} diff --git a/lib/routes.dart b/lib/routes.dart index 03e1a01..bfef6ea 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -24,7 +24,6 @@ import 'package:marco/view/tenant/tenant_selection_screen.dart'; import 'package:marco/view/finance/finance_screen.dart'; import 'package:marco/view/finance/advance_payment_screen.dart'; import 'package:marco/view/finance/payment_request_screen.dart'; -import 'package:marco/view/service_project/service_project_details_screen.dart'; import 'package:marco/view/service_project/service_project_screen.dart'; class AuthMiddleware extends GetMiddleware { @override @@ -136,11 +135,6 @@ getPageRoute() { ), // Service Projects - GetPage( - name: '/dashboard/service-project-details', - page: () => ServiceProjectDetailsScreen(), - middlewares: [AuthMiddleware()], - ), GetPage( name: '/dashboard/service-projects', page: () => ServiceProjectScreen(), diff --git a/lib/view/service_project/service_project_details_screen.dart b/lib/view/service_project/service_project_details_screen.dart new file mode 100644 index 0000000..cea61be --- /dev/null +++ b/lib/view/service_project/service_project_details_screen.dart @@ -0,0 +1,405 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:intl/intl.dart'; +import 'package:marco/controller/project_controller.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/widgets/my_text.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 { + late TabController _tabController; + final ServiceProjectDetailsController controller = + Get.put(ServiceProjectDetailsController()); + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 2, vsync: this); + controller.setProjectId(widget.projectId); + } + + @override + void dispose() { + _tabController.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), + decoration: BoxDecoration( + color: Colors.redAccent.withOpacity(0.1), + borderRadius: BorderRadius.circular(5), + ), + child: Icon(icon, size: 20, color: Colors.redAccent), + ), + MySpacing.width(16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + fontSize: 12, + color: Colors.grey[600], + fontWeight: FontWeight.w500, + ), + ), + MySpacing.height(4), + Text( + value, + style: TextStyle( + fontSize: 15, + color: isActionable && value != 'NA' + ? Colors.redAccent + : Colors.black87, + fontWeight: FontWeight.w500, + 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, color: Colors.redAccent), + MySpacing.width(8), + Text( + title, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + 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 const Center(child: Text("No project data")); + + return Padding( + padding: MySpacing.all(12), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // Header + 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: 45, color: Colors.redAccent), + 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), + ], + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFF5F5F5), + appBar: PreferredSize( + preferredSize: const Size.fromHeight(72), + child: AppBar( + backgroundColor: const Color(0xFFF5F5F5), + elevation: 0.5, + automaticallyImplyLeading: false, + titleSpacing: 0, + title: Padding( + padding: MySpacing.xy(16, 0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + IconButton( + icon: const Icon(Icons.arrow_back_ios_new, + color: Colors.black, size: 20), + onPressed: () => Get.toNamed('/dashboard/service-projects'), + ), + MySpacing.width(8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + MyText.titleLarge( + 'Service Projects', + fontWeight: 700, + color: Colors.black, + ), + MySpacing.height(2), + GetBuilder( + builder: (projectController) { + final projectName = + projectController.selectedProject?.name ?? + 'Select Project'; + return Row( + children: [ + const Icon(Icons.work_outline, + size: 14, color: Colors.grey), + MySpacing.width(4), + Expanded( + child: MyText.bodySmall( + projectName, + fontWeight: 600, + overflow: TextOverflow.ellipsis, + color: Colors.grey[700], + ), + ), + ], + ); + }, + ), + ], + ), + ), + ], + ), + ), + ), + ), + body: 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: const [ + Tab(text: "Profile"), + Tab(text: "Jobs"), + ], + ), + ), + + // ---------------- TabBarView ---------------- + Expanded( + child: Obx(() { + if (controller.isLoading.value) { + return const Center(child: CircularProgressIndicator()); + } + if (controller.errorMessage.value.isNotEmpty) { + return Center(child: Text(controller.errorMessage.value)); + } + + return TabBarView( + controller: _tabController, + children: [ + // Profile Tab + _buildProfileTab(), + + // Jobs Tab - empty + Container(color: Colors.white), + ], + ); + }), + ), + ], + ), + ); + } +} diff --git a/lib/view/service_project/service_project_screen.dart b/lib/view/service_project/service_project_screen.dart new file mode 100644 index 0000000..c4f6b7f --- /dev/null +++ b/lib/view/service_project/service_project_screen.dart @@ -0,0 +1,364 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; +import 'package:marco/helpers/widgets/my_spacing.dart'; +import 'package:marco/helpers/widgets/my_text.dart'; +import 'package:marco/helpers/widgets/my_refresh_indicator.dart'; +import 'package:marco/controller/service_project/service_project_screen_controller.dart'; +import 'package:marco/model/service_project/service_projects_list_model.dart'; +import 'package:marco/helpers/utils/date_time_utils.dart '; +import 'package:marco/view/service_project/service_project_details_screen.dart'; + +class ServiceProjectScreen extends StatefulWidget { + const ServiceProjectScreen({super.key}); + + @override + State createState() => _ServiceProjectScreenState(); +} + +class _ServiceProjectScreenState extends State + with UIMixin { + final TextEditingController searchController = TextEditingController(); + final ServiceProjectController controller = + Get.put(ServiceProjectController()); + + @override + void initState() { + super.initState(); + controller.fetchProjects(); + searchController.addListener(() { + controller.updateSearch(searchController.text); + }); + } + + Future _refreshProjects() async { + await controller.fetchProjects(); + } + + Widget _buildProjectCard(ProjectItem project) { + return Card( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)), + margin: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), + shadowColor: Colors.indigo.withOpacity(0.10), + color: Colors.white, + child: InkWell( + borderRadius: BorderRadius.circular(14), + onTap: () { + // Navigate to ServiceProjectDetailsScreen + Get.to( + () => ServiceProjectDetailsScreen(projectId: project.id), + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + /// Header Row: Avatar | Name & Tags | Status + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.titleMedium( + project.name, + fontWeight: 800, + ), + MySpacing.height(2), + Row( + children: [ + if (project.shortName.isNotEmpty) + _buildTag(project.shortName), + if (project.shortName.isNotEmpty) + MySpacing.width(6), + Icon(Icons.location_on, + size: 15, color: Colors.deepOrange.shade400), + MySpacing.width(2), + Flexible( + child: MyText.bodySmall( + project.projectAddress, + color: Colors.grey[700], + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ], + ), + ), + ], + ), + + MySpacing.height(12), + _buildDetailRow( + Icons.date_range_outlined, + Colors.teal, + "${DateTimeUtils.convertUtcToLocal(project.startDate.toIso8601String(), format: DateTimeUtils.defaultFormat)} To " + "${DateTimeUtils.convertUtcToLocal(project.endDate.toIso8601String(), format: DateTimeUtils.defaultFormat)}", + fontSize: 13, + ), + + MySpacing.height(12), + + /// Stats + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _buildStatColumn(Icons.people_alt_rounded, "Team", + "${project.teamSize}", Colors.blue[700]), + _buildStatColumn( + Icons.check_circle, + "Completed", + "${project.completedWork.toStringAsFixed(1)}%", + Colors.green[600]), + _buildStatColumn( + Icons.pending, + "Planned", + "${project.plannedWork.toStringAsFixed(1)}%", + Colors.orange[800]), + ], + ), + ], + ), + ), + ), + ); + } + +// Helper to build colored tags + Widget _buildTag(String label) { + return Container( + decoration: BoxDecoration( + color: Colors.indigo.withOpacity(0.08), + borderRadius: BorderRadius.circular(6), + ), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + child: + MyText.labelSmall(label, color: Colors.indigo[700], fontWeight: 500), + ); + } + +// Helper for detail row with icon and text + Widget _buildDetailRow(IconData icon, Color iconColor, String value, + {double fontSize = 12}) { + return Row( + children: [ + Icon(icon, size: 19, color: iconColor), + MySpacing.width(8), + Flexible( + child: MyText.bodySmall( + value, + color: Colors.grey[900], + fontWeight: 500, + fontSize: fontSize, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ); + } + +// Helper for stats column (icon + label + value) + Widget _buildStatColumn( + IconData icon, String label, String value, Color? color) { + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon(icon, color: color, size: 19), + SizedBox(height: 3), + MyText.labelSmall(value, color: color, fontWeight: 700), + MyText.bodySmall(label, color: Colors.grey[500], fontSize: 11), + ], + ); + } + + Widget _buildEmptyState() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.work_outline, size: 60, color: Colors.grey), + MySpacing.height(18), + MyText.titleMedium('No matching projects found.', + fontWeight: 600, color: Colors.grey), + MySpacing.height(10), + MyText.bodySmall('Try adjusting your filters or refresh.', + color: Colors.grey), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFF5F5F5), + + /// APPBAR + appBar: PreferredSize( + preferredSize: const Size.fromHeight(72), + child: AppBar( + backgroundColor: const Color(0xFFF5F5F5), + elevation: 0.5, + automaticallyImplyLeading: false, + titleSpacing: 0, + title: Padding( + padding: MySpacing.xy(16, 0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + IconButton( + icon: const Icon(Icons.arrow_back_ios_new, + color: Colors.black, size: 20), + onPressed: () => Get.back(), + ), + MySpacing.width(8), + MyText.titleLarge( + 'Service Projects', + fontWeight: 700, + color: Colors.black, + ), + ], + ), + ), + ), + ), + + body: Column( + children: [ + /// SEARCH + FILTER BAR + Padding( + padding: MySpacing.xy(8, 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.updateSearch(''); + }, + ); + }, + ), + hintText: 'Search projects...', + 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), + ), + ), + ), + ), + ), + MySpacing.width(8), + Container( + height: 35, + width: 35, + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(5), + ), + child: IconButton( + icon: + const Icon(Icons.tune, size: 20, color: Colors.black87), + onPressed: () { + // TODO: Open filter bottom sheet + }, + ), + ), + MySpacing.width(10), + Container( + height: 35, + width: 35, + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(5), + ), + child: PopupMenuButton( + padding: EdgeInsets.zero, + icon: const Icon(Icons.more_vert, + size: 20, color: Colors.black87), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5)), + itemBuilder: (context) => [ + const PopupMenuItem( + enabled: false, + height: 30, + child: Text("Actions", + style: TextStyle( + fontWeight: FontWeight.bold, + color: Colors.grey)), + ), + const PopupMenuItem( + value: 1, + child: Row( + children: [ + SizedBox(width: 10), + Expanded(child: Text("Manage Projects")), + Icon(Icons.chevron_right, + size: 20, color: Colors.indigo), + ], + ), + ), + ], + ), + ), + ], + ), + ), + + /// PROJECT LIST + Expanded( + child: Obx(() { + if (controller.isLoading.value) { + return const Center(child: CircularProgressIndicator()); + } + + final projects = controller.filteredProjects; + return MyRefreshIndicator( + onRefresh: _refreshProjects, + backgroundColor: Colors.indigo, + color: Colors.white, + child: projects.isEmpty + ? _buildEmptyState() + : ListView.separated( + physics: const AlwaysScrollableScrollPhysics(), + padding: MySpacing.only( + left: 8, right: 8, top: 4, bottom: 80), + itemCount: projects.length, + separatorBuilder: (_, __) => MySpacing.height(12), + itemBuilder: (_, index) => + _buildProjectCard(projects[index]), + ), + ); + }), + ), + ], + ), + ); + } +} diff --git a/lib/view/taskPlanning/daily_task_planning.dart b/lib/view/taskPlanning/daily_task_planning.dart index db3c7d2..65bf438 100644 --- a/lib/view/taskPlanning/daily_task_planning.dart +++ b/lib/view/taskPlanning/daily_task_planning.dart @@ -227,8 +227,8 @@ class _DailyTaskPlanningScreenState extends State final buildings = dailyTasks .expand((task) => task.buildings) .where((building) => - (building.plannedWork ?? 0) > 0 || - (building.completedWork ?? 0) > 0) + (building.plannedWork ) > 0 || + (building.completedWork ) > 0) .toList(); if (buildings.isEmpty) {