From 9666d39d5efe749aa1ceaeda23efc02bc88f2c4f Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Sat, 15 Nov 2025 15:35:56 +0530 Subject: [PATCH 1/4] fixed the model crashing --- .../dashboard/dashboard_tasks_model.dart | 14 ++++++++---- .../dashboard/project_progress_model.dart | 22 +++++++++---------- .../service_projects_list_model.dart | 15 +++++++------ 3 files changed, 29 insertions(+), 22 deletions(-) diff --git a/lib/model/dashboard/dashboard_tasks_model.dart b/lib/model/dashboard/dashboard_tasks_model.dart index ba10a7a..39ac835 100644 --- a/lib/model/dashboard/dashboard_tasks_model.dart +++ b/lib/model/dashboard/dashboard_tasks_model.dart @@ -1,4 +1,3 @@ -// dashboard_tasks_model.dart class DashboardTasks { final bool success; final String message; @@ -23,7 +22,7 @@ class DashboardTasks { data: json['data'] != null ? DashboardTasksData.fromJson(json['data']) : null, errors: json['errors'], statusCode: json['statusCode'] ?? 0, - timestamp: DateTime.parse(json['timestamp']), + timestamp: DateTime.tryParse(json['timestamp'] ?? '') ?? DateTime.now(), ); } } @@ -38,9 +37,16 @@ class DashboardTasksData { }); factory DashboardTasksData.fromJson(Map json) { + int toInt(dynamic value) { + if (value is int) return value; + if (value is double) return value.toInt(); + if (value is String) return int.tryParse(value) ?? 0; + return 0; + } + return DashboardTasksData( - totalTasks: json['totalTasks'] ?? 0, - completedTasks: json['completedTasks'] ?? 0, + totalTasks: toInt(json['totalTasks']), + completedTasks: toInt(json['completedTasks']), ); } } diff --git a/lib/model/dashboard/project_progress_model.dart b/lib/model/dashboard/project_progress_model.dart index 20f47c1..e29b71f 100644 --- a/lib/model/dashboard/project_progress_model.dart +++ b/lib/model/dashboard/project_progress_model.dart @@ -27,7 +27,7 @@ class ProjectResponse { [], errors: json['errors'], statusCode: json['statusCode'] ?? 0, - timestamp: DateTime.parse(json['timestamp']), + timestamp: DateTime.tryParse(json['timestamp'] ?? '') ?? DateTime.now(), ); } @@ -46,8 +46,8 @@ class ProjectResponse { class ProjectData { final String projectId; final String projectName; - final int plannedTask; - final int completedTask; + final double plannedTask; + final double completedTask; final DateTime date; ProjectData({ @@ -62,9 +62,9 @@ class ProjectData { return ProjectData( projectId: json['projectId'] ?? '', projectName: json['projectName'] ?? '', - plannedTask: json['plannedTask'] ?? 0, - completedTask: json['completedTask'] ?? 0, - date: DateTime.parse(json['date']), + plannedTask: (json['plannedTask'] ?? 0).toDouble(), + completedTask: (json['completedTask'] ?? 0).toDouble(), + date: DateTime.tryParse(json['date'] ?? '') ?? DateTime.now(), ); } @@ -81,8 +81,8 @@ class ProjectData { /// Chart-friendly model class ChartTaskData { - final DateTime date; // ✅ actual date for chart - final String dateLabel; // optional: for display + final DateTime date; + final String dateLabel; final int planned; final int completed; @@ -96,9 +96,9 @@ class ChartTaskData { factory ChartTaskData.fromProjectData(ProjectData data) { return ChartTaskData( date: data.date, - dateLabel: DateFormat('dd-MM').format(data.date), - planned: data.plannedTask, - completed: data.completedTask, + dateLabel: DateFormat('dd-MM').format(data.date), + planned: data.plannedTask.round(), + completed: data.completedTask.round(), ); } } diff --git a/lib/model/service_project/service_projects_list_model.dart b/lib/model/service_project/service_projects_list_model.dart index 7117113..bb1f672 100644 --- a/lib/model/service_project/service_projects_list_model.dart +++ b/lib/model/service_project/service_projects_list_model.dart @@ -108,18 +108,15 @@ class ProjectItem { address: json['address'] ?? '', assignedDate: DateTime.tryParse(json['assignedDate'] ?? '') ?? DateTime.now(), - status: - json['status'] != null ? Status.fromJson(json['status']) : null, - client: - json['client'] != null ? Client.fromJson(json['client']) : null, + 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))) : [], contactName: json['contactName'] ?? '', contactPhone: json['contactPhone'] ?? '', contactEmail: json['contactEmail'] ?? '', - createdAt: - DateTime.tryParse(json['createdAt'] ?? '') ?? DateTime.now(), + createdAt: DateTime.tryParse(json['createdAt'] ?? '') ?? DateTime.now(), createdBy: json['createdBy'] != null ? CreatedBy.fromJson(json['createdBy']) : null, @@ -192,7 +189,11 @@ class Client { contactPerson: json['contactPerson'] ?? '', address: json['address'] ?? '', contactNumber: json['contactNumber'] ?? '', - sprid: json['sprid'] ?? 0, + sprid: (json['sprid'] is int) + ? json['sprid'] + : (json['sprid'] is double) + ? (json['sprid'] as double).toInt() + : int.tryParse(json['sprid']?.toString() ?? "0") ?? 0, ); } From 31a27da85d92bd5cccb66775248d3315b3b18c2c Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Mon, 17 Nov 2025 11:08:50 +0530 Subject: [PATCH 2/4] added service project job details screen --- ...ice_project_details_screen_controller.dart | 38 +- lib/helpers/services/api_endpoints.dart | 4 +- lib/helpers/services/api_service.dart | 30 ++ lib/helpers/widgets/avatar.dart | 75 ++-- lib/helpers/widgets/custom_app_bar.dart | 119 +++--- .../service_project_job_detail_model.dart | 244 +++++++++++++ .../service_project_details_screen.dart | 268 ++++++-------- .../service_project_job_detail_screen.dart | 341 ++++++++++++++++++ .../service_project_screen.dart | 63 +--- 9 files changed, 869 insertions(+), 313 deletions(-) create mode 100644 lib/model/service_project/service_project_job_detail_model.dart create mode 100644 lib/view/service_project/service_project_job_detail_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 index 2761723..6f70896 100644 --- a/lib/controller/service_project/service_project_details_screen_controller.dart +++ b/lib/controller/service_project/service_project_details_screen_controller.dart @@ -2,6 +2,7 @@ import 'package:get/get.dart'; import 'package:marco/helpers/services/api_service.dart'; import 'package:marco/model/service_project/service_projects_details_model.dart'; import 'package:marco/model/service_project/job_list_model.dart'; +import 'package:marco/model/service_project/service_project_job_detail_model.dart'; class ServiceProjectDetailsController extends GetxController { // Selected project id @@ -13,13 +14,18 @@ class ServiceProjectDetailsController extends GetxController { // Job list var jobList = [].obs; + // Job detail for a selected job + var jobDetail = Rxn(); + // Loading states var isLoading = false.obs; var isJobLoading = false.obs; + var isJobDetailLoading = false.obs; // Error messages var errorMessage = ''.obs; var jobErrorMessage = ''.obs; + var jobDetailErrorMessage = ''.obs; // Pagination var pageNumber = 1; @@ -53,14 +59,12 @@ class ServiceProjectDetailsController extends GetxController { errorMessage.value = ''; try { - final result = - await ApiService.getServiceProjectDetailApi(projectId.value); + 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"; + errorMessage.value = result?.message ?? "Failed to fetch project details"; } } catch (e) { errorMessage.value = "Error: $e"; @@ -83,7 +87,7 @@ class ServiceProjectDetailsController extends GetxController { try { final result = await ApiService.getServiceProjectJobListApi( - projectId: "", + projectId: projectId.value, pageNumber: pageNumber, pageSize: pageSize, isActive: true, @@ -122,4 +126,28 @@ class ServiceProjectDetailsController extends GetxController { fetchProjectJobs(initialLoad: true), ]); } + + /// Fetch job details by job ID + Future fetchJobDetail(String jobId) async { + if (jobId.isEmpty) { + jobDetailErrorMessage.value = "Invalid job ID"; + return; + } + + isJobDetailLoading.value = true; + jobDetailErrorMessage.value = ''; + + try { + final result = await ApiService.getServiceProjectJobDetailApi(jobId); + if (result != null) { + jobDetail.value = result; + } else { + jobDetailErrorMessage.value = "Failed to fetch job details"; + } + } catch (e) { + jobDetailErrorMessage.value = "Error fetching job details: $e"; + } finally { + isJobDetailLoading.value = false; + } + } } diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index 35e7831..d4fae11 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -1,8 +1,8 @@ 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 baseUrl = "https://mapi.marcoaiot.com/api"; static const String getMasterCurrencies = "/Master/currencies/list"; diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index a5e2219..923baa0 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -36,6 +36,7 @@ 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'; import 'package:marco/model/service_project/job_list_model.dart'; +import 'package:marco/model/service_project/service_project_job_detail_model.dart'; class ApiService { static const bool enableLogs = true; @@ -307,6 +308,35 @@ class ApiService { // Service Project Module APIs + +/// Get details for a single Service Project Job +static Future getServiceProjectJobDetailApi(String jobId) async { + final endpoint = "${ApiEndpoints.getServiceProjectJobDetail}/$jobId"; + logSafe("Fetching Job Detail for Job ID: $jobId"); + + try { + final response = await _getRequest(endpoint); + if (response == null) { + logSafe("Service Project Job Detail request failed: null response", level: LogLevel.error); + return null; + } + + final jsonResponse = _parseResponseForAllData( + response, + label: "Service Project Job Detail", + ); + + if (jsonResponse != null) { + return JobDetailsResponse.fromJson(jsonResponse); + } + } catch (e, stack) { + logSafe("Exception during getServiceProjectJobDetailApi: $e", level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } + + return null; +} + /// Create a new Service Project Job static Future createServiceProjectJobApi({ required String title, diff --git a/lib/helpers/widgets/avatar.dart b/lib/helpers/widgets/avatar.dart index fdb32de..c206631 100644 --- a/lib/helpers/widgets/avatar.dart +++ b/lib/helpers/widgets/avatar.dart @@ -5,14 +5,16 @@ import 'package:marco/helpers/widgets/my_text.dart'; class Avatar extends StatelessWidget { final String firstName; final String lastName; + final String? imageUrl; final double size; - final Color? backgroundColor; + final Color? backgroundColor; final Color textColor; const Avatar({ super.key, required this.firstName, required this.lastName, + this.imageUrl, this.size = 46.0, this.backgroundColor, this.textColor = Colors.white, @@ -20,9 +22,24 @@ class Avatar extends StatelessWidget { @override Widget build(BuildContext context) { - String initials = "${firstName.isNotEmpty ? firstName[0] : ''}${lastName.isNotEmpty ? lastName[0] : ''}".toUpperCase(); + if (imageUrl != null && imageUrl!.isNotEmpty) { + return ClipRRect( + borderRadius: BorderRadius.circular(size / 2), + child: Image.network( + imageUrl!, + width: size, + height: size, + fit: BoxFit.cover, + ), + ); + } - final Color bgColor = backgroundColor ?? _getFlatColorFromName('$firstName$lastName'); + String initials = + "${firstName.isNotEmpty ? firstName[0] : ''}${lastName.isNotEmpty ? lastName[0] : ''}" + .toUpperCase(); + + final Color bgColor = + backgroundColor ?? _getFlatColorFromName('$firstName$lastName'); return MyContainer.rounded( height: size, @@ -32,36 +49,36 @@ class Avatar extends StatelessWidget { child: Center( child: MyText( initials, - fontSize: size * 0.45, // 👈 scales with avatar size + fontSize: size * 0.45, fontWeight: 600, color: textColor, ), ), ); } - - // Use fixed flat color palette and pick based on hash - Color _getFlatColorFromName(String name) { - final colors = [ - Color(0xFFE57373), // Red - Color(0xFFF06292), // Pink - Color(0xFFBA68C8), // Purple - Color(0xFF9575CD), // Deep Purple - Color(0xFF7986CB), // Indigo - Color(0xFF64B5F6), // Blue - Color(0xFF4FC3F7), // Light Blue - Color(0xFF4DD0E1), // Cyan - Color(0xFF4DB6AC), // Teal - Color(0xFF81C784), // Green - Color(0xFFAED581), // Light Green - Color(0xFFDCE775), // Lime - Color(0xFFFFD54F), // Amber - Color(0xFFFFB74D), // Orange - Color(0xFFA1887F), // Brown - Color(0xFF90A4AE), // Blue Grey - ]; - - int index = name.hashCode.abs() % colors.length; - return colors[index]; - } +} + +// Use fixed flat color palette and pick based on hash +Color _getFlatColorFromName(String name) { + final colors = [ + Color(0xFFE57373), // Red + Color(0xFFF06292), // Pink + Color(0xFFBA68C8), // Purple + Color(0xFF9575CD), // Deep Purple + Color(0xFF7986CB), // Indigo + Color(0xFF64B5F6), // Blue + Color(0xFF4FC3F7), // Light Blue + Color(0xFF4DD0E1), // Cyan + Color(0xFF4DB6AC), // Teal + Color(0xFF81C784), // Green + Color(0xFFAED581), // Light Green + Color(0xFFDCE775), // Lime + Color(0xFFFFD54F), // Amber + Color(0xFFFFB74D), // Orange + Color(0xFFA1887F), // Brown + Color(0xFF90A4AE), // Blue Grey + ]; + + int index = name.hashCode.abs() % colors.length; + return colors[index]; } diff --git a/lib/helpers/widgets/custom_app_bar.dart b/lib/helpers/widgets/custom_app_bar.dart index fdd6d5c..0b39ad0 100644 --- a/lib/helpers/widgets/custom_app_bar.dart +++ b/lib/helpers/widgets/custom_app_bar.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/controller/project_controller.dart'; +import 'package:marco/helpers/widgets/my_spacing.dart'; +import 'package:marco/helpers/widgets/my_text.dart'; class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { final String title; @@ -17,67 +18,67 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { Widget build(BuildContext context) { return PreferredSize( preferredSize: const Size.fromHeight(72), - child: Container( - decoration: BoxDecoration( - color: const Color(0xFFF5F5F5), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.15), - blurRadius: 0.5, - offset: const Offset(0, 0.5), - ) - ], - ), - child: SafeArea( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Row( - children: [ - IconButton( - icon: const Icon(Icons.arrow_back_ios_new, - color: Colors.black, size: 20), - onPressed: onBackPressed ?? Get.back, - splashRadius: 24, + 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, ), - const SizedBox(width: 8), - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MyText.titleLarge( - title, - fontWeight: 700, - color: Colors.black, - ), - const SizedBox(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), - const SizedBox(width: 4), - Expanded( - child: MyText.bodySmall( - projectName, - fontWeight: 600, - overflow: TextOverflow.ellipsis, - color: Colors.grey[700], - ), + onPressed: onBackPressed ?? () => Get.back(), + ), + MySpacing.width(5), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + // TITLE + MyText.titleLarge( + title, + fontWeight: 700, + color: Colors.black, + ), + + MySpacing.height(2), + + // PROJECT NAME ROW (copied exactly) + 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], ), - ], - ); - }, - ), - ], - ), + ), + ], + ); + }, + ), + ], ), - ], - ), + ), + ], ), ), ), diff --git a/lib/model/service_project/service_project_job_detail_model.dart b/lib/model/service_project/service_project_job_detail_model.dart new file mode 100644 index 0000000..a74f309 --- /dev/null +++ b/lib/model/service_project/service_project_job_detail_model.dart @@ -0,0 +1,244 @@ +class JobDetailsResponse { + final bool success; + final String message; + final JobData? data; + final dynamic errors; + final int statusCode; + final String timestamp; + + JobDetailsResponse({ + required this.success, + required this.message, + this.data, + this.errors, + required this.statusCode, + required this.timestamp, + }); + + factory JobDetailsResponse.fromJson(Map json) { + return JobDetailsResponse( + success: json['success'] as bool, + message: json['message'] as String, + data: json['data'] != null ? JobData.fromJson(json['data']) : null, + errors: json['errors'], + statusCode: json['statusCode'] as int, + timestamp: json['timestamp'] as String, + ); + } +} + +class JobData { + final String id; + final String title; + final String description; + final Project project; + final List assignees; + final Status status; + final String startDate; + final String dueDate; + final bool isActive; + final String createdAt; + final User createdBy; + final List tags; + final List updateLogs; + + JobData({ + required this.id, + required this.title, + required this.description, + required this.project, + required this.assignees, + required this.status, + required this.startDate, + required this.dueDate, + required this.isActive, + required this.createdAt, + required this.createdBy, + required this.tags, + required this.updateLogs, + }); + + factory JobData.fromJson(Map json) { + return JobData( + id: json['id'] as String, + title: json['title'] as String, + description: json['description'] as String, + project: Project.fromJson(json['project']), + assignees: (json['assignees'] as List) + .map((e) => Assignee.fromJson(e)) + .toList(), + status: Status.fromJson(json['status']), + startDate: json['startDate'] as String, + dueDate: json['dueDate'] as String, + isActive: json['isActive'] as bool, + createdAt: json['createdAt'] as String, + createdBy: User.fromJson(json['createdBy']), + tags: (json['tags'] as List).map((e) => Tag.fromJson(e)).toList(), + updateLogs: (json['updateLogs'] as List) + .map((e) => UpdateLog.fromJson(e)) + .toList(), + ); + } +} + +class Project { + final String id; + final String name; + final String shortName; + final String assignedDate; + final String contactName; + final String contactPhone; + final String contactEmail; + + Project({ + required this.id, + required this.name, + required this.shortName, + required this.assignedDate, + required this.contactName, + required this.contactPhone, + required this.contactEmail, + }); + + factory Project.fromJson(Map json) { + return Project( + id: json['id'] as String, + name: json['name'] as String, + shortName: json['shortName'] as String, + assignedDate: json['assignedDate'] as String, + contactName: json['contactName'] as String, + contactPhone: json['contactPhone'] as String, + contactEmail: json['contactEmail'] as String, + ); + } +} + +class Assignee { + final String id; + final String firstName; + final String lastName; + final String email; + final String photo; + final String jobRoleId; + final String jobRoleName; + + Assignee({ + required this.id, + required this.firstName, + required this.lastName, + required this.email, + required this.photo, + required this.jobRoleId, + required this.jobRoleName, + }); + + factory Assignee.fromJson(Map json) { + return Assignee( + id: json['id'] as String, + firstName: json['firstName'] as String, + lastName: json['lastName'] as String, + email: json['email'] as String, + photo: json['photo'] as String, + jobRoleId: json['jobRoleId'] as String, + jobRoleName: json['jobRoleName'] as String, + ); + } +} + +class Status { + final String id; + final String name; + final String displayName; + final int level; + + Status({ + required this.id, + required this.name, + required this.displayName, + required this.level, + }); + + factory Status.fromJson(Map json) { + return Status( + id: json['id'] as String, + name: json['name'] as String, + displayName: json['displayName'] as String, + level: json['level'] as int, + ); + } +} + +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, + required this.photo, + required this.jobRoleId, + required this.jobRoleName, + }); + + factory User.fromJson(Map json) { + return User( + id: json['id'] as String, + firstName: json['firstName'] as String, + lastName: json['lastName'] as String, + email: json['email'] as String, + photo: json['photo'] as String, + jobRoleId: json['jobRoleId'] as String, + jobRoleName: json['jobRoleName'] as String, + ); + } +} + +class Tag { + final String id; + final String name; + + Tag({ + required this.id, + required this.name, + }); + + factory Tag.fromJson(Map json) { + return Tag( + id: json['id'] as String, + name: json['name'] as String, + ); + } +} + +class UpdateLog { + final String id; + final Status? status; + final Status nextStatus; + final String comment; + final User updatedBy; + + UpdateLog({ + required this.id, + this.status, + required this.nextStatus, + required this.comment, + required this.updatedBy, + }); + + factory UpdateLog.fromJson(Map json) { + return UpdateLog( + id: json['id'] as String, + status: json['status'] != null ? Status.fromJson(json['status']) : null, + nextStatus: Status.fromJson(json['nextStatus']), + comment: json['comment'] as String, + updatedBy: User.fromJson(json['updatedBy']), + ); + } +} diff --git a/lib/view/service_project/service_project_details_screen.dart b/lib/view/service_project/service_project_details_screen.dart index ad39ef7..6e1dc36 100644 --- a/lib/view/service_project/service_project_details_screen.dart +++ b/lib/view/service_project/service_project_details_screen.dart @@ -1,7 +1,6 @@ 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'; @@ -9,6 +8,9 @@ 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; @@ -345,115 +347,118 @@ class _ServiceProjectDetailsScreenState } 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(), - ), + 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], ), - 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), + // 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(), + ), ), - const Spacer(), + MySpacing.height(8), - // 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), + // 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), ), - child: Text( - job.status.displayName, - style: TextStyle( - fontSize: 12, + + const Spacer(), + + // Status Chip + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 4), + decoration: BoxDecoration( color: job.status.name.toLowerCase() == 'completed' - ? Colors.green[800] - : Colors.orange[800], - fontWeight: FontWeight.w600, + ? 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, + ), ), ), - ), - ], - ), - ], + ], + ), + ], + ), ), ), ); @@ -466,64 +471,9 @@ class _ServiceProjectDetailsScreenState 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 Project Details', - 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], - ), - ), - ], - ); - }, - ), - ], - ), - ), - ], - ), - ), - ), + appBar: CustomAppBar( + title: "Service Project Details", + onBackPressed: () => Get.toNamed('/dashboard/service-projects'), ), body: SafeArea( child: Column( diff --git a/lib/view/service_project/service_project_job_detail_screen.dart b/lib/view/service_project/service_project_job_detail_screen.dart new file mode 100644 index 0000000..09c2990 --- /dev/null +++ b/lib/view/service_project/service_project_job_detail_screen.dart @@ -0,0 +1,341 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:marco/controller/service_project/service_project_details_screen_controller.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/utils/date_time_utils.dart'; +import 'package:marco/helpers/utils/launcher_utils.dart'; +import 'package:timeline_tile/timeline_tile.dart'; +import 'package:marco/model/service_project/service_project_job_detail_model.dart'; +import 'package:marco/helpers/widgets/custom_app_bar.dart'; +import 'package:marco/helpers/widgets/avatar.dart'; + +class JobDetailsScreen extends StatefulWidget { + final String jobId; + + const JobDetailsScreen({super.key, required this.jobId}); + + @override + State createState() => _JobDetailsScreenState(); +} + +class _JobDetailsScreenState extends State with UIMixin { + late final ServiceProjectDetailsController controller; + + @override + void initState() { + super.initState(); + controller = Get.put(ServiceProjectDetailsController()); + controller.fetchJobDetail(widget.jobId); + } + + 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, + fontWeight: 700, + fontSize: 16, + ) + ], + ), + MySpacing.height(8), + const Divider(), + ...children + ], + ), + ), + ); + } + + Widget _rowTile(String label, String value, {bool copyable = false}) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 3, + child: MyText.bodySmall(label, + color: Colors.grey[600], fontWeight: 600), + ), + Expanded( + flex: 5, + child: GestureDetector( + onLongPress: copyable + ? () => LauncherUtils.copyToClipboard(value, typeLabel: label) + : null, + child: MyText.bodyMedium(value, + fontWeight: 600, + color: copyable ? Colors.blue : Colors.black87), + ), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFF5F5F5), + appBar: CustomAppBar( + title: "Service Project Job Details", + onBackPressed: () => Get.back(), + ), + body: Obx(() { + if (controller.isJobDetailLoading.value) { + return const Center(child: CircularProgressIndicator()); + } + + if (controller.jobDetailErrorMessage.value.isNotEmpty) { + return Center( + child: MyText.bodyMedium(controller.jobDetailErrorMessage.value), + ); + } + + final job = controller.jobDetail.value?.data; + if (job == null) { + return Center(child: MyText.bodyMedium("No details available")); + } + + return SingleChildScrollView( + padding: MySpacing.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + 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.task_outlined, size: 35), + MySpacing.width(16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.titleMedium(job.title, fontWeight: 700), + MySpacing.height(5), + MyText.bodySmall(job.project.name, + color: Colors.grey[700]), + ], + ), + ) + ], + ), + ), + ), + + MySpacing.height(20), + + // ====== Job Information ======= + _buildSectionCard( + title: "Job Information", + titleIcon: Icons.info_outline, + children: [ + _rowTile("Description", job.description), + _rowTile( + "Start Date", + DateTimeUtils.convertUtcToLocal(job.startDate, + format: "dd MMM yyyy"), + ), + _rowTile( + "Due Date", + DateTimeUtils.convertUtcToLocal(job.dueDate, + format: "dd MMM yyyy"), + ), + _rowTile("Status", job.status.displayName), + ], + ), + + MySpacing.height(16), + + // ====== Assignees ======= + _buildSectionCard( + title: "Assigned To", + titleIcon: Icons.people_outline, + children: job.assignees.isNotEmpty + ? job.assignees.map((a) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: Row( + children: [ + Avatar( + firstName: a.firstName, + lastName: a.lastName, + size: + 32, + backgroundColor: + a.photo.isEmpty ? null : Colors.transparent, + textColor: Colors.white, + ), + MySpacing.width(10), + MyText.bodyMedium("${a.firstName} ${a.lastName}"), + ], + ), + ); + }).toList() + : [MyText.bodySmall("No assignees", color: Colors.grey)], + ), + + MySpacing.height(16), + + // ====== Tags ======= + if (job.tags.isNotEmpty) + _buildSectionCard( + title: "Tags", + titleIcon: Icons.label_outline, + children: [ + Wrap( + spacing: 6, + runSpacing: 6, + children: job.tags.map((tag) { + return Chip( + label: Text(tag.name), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5), + ), + ); + }).toList(), + ) + ], + ), + + MySpacing.height(16), + + // ====== Update Logs (Timeline UI) ======= + if (job.updateLogs.isNotEmpty) + _buildSectionCard( + title: "Update Logs", + titleIcon: Icons.history, + children: [ + JobTimeline(logs: job.updateLogs), + ], + ), + + MySpacing.height(40), + ], + ), + ); + }), + ); + } +} + +class JobTimeline extends StatelessWidget { + final List logs; + + const JobTimeline({super.key, required this.logs}); + + @override + Widget build(BuildContext context) { + if (logs.isEmpty) { + return MyText.bodyMedium('No timeline available', color: Colors.grey); + } + + // Show latest updates at the top + final reversedLogs = logs.reversed.toList(); + + return ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: reversedLogs.length, + itemBuilder: (_, index) { + final log = reversedLogs[index]; + + final statusName = log.status?.displayName ?? "Created"; + final nextStatusName = log.nextStatus.displayName; + final comment = log.comment; + + final updatedBy = + "${log.updatedBy.firstName} ${log.updatedBy.lastName}"; + + final initials = + "${log.updatedBy.firstName.isNotEmpty ? log.updatedBy.firstName[0] : ''}" + "${log.updatedBy.lastName.isNotEmpty ? log.updatedBy.lastName[0] : ''}"; + + return TimelineTile( + alignment: TimelineAlign.start, + isFirst: index == 0, + isLast: index == reversedLogs.length - 1, + indicatorStyle: IndicatorStyle( + width: 16, + height: 16, + indicator: Container( + decoration: const BoxDecoration( + color: Colors.blue, + shape: BoxShape.circle, + ), + ), + ), + beforeLineStyle: LineStyle( + color: Colors.grey.shade300, + thickness: 2, + ), + endChild: Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // STATUS CHANGE ROW + MyText.bodyMedium( + "$statusName → $nextStatusName", + fontWeight: 600, + ), + const SizedBox(height: 8), + + // COMMENT + if (comment.isNotEmpty) MyText.bodyMedium(comment), + + const SizedBox(height: 10), + + // Updated by + Row( + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: Colors.grey.shade300, + borderRadius: BorderRadius.circular(4), + ), + child: MyText.bodySmall(initials, fontWeight: 600), + ), + const SizedBox(width: 6), + Expanded( + child: MyText.bodySmall(updatedBy), + ), + ], + ), + + const SizedBox(height: 10), + ], + ), + ), + ); + }, + ); + } +} diff --git a/lib/view/service_project/service_project_screen.dart b/lib/view/service_project/service_project_screen.dart index 8831369..50bdee7 100644 --- a/lib/view/service_project/service_project_screen.dart +++ b/lib/view/service_project/service_project_screen.dart @@ -8,7 +8,7 @@ import 'package:marco/controller/service_project/service_project_screen_controll 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'; -import 'package:marco/controller/project_controller.dart'; +import 'package:marco/helpers/widgets/custom_app_bar.dart'; class ServiceProjectScreen extends StatefulWidget { const ServiceProjectScreen({super.key}); @@ -194,64 +194,9 @@ class _ServiceProjectScreenState extends State 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'), - ), - 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], - ), - ), - ], - ); - }, - ), - ], - ), - ), - ], - ), - ), - ), + appBar: CustomAppBar( + title: "Service Projects", + onBackPressed: () => Get.toNamed('/dashboard'), ), body: Column( children: [ From ee1e5014b407f85f1e584a0a446d59b910dc3ab5 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Mon, 17 Nov 2025 11:15:17 +0530 Subject: [PATCH 3/4] added edit job api --- lib/helpers/services/api_endpoints.dart | 7 +- lib/helpers/services/api_service.dart | 98 ++++++++++++++++++------- 2 files changed, 77 insertions(+), 28 deletions(-) diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index d4fae11..49ae63a 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -2,8 +2,7 @@ class ApiEndpoints { 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 baseUrl = "https://mapi.marcoaiot.com/api"; static const String getMasterCurrencies = "/Master/currencies/list"; static const String getMasterExpensesCategories = @@ -134,6 +133,8 @@ class ApiEndpoints { static const String getServiceProjectsList = "/serviceproject/list"; static const String getServiceProjectDetail = "/serviceproject/details"; static const String getServiceProjectJobList = "/serviceproject/job/list"; - static const String getServiceProjectJobDetail = "/serviceproject/job/details"; + static const String getServiceProjectJobDetail = + "/serviceproject/job/details"; + static const String editServiceProjectJob = "/serviceproject/job/edit"; static const String createServiceProjectJob = "/serviceproject/job/create"; } diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index 923baa0..55949f4 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -307,35 +307,83 @@ class ApiService { } // Service Project Module APIs + /// Edit a Service Project Job + static Future editServiceProjectJobApi({ + required String jobId, + required List> + operations, + }) async { + final endpoint = "${ApiEndpoints.editServiceProjectJob}/$jobId"; + logSafe("Editing Service Project Job: $jobId with operations: $operations"); -/// Get details for a single Service Project Job -static Future getServiceProjectJobDetailApi(String jobId) async { - final endpoint = "${ApiEndpoints.getServiceProjectJobDetail}/$jobId"; - logSafe("Fetching Job Detail for Job ID: $jobId"); - - try { - final response = await _getRequest(endpoint); - if (response == null) { - logSafe("Service Project Job Detail request failed: null response", level: LogLevel.error); - return null; + try { + // PATCH request is usually similar to PUT, but with http.patch + String? token = await _getToken(); + if (token == null) return false; + + final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint"); + + final headers = _headers(token); + + final response = await http + .patch(uri, headers: headers, body: jsonEncode(operations)) + .timeout(extendedTimeout); + + logSafe( + "Edit Service Project Job response status: ${response.statusCode}"); + logSafe("Edit Service Project Job response body: ${response.body}"); + + final json = jsonDecode(response.body); + + if (response.statusCode == 200 && json['success'] == true) { + logSafe("Service Project Job edited successfully: ${json['data']}"); + return true; + } else { + logSafe( + "Failed to edit Service Project Job: ${json['message'] ?? 'Unknown error'}", + level: LogLevel.warning, + ); + return false; + } + } catch (e, stack) { + logSafe("Exception during editServiceProjectJobApi: $e", + level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + return false; } - - final jsonResponse = _parseResponseForAllData( - response, - label: "Service Project Job Detail", - ); - - if (jsonResponse != null) { - return JobDetailsResponse.fromJson(jsonResponse); - } - } catch (e, stack) { - logSafe("Exception during getServiceProjectJobDetailApi: $e", level: LogLevel.error); - logSafe("StackTrace: $stack", level: LogLevel.debug); } - - return null; -} + + /// Get details for a single Service Project Job + static Future getServiceProjectJobDetailApi( + String jobId) async { + final endpoint = "${ApiEndpoints.getServiceProjectJobDetail}/$jobId"; + logSafe("Fetching Job Detail for Job ID: $jobId"); + + try { + final response = await _getRequest(endpoint); + if (response == null) { + logSafe("Service Project Job Detail request failed: null response", + level: LogLevel.error); + return null; + } + + final jsonResponse = _parseResponseForAllData( + response, + label: "Service Project Job Detail", + ); + + if (jsonResponse != null) { + return JobDetailsResponse.fromJson(jsonResponse); + } + } catch (e, stack) { + logSafe("Exception during getServiceProjectJobDetailApi: $e", + level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + } + + return null; + } /// Create a new Service Project Job static Future createServiceProjectJobApi({ From e1952d505bd6e91b1b5875f87e5f4a48c6986929 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Mon, 17 Nov 2025 11:22:31 +0530 Subject: [PATCH 4/4] modified api service --- lib/helpers/services/api_service.dart | 28 +-------------------------- 1 file changed, 1 insertion(+), 27 deletions(-) diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index e148861..4340118 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -310,8 +310,7 @@ class ApiService { /// Edit a Service Project Job static Future editServiceProjectJobApi({ required String jobId, - required List> - operations, + required List> operations, }) async { final endpoint = "${ApiEndpoints.editServiceProjectJob}/$jobId"; @@ -505,35 +504,20 @@ class ApiService { } /// 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"); 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); try { final response = await _getRequest(endpoint); - if (response == null) { - logSafe("Service Project Detail request failed: null response", - level: LogLevel.error); - return null; - } if (response == null) { logSafe("Service Project Detail request failed: null response", level: LogLevel.error); return null; } - final jsonResponse = _parseResponseForAllData( - response, - label: "Service Project Detail", - ); final jsonResponse = _parseResponseForAllData( response, label: "Service Project Detail", @@ -547,19 +531,9 @@ class ApiService { level: LogLevel.error); logSafe("StackTrace: $stack", level: LogLevel.debug); } - 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; } - return null; - } /// Get Service Project List static Future getServiceProjectsListApi({