From cb009119834364b677ef36132cb0521007b19c69 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 | 299 +++++++++++++- .../service_project_screen.dart | 386 ++++++++---------- .../taskPlanning/daily_task_planning.dart | 4 +- 10 files changed, 991 insertions(+), 245 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 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 index 491ea8f..cea61be 100644 --- a/lib/view/service_project/service_project_details_screen.dart +++ b/lib/view/service_project/service_project_details_screen.dart @@ -1,11 +1,16 @@ 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 { - const ServiceProjectDetailsScreen({super.key}); + final String projectId; + + const ServiceProjectDetailsScreen({super.key, required this.projectId}); @override State createState() => @@ -13,13 +18,17 @@ class ServiceProjectDetailsScreen extends StatefulWidget { } class _ServiceProjectDetailsScreenState - extends State with SingleTickerProviderStateMixin { + 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 @@ -28,6 +37,263 @@ class _ServiceProjectDetailsScreenState 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( @@ -47,7 +313,7 @@ class _ServiceProjectDetailsScreenState IconButton( icon: const Icon(Icons.arrow_back_ios_new, color: Colors.black, size: 20), - onPressed: () => Get.offNamed('/dashboard'), + onPressed: () => Get.toNamed('/dashboard/service-projects'), ), MySpacing.width(8), Expanded( @@ -101,6 +367,8 @@ class _ServiceProjectDetailsScreenState labelColor: Colors.black, unselectedLabelColor: Colors.grey, indicatorColor: Colors.red, + indicatorWeight: 3, + isScrollable: false, tabs: const [ Tab(text: "Profile"), Tab(text: "Jobs"), @@ -110,12 +378,25 @@ class _ServiceProjectDetailsScreenState // ---------------- TabBarView ---------------- Expanded( - child: TabBarView( - controller: _tabController, - children: const [ - // Add your tab content here later - ], - ), + 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 index aa56f80..c4f6b7f 100644 --- a/lib/view/service_project/service_project_screen.dart +++ b/lib/view/service_project/service_project_screen.dart @@ -1,12 +1,13 @@ import 'package:flutter/material.dart'; -import 'package:font_awesome_flutter/font_awesome_flutter.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/helpers/widgets/avatar.dart'; -import 'package:marco/controller/project_controller.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}); @@ -18,190 +19,104 @@ class ServiceProjectScreen extends StatefulWidget { class _ServiceProjectScreenState extends State with UIMixin { final TextEditingController searchController = TextEditingController(); - final RxList> allProjects = >[].obs; - final RxList> filteredProjects = - >[].obs; + final ServiceProjectController controller = + Get.put(ServiceProjectController()); @override void initState() { super.initState(); - _loadProjects(); - } - - void _loadProjects() { - final staticProjects = [ - { - "name": "Website Redesign", - "description": "Revamping the corporate website UI/UX", - "status": "In Progress", - "manager": "John Doe", - "email": "john@company.com", - "phone": "+91 9876543210", - "tags": ["UI", "Frontend", "High Priority"] - }, - { - "name": "Mobile App Development", - "description": "Cross-platform mobile app for customers", - "status": "Completed", - "manager": "Priya Sharma", - "email": "priya@company.com", - "phone": "+91 9812345678", - "tags": ["Flutter", "Backend"] - }, - { - "name": "Data Migration", - "description": "Migrating legacy data to AWS", - "status": "Pending", - "manager": "Arun Mehta", - "email": "arun@company.com", - "phone": "+91 9999988888", - "tags": ["Database", "Cloud"] - }, - ]; - allProjects.assignAll(staticProjects); - filteredProjects.assignAll(staticProjects); - } - - void _filterProjects(String query) { - if (query.isEmpty) { - filteredProjects.assignAll(allProjects); - } else { - filteredProjects.assignAll(allProjects - .where((p) => - p["name"].toLowerCase().contains(query.toLowerCase()) || - p["manager"].toLowerCase().contains(query.toLowerCase())) - .toList()); - } + controller.fetchProjects(); + searchController.addListener(() { + controller.updateSearch(searchController.text); + }); } Future _refreshProjects() async { - await Future.delayed(const Duration(seconds: 1)); + await controller.fetchProjects(); } - Widget _buildProjectCard(Map project) { + Widget _buildProjectCard(ProjectItem project) { return Card( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)), - elevation: 3, - shadowColor: Colors.grey.withOpacity(0.3), + margin: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), + shadowColor: Colors.indigo.withOpacity(0.10), color: Colors.white, child: InkWell( - borderRadius: BorderRadius.circular(5), + borderRadius: BorderRadius.circular(14), onTap: () { - // TODO: Navigate to Project Details screen + // Navigate to ServiceProjectDetailsScreen + Get.to( + () => ServiceProjectDetailsScreen(projectId: project.id), + ); }, child: Padding( - padding: const EdgeInsets.all(12), - child: Row( + padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14), + child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Avatar( - firstName: project["name"].split(" ").first, - lastName: project["name"].split(" ").length > 1 - ? project["name"].split(" ").last - : "", - size: 40, - ), - MySpacing.width(12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MyText.titleSmall(project["name"], - fontWeight: 600, overflow: TextOverflow.ellipsis), - MyText.bodySmall(project["description"], - color: Colors.grey[700], - overflow: TextOverflow.ellipsis), - MySpacing.height(6), - Row( - children: [ - Icon(Icons.person_outline, - size: 16, color: Colors.indigo), - MySpacing.width(4), - MyText.labelSmall(project["manager"], - color: Colors.indigo), - ], - ), - MySpacing.height(4), - Row( - children: [ - Icon(Icons.email_outlined, - size: 16, color: Colors.indigo), - MySpacing.width(4), - Expanded( - child: MyText.labelSmall( - project["email"], - color: Colors.indigo, - decoration: TextDecoration.underline, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - MySpacing.height(4), - Row( - children: [ - Icon(Icons.phone_outlined, - size: 16, color: Colors.indigo), - MySpacing.width(4), - Expanded( - child: MyText.labelSmall( - project["phone"], - color: Colors.indigo, - decoration: TextDecoration.underline, - overflow: TextOverflow.ellipsis, - ), - ), - MySpacing.width(8), - const FaIcon(FontAwesomeIcons.whatsapp, - color: Colors.green, size: 20), - ], - ), - MySpacing.height(6), - Wrap( - spacing: 6, - runSpacing: 2, - children: (project["tags"] as List) - .map((tag) => Chip( - label: Text(tag), - backgroundColor: Colors.indigo.shade50, - labelStyle: const TextStyle( - color: Colors.indigo, fontSize: 12), - visualDensity: VisualDensity.compact, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(5), - ), - )) - .toList(), - ), - ], - ), - ), - Column( + /// Header Row: Avatar | Name & Tags | Status + Row( children: [ - Container( - padding: - const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: project["status"] == "Completed" - ? Colors.green.shade100 - : project["status"] == "In Progress" - ? Colors.orange.shade100 - : Colors.red.shade100, - borderRadius: BorderRadius.circular(12), - ), - child: MyText.labelSmall( - project["status"], - fontWeight: 600, - color: project["status"] == "Completed" - ? Colors.green - : project["status"] == "In Progress" - ? Colors.orange - : Colors.red, + 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, + ), + ), + ], + ), + ], ), ), - const SizedBox(height: 10), - const Icon(Icons.arrow_forward_ios, - color: Colors.grey, size: 20), + ], + ), + + 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]), ], ), ], @@ -211,6 +126,53 @@ class _ServiceProjectScreenState extends State ); } +// 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( @@ -233,7 +195,7 @@ class _ServiceProjectScreenState extends State return Scaffold( backgroundColor: const Color(0xFFF5F5F5), - /// --- SAME APPBAR AS DETAILS SCREEN --- + /// APPBAR appBar: PreferredSize( preferredSize: const Size.fromHeight(72), child: AppBar( @@ -249,44 +211,13 @@ class _ServiceProjectScreenState extends State IconButton( icon: const Icon(Icons.arrow_back_ios_new, color: Colors.black, size: 20), - onPressed: () => Get.offNamed('/dashboard'), + onPressed: () => Get.back(), ), 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 ?? - 'All Projects'; - 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], - ), - ), - ], - ); - }, - ), - ], - ), + MyText.titleLarge( + 'Service Projects', + fontWeight: 700, + color: Colors.black, ), ], ), @@ -296,7 +227,7 @@ class _ServiceProjectScreenState extends State body: Column( children: [ - /// --- SEARCH + FILTER BAR --- + /// SEARCH + FILTER BAR Padding( padding: MySpacing.xy(8, 8), child: Row( @@ -306,7 +237,6 @@ class _ServiceProjectScreenState extends State height: 35, child: TextField( controller: searchController, - onChanged: _filterProjects, decoration: InputDecoration( contentPadding: const EdgeInsets.symmetric(horizontal: 12), @@ -315,15 +245,14 @@ class _ServiceProjectScreenState extends State suffixIcon: ValueListenableBuilder( valueListenable: searchController, builder: (context, value, _) { - if (value.text.isEmpty) { + if (value.text.isEmpty) return const SizedBox.shrink(); - } return IconButton( icon: const Icon(Icons.clear, size: 20, color: Colors.grey), onPressed: () { searchController.clear(); - _filterProjects(''); + controller.updateSearch(''); }, ); }, @@ -402,24 +331,31 @@ class _ServiceProjectScreenState extends State ), ), - /// --- PROJECT LIST --- + /// PROJECT LIST Expanded( - child: Obx(() => MyRefreshIndicator( - onRefresh: _refreshProjects, - backgroundColor: Colors.indigo, - color: Colors.white, - child: filteredProjects.isEmpty - ? _buildEmptyState() - : ListView.separated( - physics: const AlwaysScrollableScrollPhysics(), - padding: MySpacing.only( - left: 8, right: 8, top: 4, bottom: 80), - itemCount: filteredProjects.length, - separatorBuilder: (_, __) => MySpacing.height(12), - itemBuilder: (_, index) => - _buildProjectCard(filteredProjects[index]), - ), - )), + 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) {