From dc4ea7979c46aa5a911a4fb463bae163c8b6ab37 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Tue, 18 Nov 2025 10:57:46 +0530 Subject: [PATCH] resoled payment request model crash issue --- .../payment_request_details_model.dart | 528 +++++++++--------- .../payment_request_detail_screen.dart | 361 ++++++------ 2 files changed, 473 insertions(+), 416 deletions(-) diff --git a/lib/model/finance/payment_request_details_model.dart b/lib/model/finance/payment_request_details_model.dart index aa4779f..64ffe94 100644 --- a/lib/model/finance/payment_request_details_model.dart +++ b/lib/model/finance/payment_request_details_model.dart @@ -1,30 +1,32 @@ class PaymentRequestDetail { - bool success; - String message; - PaymentRequestData? data; - dynamic errors; - int statusCode; - DateTime timestamp; + final bool? success; + final String? message; + final PaymentRequestData? data; + final dynamic errors; + final int? statusCode; + final DateTime? timestamp; PaymentRequestDetail({ - required this.success, - required this.message, + this.success, + this.message, this.data, this.errors, - required this.statusCode, - required this.timestamp, + this.statusCode, + this.timestamp, }); factory PaymentRequestDetail.fromJson(Map json) => PaymentRequestDetail( - success: json['success'], - message: json['message'], + success: json['success'] as bool?, + message: json['message'] as String?, data: json['data'] != null ? PaymentRequestData.fromJson(json['data']) : null, errors: json['errors'], - statusCode: json['statusCode'], - timestamp: DateTime.parse(json['timestamp']), + statusCode: json['statusCode'] as int?, + timestamp: json['timestamp'] != null + ? DateTime.tryParse(json['timestamp']) + : null, ); Map toJson() => { @@ -33,117 +35,129 @@ class PaymentRequestDetail { 'data': data?.toJson(), 'errors': errors, 'statusCode': statusCode, - 'timestamp': timestamp.toIso8601String(), + 'timestamp': timestamp?.toIso8601String(), }; } class PaymentRequestData { - String id; - String title; - String description; - String paymentRequestUID; - String payee; - Currency currency; - double amount; - double? baseAmount; - double? taxAmount; - DateTime dueDate; - Project project; - dynamic recurringPayment; - ExpenseCategory expenseCategory; - ExpenseStatus expenseStatus; - String? paidTransactionId; - DateTime? paidAt; - User? paidBy; - bool isAdvancePayment; - DateTime createdAt; - User createdBy; - DateTime updatedAt; - User? updatedBy; - List nextStatus; - List updateLogs; - List attachments; - bool isActive; - bool isExpenseCreated; + final String? id; + final String? title; + final String? description; + final String? paymentRequestUID; + final String? payee; + final Currency? currency; + final double? amount; + final double? baseAmount; + final double? taxAmount; + final DateTime? dueDate; + final Project? project; + final RecurringPayment? recurringPayment; + final ExpenseCategory? expenseCategory; + final ExpenseStatus? expenseStatus; + final String? paidTransactionId; + final DateTime? paidAt; + final User? paidBy; + final bool? isAdvancePayment; + final DateTime? createdAt; + final User? createdBy; + final DateTime? updatedAt; + final User? updatedBy; + final List? nextStatus; + final List? updateLogs; + final List? attachments; + final bool? isActive; + final bool? isExpenseCreated; PaymentRequestData({ - required this.id, - required this.title, - required this.description, - required this.paymentRequestUID, - required this.payee, - required this.currency, - required this.amount, + this.id, + this.title, + this.description, + this.paymentRequestUID, + this.payee, + this.currency, + this.amount, this.baseAmount, this.taxAmount, - required this.dueDate, - required this.project, + this.dueDate, + this.project, this.recurringPayment, - required this.expenseCategory, - required this.expenseStatus, + this.expenseCategory, + this.expenseStatus, this.paidTransactionId, this.paidAt, this.paidBy, - required this.isAdvancePayment, - required this.createdAt, - required this.createdBy, - required this.updatedAt, + this.isAdvancePayment, + this.createdAt, + this.createdBy, + this.updatedAt, this.updatedBy, - required this.nextStatus, - required this.updateLogs, - required this.attachments, - required this.isActive, - required this.isExpenseCreated, + this.nextStatus, + this.updateLogs, + this.attachments, + this.isActive, + this.isExpenseCreated, }); factory PaymentRequestData.fromJson(Map json) => PaymentRequestData( - id: json['id'], - title: json['title'], - description: json['description'], - paymentRequestUID: json['paymentRequestUID'], - payee: json['payee'], - currency: Currency.fromJson(json['currency']), - amount: (json['amount'] as num).toDouble(), - baseAmount: json['baseAmount'] != null - ? (json['baseAmount'] as num).toDouble() + id: json['id'] as String?, + title: json['title'] as String?, + description: json['description'] as String?, + paymentRequestUID: json['paymentRequestUID'] as String?, + payee: json['payee'] as String?, + currency: json['currency'] != null + ? Currency.fromJson(json['currency']) : null, - taxAmount: json['taxAmount'] != null - ? (json['taxAmount'] as num).toDouble() + amount: (json['amount'] as num?)?.toDouble(), + baseAmount: (json['baseAmount'] as num?)?.toDouble(), + taxAmount: (json['taxAmount'] as num?)?.toDouble(), + dueDate: json['dueDate'] != null + ? DateTime.tryParse(json['dueDate']) + : null, + project: json['project'] != null + ? Project.fromJson(json['project']) : null, - dueDate: DateTime.parse(json['dueDate']), - project: Project.fromJson(json['project']), recurringPayment: json['recurringPayment'] != null ? RecurringPayment.fromJson(json['recurringPayment']) : null, - expenseCategory: ExpenseCategory.fromJson(json['expenseCategory']), - expenseStatus: ExpenseStatus.fromJson(json['expenseStatus']), - paidTransactionId: json['paidTransactionId'], - paidAt: json['paidAt'] != null ? DateTime.parse(json['paidAt']) : null, - paidBy: json['paidBy'] != null ? User.fromJson(json['paidBy']) : null, - isAdvancePayment: json['isAdvancePayment'] ?? false, - createdAt: DateTime.parse(json['createdAt']), - createdBy: User.fromJson(json['createdBy']), - updatedAt: DateTime.parse(json['updatedAt']), + expenseCategory: json['expenseCategory'] != null + ? ExpenseCategory.fromJson(json['expenseCategory']) + : null, + expenseStatus: json['expenseStatus'] != null + ? ExpenseStatus.fromJson(json['expenseStatus']) + : null, + paidTransactionId: json['paidTransactionId'] as String?, + paidAt: json['paidAt'] != null + ? DateTime.tryParse(json['paidAt']) + : null, + paidBy: + json['paidBy'] != null ? User.fromJson(json['paidBy']) : null, + isAdvancePayment: json['isAdvancePayment'] as bool?, + createdAt: json['createdAt'] != null + ? DateTime.tryParse(json['createdAt']) + : null, + createdBy: json['createdBy'] != null + ? User.fromJson(json['createdBy']) + : null, + updatedAt: json['updatedAt'] != null + ? DateTime.tryParse(json['updatedAt']) + : null, updatedBy: json['updatedBy'] != null ? User.fromJson(json['updatedBy']) : null, - nextStatus: (json['nextStatus'] != null - ? (json['nextStatus'] as List) - .map((e) => NextStatus.fromJson(e)) - .toList() - : []), - updateLogs: (json['updateLogs'] != null - ? (json['updateLogs'] as List) - .map((e) => UpdateLog.fromJson(e)) - .toList() - : []), - attachments: (json['attachments'] != null - ? (json['attachments'] as List) - .map((e) => Attachment.fromJson(e)) - .toList() - : []), - isActive: json['isActive'] ?? true, - isExpenseCreated: json['isExpenseCreated'] ?? false, + nextStatus: (json['nextStatus'] as List?) + ?.map((e) => NextStatus.fromJson(e)) + .toList() ?? + [], + updateLogs: (json['updateLogs'] as List?) + ?.map((e) => UpdateLog.fromJson(e)) + .toList() ?? + [], + attachments: (json['attachments'] as List?) + ?.map((e) => Attachment.fromJson(e)) + .toList() ?? + [], + isActive: json['isActive'] as bool?, + isExpenseCreated: json['isExpenseCreated'] as bool?, ); Map toJson() => { @@ -152,50 +166,50 @@ class PaymentRequestData { 'description': description, 'paymentRequestUID': paymentRequestUID, 'payee': payee, - 'currency': currency.toJson(), + 'currency': currency?.toJson(), 'amount': amount, 'baseAmount': baseAmount, 'taxAmount': taxAmount, - 'dueDate': dueDate.toIso8601String(), - 'project': project.toJson(), - 'recurringPayment': recurringPayment, - 'expenseCategory': expenseCategory.toJson(), - 'expenseStatus': expenseStatus.toJson(), + 'dueDate': dueDate?.toIso8601String(), + 'project': project?.toJson(), + 'recurringPayment': recurringPayment?.toJson(), + 'expenseCategory': expenseCategory?.toJson(), + 'expenseStatus': expenseStatus?.toJson(), 'paidTransactionId': paidTransactionId, 'paidAt': paidAt?.toIso8601String(), 'paidBy': paidBy?.toJson(), 'isAdvancePayment': isAdvancePayment, - 'createdAt': createdAt.toIso8601String(), - 'createdBy': createdBy.toJson(), - 'updatedAt': updatedAt.toIso8601String(), + 'createdAt': createdAt?.toIso8601String(), + 'createdBy': createdBy?.toJson(), + 'updatedAt': updatedAt?.toIso8601String(), 'updatedBy': updatedBy?.toJson(), - 'nextStatus': nextStatus.map((e) => e.toJson()).toList(), - 'updateLogs': updateLogs.map((e) => e.toJson()).toList(), - 'attachments': attachments.map((e) => e.toJson()).toList(), + 'nextStatus': nextStatus?.map((e) => e.toJson()).toList(), + 'updateLogs': updateLogs?.map((e) => e.toJson()).toList(), + 'attachments': attachments?.map((e) => e.toJson()).toList(), 'isActive': isActive, 'isExpenseCreated': isExpenseCreated, }; } class RecurringPayment { - String id; - String recurringPaymentUID; - double amount; - bool isVariable; + final String? id; + final String? recurringPaymentUID; + final double? amount; + final bool? isVariable; RecurringPayment({ - required this.id, - required this.recurringPaymentUID, - required this.amount, - required this.isVariable, + this.id, + this.recurringPaymentUID, + this.amount, + this.isVariable, }); factory RecurringPayment.fromJson(Map json) => RecurringPayment( - id: json['id'], - recurringPaymentUID: json['recurringPaymentUID'], - amount: (json['amount'] as num).toDouble(), - isVariable: json['isVariable'], + id: json['id'] as String?, + recurringPaymentUID: json['recurringPaymentUID'] as String?, + amount: (json['amount'] as num?)?.toDouble(), + isVariable: json['isVariable'] as bool?, ); Map toJson() => { @@ -207,26 +221,26 @@ class RecurringPayment { } class Currency { - String id; - String currencyCode; - String currencyName; - String symbol; - bool isActive; + final String? id; + final String? currencyCode; + final String? currencyName; + final String? symbol; + final bool? isActive; Currency({ - required this.id, - required this.currencyCode, - required this.currencyName, - required this.symbol, - required this.isActive, + this.id, + this.currencyCode, + this.currencyName, + this.symbol, + this.isActive, }); factory Currency.fromJson(Map json) => Currency( - id: json['id'], - currencyCode: json['currencyCode'], - currencyName: json['currencyName'], - symbol: json['symbol'], - isActive: json['isActive'], + id: json['id'] as String?, + currencyCode: json['currencyCode'] as String?, + currencyName: json['currencyName'] as String?, + symbol: json['symbol'] as String?, + isActive: json['isActive'] as bool?, ); Map toJson() => { @@ -239,39 +253,39 @@ class Currency { } class Project { - String id; - String name; + final String? id; + final String? name; - Project({required this.id, required this.name}); + Project({this.id, this.name}); factory Project.fromJson(Map json) => - Project(id: json['id'], name: json['name']); + Project(id: json['id'] as String?, name: json['name'] as String?); Map toJson() => {'id': id, 'name': name}; } class ExpenseCategory { - String id; - String name; - bool noOfPersonsRequired; - bool isAttachmentRequried; - String description; + final String? id; + final String? name; + final bool? noOfPersonsRequired; + final bool? isAttachmentRequried; + final String? description; ExpenseCategory({ - required this.id, - required this.name, - required this.noOfPersonsRequired, - required this.isAttachmentRequried, - required this.description, + this.id, + this.name, + this.noOfPersonsRequired, + this.isAttachmentRequried, + this.description, }); factory ExpenseCategory.fromJson(Map json) => ExpenseCategory( - id: json['id'], - name: json['name'], - noOfPersonsRequired: json['noOfPersonsRequired'], - isAttachmentRequried: json['isAttachmentRequried'], - description: json['description'], + id: json['id'] as String?, + name: json['name'] as String?, + noOfPersonsRequired: json['noOfPersonsRequired'] as bool?, + isAttachmentRequried: json['isAttachmentRequried'] as bool?, + description: json['description'] as String?, ); Map toJson() => { @@ -284,34 +298,34 @@ class ExpenseCategory { } class ExpenseStatus { - String id; - String name; - String displayName; - String description; - List? permissionIds; - String color; - bool isSystem; + final String? id; + final String? name; + final String? displayName; + final String? description; + final List? permissionIds; + final String? color; + final bool? isSystem; ExpenseStatus({ - required this.id, - required this.name, - required this.displayName, - required this.description, + this.id, + this.name, + this.displayName, + this.description, this.permissionIds, - required this.color, - required this.isSystem, + this.color, + this.isSystem, }); factory ExpenseStatus.fromJson(Map json) => ExpenseStatus( - id: json['id'], - name: json['name'], - displayName: json['displayName'], - description: json['description'], + id: json['id'] as String?, + name: json['name'] as String?, + displayName: json['displayName'] as String?, + description: json['description'] as String?, permissionIds: json['permissionIds'] != null ? List.from(json['permissionIds']) : null, - color: json['color'], - isSystem: json['isSystem'], + color: json['color'] as String?, + isSystem: json['isSystem'] as bool?, ); Map toJson() => { @@ -326,32 +340,32 @@ class ExpenseStatus { } class User { - String id; - String firstName; - String lastName; - String email; - String photo; - String jobRoleId; - String jobRoleName; + 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, + this.id, + this.firstName, + this.lastName, + 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'], + 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?, ); Map toJson() => { @@ -366,34 +380,34 @@ class User { } class NextStatus { - String id; - String name; - String displayName; - String description; - List? permissionIds; - String color; - bool isSystem; + final String? id; + final String? name; + final String? displayName; + final String? description; + final List? permissionIds; + final String? color; + final bool? isSystem; NextStatus({ - required this.id, - required this.name, - required this.displayName, - required this.description, + this.id, + this.name, + this.displayName, + this.description, this.permissionIds, - required this.color, - required this.isSystem, + this.color, + this.isSystem, }); factory NextStatus.fromJson(Map json) => NextStatus( - id: json['id'], - name: json['name'], - displayName: json['displayName'], - description: json['description'], + id: json['id'] as String?, + name: json['name'] as String?, + displayName: json['displayName'] as String?, + description: json['description'] as String?, permissionIds: json['permissionIds'] != null ? List.from(json['permissionIds']) : null, - color: json['color'], - isSystem: json['isSystem'], + color: json['color'] as String?, + isSystem: json['isSystem'] as bool?, ); Map toJson() => { @@ -408,67 +422,73 @@ class NextStatus { } class UpdateLog { - String id; - ExpenseStatus? status; - ExpenseStatus nextStatus; - String comment; - DateTime updatedAt; - User updatedBy; + final String? id; + final ExpenseStatus? status; + final ExpenseStatus? nextStatus; + final String? comment; + final DateTime? updatedAt; + final User? updatedBy; UpdateLog({ - required this.id, + this.id, this.status, - required this.nextStatus, - required this.comment, - required this.updatedAt, - required this.updatedBy, + this.nextStatus, + this.comment, + this.updatedAt, + this.updatedBy, }); factory UpdateLog.fromJson(Map json) => UpdateLog( - id: json['id'], + id: json['id'] as String?, status: json['status'] != null ? ExpenseStatus.fromJson(json['status']) : null, - nextStatus: ExpenseStatus.fromJson(json['nextStatus']), - comment: json['comment'], - updatedAt: DateTime.parse(json['updatedAt']), - updatedBy: User.fromJson(json['updatedBy']), + nextStatus: json['nextStatus'] != null + ? ExpenseStatus.fromJson(json['nextStatus']) + : null, + comment: json['comment'] as String?, + updatedAt: json['updatedAt'] != null + ? DateTime.tryParse(json['updatedAt']) + : null, + updatedBy: json['updatedBy'] != null + ? User.fromJson(json['updatedBy']) + : null, ); Map toJson() => { 'id': id, 'status': status?.toJson(), - 'nextStatus': nextStatus.toJson(), + 'nextStatus': nextStatus?.toJson(), 'comment': comment, - 'updatedAt': updatedAt.toIso8601String(), - 'updatedBy': updatedBy.toJson(), + 'updatedAt': updatedAt?.toIso8601String(), + 'updatedBy': updatedBy?.toJson(), }; } class Attachment { - String id; - String fileName; - String url; - String? thumbUrl; - int fileSize; - String contentType; + final String? id; + final String? fileName; + final String? url; + final String? thumbUrl; + final int? fileSize; + final String? contentType; Attachment({ - required this.id, - required this.fileName, - required this.url, + this.id, + this.fileName, + this.url, this.thumbUrl, - required this.fileSize, - required this.contentType, + this.fileSize, + this.contentType, }); factory Attachment.fromJson(Map json) => Attachment( - id: json['id'], - fileName: json['fileName'], - url: json['url'], - thumbUrl: json['thumbUrl'], - fileSize: json['fileSize'], - contentType: json['contentType'], + id: json['id'] as String?, + fileName: json['fileName'] as String?, + url: json['url'] as String?, + thumbUrl: json['thumbUrl'] as String?, + fileSize: json['fileSize'] as int?, + contentType: json['contentType'] as String?, ); Map toJson() => { diff --git a/lib/view/finance/payment_request_detail_screen.dart b/lib/view/finance/payment_request_detail_screen.dart index 91a99c2..3d1e647 100644 --- a/lib/view/finance/payment_request_detail_screen.dart +++ b/lib/view/finance/payment_request_detail_screen.dart @@ -50,9 +50,9 @@ class _PaymentRequestDetailScreenState extends State void _checkPermissionToSubmit(PaymentRequestData request) { const draftStatusId = '6537018f-f4e9-4cb3-a210-6c3b2da999d7'; - final isCreatedByCurrentUser = employeeInfo?.id == request.createdBy.id; + final isCreatedByCurrentUser = employeeInfo?.id == request.createdBy?.id; final hasDraftNextStatus = - request.nextStatus.any((s) => s.id == draftStatusId); + (request.nextStatus ?? []).any((s) => (s.id ?? '') == draftStatusId); canSubmit.value = isCreatedByCurrentUser && hasDraftNextStatus; } @@ -61,36 +61,41 @@ class _PaymentRequestDetailScreenState extends State setState(() {}); } - Color _parseColor(String hexColor) { - String hex = hexColor.toUpperCase().replaceAll('#', ''); - if (hex.length == 6) hex = 'FF$hex'; - return Color(int.parse(hex, radix: 16)); + Color _parseColor(String? hexColor) { + try { + if (hexColor == null || hexColor.trim().isEmpty) return Colors.grey; + String hex = hexColor.toUpperCase().replaceAll('#', ''); + if (hex.length == 6) hex = 'FF$hex'; + return Color(int.parse(hex, radix: 16)); + } catch (_) { + return Colors.grey; + } } - void _openEditPaymentRequestBottomSheet(request) { + void _openEditPaymentRequestBottomSheet(PaymentRequestData request) { showPaymentRequestBottomSheet( isEdit: true, existingData: { - "id": request.id, - "paymentRequestId": request.paymentRequestUID, - "title": request.title, - "projectId": request.project.id, - "projectName": request.project.name, - "expenseCategoryId": request.expenseCategory.id, - "expenseCategoryName": request.expenseCategory.name, - "amount": request.amount.toString(), - "currencyId": request.currency.id, - "currencySymbol": request.currency.symbol, - "payee": request.payee, - "description": request.description, - "isAdvancePayment": request.isAdvancePayment, - "dueDate": request.dueDate, - "attachments": request.attachments + "id": request.id ?? '', + "paymentRequestId": request.paymentRequestUID ?? '', + "title": request.title ?? '', + "projectId": request.project?.id ?? '', + "projectName": request.project?.name ?? '', + "expenseCategoryId": request.expenseCategory?.id ?? '', + "expenseCategoryName": request.expenseCategory?.name ?? '', + "amount": (request.amount ?? 0).toString(), + "currencyId": request.currency?.id ?? '', + "currencySymbol": request.currency?.symbol ?? '', + "payee": request.payee ?? '', + "description": request.description ?? '', + "isAdvancePayment": request.isAdvancePayment ?? false, + "dueDate": request.dueDate?.toIso8601String() ?? '', + "attachments": (request.attachments ?? []) .map((a) => { - "url": a.url, - "fileName": a.fileName, - "documentId": a.id, - "contentType": a.contentType, + "url": a.url ?? '', + "fileName": a.fileName ?? '', + "documentId": a.id ?? '', + "contentType": a.contentType ?? '', }) .toList(), }, @@ -105,70 +110,72 @@ class _PaymentRequestDetailScreenState extends State return Scaffold( backgroundColor: Colors.white, appBar: _buildAppBar(), - body: SafeArea(child: Obx(() { - if (controller.isLoading.value && - controller.paymentRequest.value == null) { - return SkeletonLoaders.paymentRequestDetailSkeletonLoader(); - } + body: SafeArea( + child: Obx(() { + if (controller.isLoading.value && + controller.paymentRequest.value == null) { + return SkeletonLoaders.paymentRequestDetailSkeletonLoader(); + } - final request = controller.paymentRequest.value; + final request = controller.paymentRequest.value; - if (controller.errorMessage.isNotEmpty) { - return Center( - child: MyText.bodyMedium(controller.errorMessage.value)); - } + if ((controller.errorMessage.value ).isNotEmpty) { + return Center(child: MyText.bodyMedium(controller.errorMessage.value)); + } - if (request == null) { - return Center(child: MyText.bodyMedium("No data to display.")); - } + if (request == null) { + return Center(child: MyText.bodyMedium("No data to display.")); + } - return MyRefreshIndicator( - onRefresh: controller.fetchPaymentRequestDetail, - child: SingleChildScrollView( - padding: EdgeInsets.fromLTRB( - 12, - 12, - 12, - 60 + MediaQuery.of(context).padding.bottom, - ), - child: Center( - child: Container( - constraints: const BoxConstraints(maxWidth: 520), - child: Card( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(5)), - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 12, horizontal: 14), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _Header( - request: request, - colorParser: _parseColor, - employeeInfo: employeeInfo, - onEdit: () => - _openEditPaymentRequestBottomSheet(request), - ), - const Divider(height: 30, thickness: 1.2), - _Logs( - logs: request.updateLogs, colorParser: _parseColor), - const Divider(height: 30, thickness: 1.2), - _Parties(request: request), - const Divider(height: 30, thickness: 1.2), - _DetailsTable(request: request), - const Divider(height: 30, thickness: 1.2), - _Documents(documents: request.attachments), - MySpacing.height(24), - ], + return MyRefreshIndicator( + onRefresh: controller.fetchPaymentRequestDetail, + child: SingleChildScrollView( + padding: EdgeInsets.fromLTRB( + 12, + 12, + 12, + 60 + MediaQuery.of(context).padding.bottom, + ), + child: Center( + child: Container( + constraints: const BoxConstraints(maxWidth: 520), + child: Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5)), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 12, horizontal: 14), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _Header( + request: request, + colorParser: _parseColor, + employeeInfo: employeeInfo, + onEdit: () => _openEditPaymentRequestBottomSheet(request), + ), + const Divider(height: 30, thickness: 1.2), + _Logs( + logs: request.updateLogs ?? [], + colorParser: _parseColor, + ), + const Divider(height: 30, thickness: 1.2), + _Parties(request: request), + const Divider(height: 30, thickness: 1.2), + _DetailsTable(request: request), + const Divider(height: 30, thickness: 1.2), + _Documents(documents: request.attachments ?? []), + MySpacing.height(24), + ], + ), ), ), ), ), ), - ), - ); - })), + ); + }), + ), bottomNavigationBar: _buildBottomActionBar(), ); } @@ -192,14 +199,18 @@ class _PaymentRequestDetailScreenState extends State const reimbursementStatusId = '61578360-3a49-4c34-8604-7b35a3787b95'; const draftStatusId = '6537018f-f4e9-4cb3-a210-6c3b2da999d7'; - final availableStatuses = request.nextStatus.where((status) { - if (status.id == draftStatusId) { - return employeeInfo?.id == request.createdBy.id; + final availableStatuses = (request.nextStatus ?? []).where((status) { + if ((status.id ?? '') == draftStatusId) { + return employeeInfo?.id == request.createdBy?.id; } return permissionController .hasAnyPermission(status.permissionIds ?? []); }).toList(); + if (availableStatuses.isEmpty) { + return const SizedBox.shrink(); + } + return SafeArea( child: Container( decoration: BoxDecoration( @@ -224,7 +235,10 @@ class _PaymentRequestDetailScreenState extends State ), ), onPressed: () async { - if (status.id == reimbursementStatusId) { + final statusId = status.id ?? ''; + final dispName = status.displayName ?? ''; + + if (statusId == reimbursementStatusId) { showModalBottomSheet( context: context, isScrollControlled: true, @@ -233,23 +247,23 @@ class _PaymentRequestDetailScreenState extends State BorderRadius.vertical(top: Radius.circular(5)), ), builder: (ctx) => UpdatePaymentRequestWithReimbursement( - expenseId: request.paymentRequestUID, - statusId: status.id, + expenseId: request.paymentRequestUID ?? '', + statusId: statusId, onClose: () {}, ), ); - } else if (status.id == + } else if (statusId == 'b8586f67-dc19-49c3-b4af-224149efe1d3') { showCreateExpenseBottomSheet( - statusId: status.id, + statusId: statusId, ); } else { - final comment = await showCommentBottomSheet( - context, status.displayName); + final comment = + await showCommentBottomSheet(context, dispName); if (comment == null || comment.trim().isEmpty) return; final success = await controller.updatePaymentRequestStatus( - statusId: status.id, + statusId: statusId, comment: comment.trim(), ); @@ -258,14 +272,15 @@ class _PaymentRequestDetailScreenState extends State message: success ? 'Status updated successfully' : 'Failed to update status', - type: success ? SnackbarType.success : SnackbarType.error, + type: + success ? SnackbarType.success : SnackbarType.error, ); if (success) await controller.fetchPaymentRequestDetail(); } }, child: MyText.bodySmall( - status.displayName, + status.displayName ?? '', color: Colors.white, fontWeight: 600, ), @@ -308,8 +323,8 @@ class _PaymentRequestDetailScreenState extends State ), MySpacing.height(2), GetBuilder(builder: (_) { - final name = projectController.selectedProject?.name ?? - 'Select Project'; + final name = + projectController.selectedProject?.name ?? 'Select Project'; return Row( children: [ const Icon(Icons.work_outline, @@ -340,14 +355,14 @@ class _PaymentRequestDetailScreenState extends State class PaymentRequestPermissionHelper { static bool canEditPaymentRequest( EmployeeInfo? employee, PaymentRequestData request) { - return employee?.id == request.createdBy.id && - _isInAllowedEditStatus(request.expenseStatus.id); + return employee?.id == request.createdBy?.id && + _isInAllowedEditStatus(request.expenseStatus?.id ?? ''); } static bool canSubmitPaymentRequest( EmployeeInfo? employee, PaymentRequestData request) { - return employee?.id == request.createdBy.id && - request.nextStatus.isNotEmpty; + return employee?.id == request.createdBy?.id && + (request.nextStatus ?? []).isNotEmpty; } static bool _isInAllowedEditStatus(String statusId) { @@ -362,9 +377,10 @@ class PaymentRequestPermissionHelper { // ------------------ Sub-widgets ------------------ +// Header widget class _Header extends StatefulWidget { final PaymentRequestData request; - final Color Function(String) colorParser; + final Color Function(String?) colorParser; final VoidCallback? onEdit; final EmployeeInfo? employeeInfo; @@ -382,7 +398,7 @@ class _Header extends StatefulWidget { class _HeaderState extends State<_Header> with UIMixin { @override Widget build(BuildContext context) { - final statusColor = widget.colorParser(widget.request.expenseStatus.color); + final statusColor = widget.colorParser(widget.request.expenseStatus?.color); final canEdit = widget.employeeInfo != null && PaymentRequestPermissionHelper.canEditPaymentRequest( @@ -395,7 +411,7 @@ class _HeaderState extends State<_Header> with UIMixin { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ MyText.bodyMedium( - 'ID: ${widget.request.paymentRequestUID}', + 'ID: ${widget.request.paymentRequestUID ?? '-'}', fontWeight: 700, fontSize: 14, ), @@ -422,7 +438,7 @@ class _HeaderState extends State<_Header> with UIMixin { Expanded( child: MyText.bodySmall( DateTimeUtils.convertUtcToLocal( - widget.request.createdAt.toIso8601String(), + widget.request.createdAt?.toIso8601String() ?? '', format: 'dd MMM yyyy'), fontWeight: 600, overflow: TextOverflow.ellipsis, @@ -443,7 +459,7 @@ class _HeaderState extends State<_Header> with UIMixin { SizedBox( width: 100, child: MyText.labelSmall( - widget.request.expenseStatus.displayName, + widget.request.expenseStatus?.displayName ?? '-', color: statusColor, fontWeight: 600, overflow: TextOverflow.ellipsis, @@ -459,11 +475,10 @@ class _HeaderState extends State<_Header> with UIMixin { } } -// ------------------ Logs, Parties, Details, Documents ------------------ - +// Logs widget class _Logs extends StatelessWidget { final List logs; - final Color Function(String) colorParser; + final Color Function(String?) colorParser; const _Logs({required this.logs, required this.colorParser}); @override @@ -490,21 +505,25 @@ class _Logs extends StatelessWidget { ? colorParser(log.status!.color) : Colors.grey; - final comment = log.comment; - final nextStatusName = log.nextStatus.name; + final comment = log.comment ?? ''; + final nextStatusName = log.nextStatus?.name ?? ''; final updatedBy = log.updatedBy; + final first = updatedBy?.firstName ?? ''; + final last = updatedBy?.lastName ?? ''; final initials = - '${updatedBy.firstName.isNotEmpty == true ? updatedBy.firstName[0] : ''}' - '${updatedBy.lastName.isNotEmpty == true ? updatedBy.lastName[0] : ''}'; - final name = '${updatedBy.firstName} ${updatedBy.lastName}'; + '${first.isNotEmpty ? first[0] : ''}${last.isNotEmpty ? last[0] : ''}'; + final name = ((first + ' ' + last).trim().isNotEmpty) + ? '$first $last' + : '-'; - final timestamp = log.updatedAt - .toUtc() - .add(const Duration(hours: 5, minutes: 30)); - final timeAgo = timeago.format(timestamp); + final updatedAt = log.updatedAt; + final timeAgo = (updatedAt != null) + ? timeago.format(updatedAt.toUtc().add(const Duration(hours: 5, minutes: 30))) + : '-'; - final nextStatusColor = colorParser(log.nextStatus.color); + final nextStatusColor = + colorParser(log.nextStatus?.color ?? ''); return TimelineTile( alignment: TimelineAlign.start, @@ -583,6 +602,7 @@ class _Logs extends StatelessWidget { } } +// Parties widget class _Parties extends StatelessWidget { final PaymentRequestData request; const _Parties({required this.request}); @@ -602,7 +622,7 @@ class _Parties extends StatelessWidget { children: [ MyText.labelMedium("Payee", fontWeight: 600), MySpacing.height(2), - MyText.bodyMedium(request.payee), + MyText.bodyMedium(request.payee ?? '-'), ], ), ), @@ -612,7 +632,7 @@ class _Parties extends StatelessWidget { children: [ MyText.labelMedium("Project", fontWeight: 600), MySpacing.height(2), - MyText.bodyMedium(request.project.name), + MyText.bodyMedium(request.project?.name ?? '-'), ], ), ), @@ -623,98 +643,105 @@ class _Parties extends StatelessWidget { } } +// Details table widget class _DetailsTable extends StatelessWidget { final PaymentRequestData request; const _DetailsTable({required this.request}); + String _formatCurrencyAmount(String? symbol, double? amount) { + final sym = symbol ?? ''; + final amt = amount ?? 0.0; + return '$sym ${amt.toStringAsFixed(2)}'; + } + @override Widget build(BuildContext context) { + final currencySymbol = request.currency?.symbol ?? ''; + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Basic Info - _labelValueRow("Payment Request ID:", request.paymentRequestUID), - if (request.paidTransactionId != null && - request.paidTransactionId!.isNotEmpty) - _labelValueRow("Transaction ID:", request.paidTransactionId!), - _labelValueRow("Payee:", request.payee), - _labelValueRow("Project:", request.project.name), - _labelValueRow("Expense Category:", request.expenseCategory.name), + _labelValueRow("Payment Request ID:", request.paymentRequestUID ?? '-'), + if ((request.paidTransactionId ?? '').isNotEmpty) + _labelValueRow("Transaction ID:", request.paidTransactionId ?? ''), + _labelValueRow("Payee:", request.payee ?? '-'), + _labelValueRow("Project:", request.project?.name ?? '-'), + _labelValueRow("Expense Category:", request.expenseCategory?.name ?? '-'), // Amounts - _labelValueRow("Amount:", - "${request.currency.symbol} ${request.amount.toStringAsFixed(2)}"), + _labelValueRow("Amount:", _formatCurrencyAmount(currencySymbol, request.amount)), if (request.baseAmount != null) - _labelValueRow("Base Amount:", - "${request.currency.symbol} ${request.baseAmount!.toStringAsFixed(2)}"), + _labelValueRow("Base Amount:", _formatCurrencyAmount(currencySymbol, request.baseAmount)), if (request.taxAmount != null) - _labelValueRow("Tax Amount:", - "${request.currency.symbol} ${request.taxAmount!.toStringAsFixed(2)}"), - if (request.expenseCategory.noOfPersonsRequired) + _labelValueRow("Tax Amount:", _formatCurrencyAmount(currencySymbol, request.taxAmount)), + if (request.expenseCategory?.noOfPersonsRequired == true) _labelValueRow("Additional Persons Required:", "Yes"), - if (request.expenseCategory.isAttachmentRequried) + if (request.expenseCategory?.isAttachmentRequried == true) _labelValueRow("Attachment Required:", "Yes"), // Dates _labelValueRow( "Due Date:", - DateTimeUtils.convertUtcToLocal(request.dueDate.toIso8601String(), + DateTimeUtils.convertUtcToLocal( + request.dueDate?.toIso8601String() ?? '', format: 'dd MMM yyyy')), _labelValueRow( "Created At:", - DateTimeUtils.convertUtcToLocal(request.createdAt.toIso8601String(), + DateTimeUtils.convertUtcToLocal( + request.createdAt?.toIso8601String() ?? '', format: 'dd MMM yyyy')), _labelValueRow( "Updated At:", - DateTimeUtils.convertUtcToLocal(request.updatedAt.toIso8601String(), + DateTimeUtils.convertUtcToLocal( + request.updatedAt?.toIso8601String() ?? '', format: 'dd MMM yyyy')), // Payment Info if (request.paidAt != null) _labelValueRow( - "Transaction Date:", - DateTimeUtils.convertUtcToLocal(request.paidAt!.toIso8601String(), - format: 'dd MMM yyyy')), + "Transaction Date:", + DateTimeUtils.convertUtcToLocal( + request.paidAt?.toIso8601String() ?? '', + format: 'dd MMM yyyy'), + ), if (request.paidBy != null) - _labelValueRow("Paid By:", - "${request.paidBy!.firstName} ${request.paidBy!.lastName}"), + _labelValueRow("Paid By:", "${request.paidBy?.firstName ?? ''} ${request.paidBy?.lastName ?? ''}".trim()), // Flags _labelValueRow( - "Advance Payment:", request.isAdvancePayment ? "Yes" : "No"), + "Advance Payment:", (request.isAdvancePayment ?? false) ? "Yes" : "No"), _labelValueRow( - "Expense Created:", request.isExpenseCreated ? "Yes" : "No"), - _labelValueRow("Active:", request.isActive ? "Yes" : "No"), + "Expense Created:", (request.isExpenseCreated ?? false) ? "Yes" : "No"), + _labelValueRow("Active:", (request.isActive ?? false) ? "Yes" : "No"), // Recurring Payment Info if (request.recurringPayment != null) ...[ const SizedBox(height: 6), MyText.bodySmall("Recurring Payment Info:", fontWeight: 600), - _labelValueRow( - "Recurring ID:", request.recurringPayment!.recurringPaymentUID), - _labelValueRow("Amount:", - "${request.currency.symbol} ${request.recurringPayment!.amount.toStringAsFixed(2)}"), - _labelValueRow("Variable Amount:", - request.recurringPayment!.isVariable ? "Yes" : "No"), + _labelValueRow("Recurring ID:", request.recurringPayment?.recurringPaymentUID ?? '-'), + _labelValueRow("Amount:", _formatCurrencyAmount(currencySymbol, request.recurringPayment?.amount)), + _labelValueRow("Variable Amount:", (request.recurringPayment?.isVariable ?? false) ? "Yes" : "No"), ], // Description & Attachments - _labelValueRow("Description:", request.description), - _labelValueRow( - "Attachment:", request.attachments.isNotEmpty ? "Yes" : "No"), + _labelValueRow("Description:", request.description ?? '-'), + _labelValueRow("Attachment:", (request.attachments ?? []).isNotEmpty ? "Yes" : "No"), ], ); } } +// Documents widget class _Documents extends StatelessWidget { final List documents; const _Documents({required this.documents}); @override Widget build(BuildContext context) { - if (documents.isEmpty) + if (documents.isEmpty) { return MyText.bodyMedium('No Documents', color: Colors.grey); + } return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -728,26 +755,36 @@ class _Documents extends StatelessWidget { separatorBuilder: (_, __) => const SizedBox(height: 8), itemBuilder: (context, index) { final doc = documents[index]; - final isImage = doc.contentType.startsWith('image/'); + final contentType = doc.contentType ?? ''; + final isImage = contentType.startsWith('image/'); return GestureDetector( onTap: () async { final imageDocs = documents - .where((d) => d.contentType.startsWith('image/')) + .where((d) => (d.contentType ?? '').startsWith('image/')) .toList(); final initialIndex = - imageDocs.indexWhere((d) => d.id == doc.id); + imageDocs.indexWhere((d) => (d.id ?? '') == (doc.id ?? '')); if (isImage && imageDocs.isNotEmpty && initialIndex != -1) { showDialog( context: context, builder: (_) => ImageViewerDialog( - imageSources: imageDocs.map((e) => e.url).toList(), + imageSources: imageDocs.map((e) => e.url ?? '').toList(), initialIndex: initialIndex, ), ); } else { - final Uri url = Uri.parse(doc.url); + final urlStr = doc.url ?? ''; + if (urlStr.isEmpty) { + showAppSnackbar( + title: 'Error', + message: 'Document URL is missing.', + type: SnackbarType.error, + ); + return; + } + final Uri url = Uri.parse(urlStr); if (await canLaunchUrl(url)) { await launchUrl(url, mode: LaunchMode.externalApplication); } else { @@ -774,7 +811,7 @@ class _Documents extends StatelessWidget { const SizedBox(width: 7), Expanded( child: MyText.bodySmall( - doc.fileName, + doc.fileName ?? (doc.url ?? '-'), overflow: TextOverflow.ellipsis, ), ),