From 5dd09869ad32d12401e3f5a87aec5b7e28179e76 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Wed, 19 Nov 2025 17:17:04 +0530 Subject: [PATCH] feat: enhance ExpenseDetailModel and ExpenseDetailScreen with additional fields and improved UI structure --- lib/model/expense/expense_detail_model.dart | 146 ++++++++++--- lib/view/expense/expense_detail_screen.dart | 229 +++++++++++++------- 2 files changed, 262 insertions(+), 113 deletions(-) diff --git a/lib/model/expense/expense_detail_model.dart b/lib/model/expense/expense_detail_model.dart index 116205e..5477e49 100644 --- a/lib/model/expense/expense_detail_model.dart +++ b/lib/model/expense/expense_detail_model.dart @@ -2,7 +2,7 @@ class ExpenseDetailModel { final String id; final Project project; - final ExpensesType expensesType; + final ExpensesType expensesType; final PaymentMode paymentMode; final Person paidBy; final Person createdBy; @@ -13,18 +13,25 @@ class ExpenseDetailModel { final String createdAt; final String supplerName; final double amount; + final double? baseAmount; + final double? taxAmount; + final double? tdsPercentage; final ExpenseStatus status; - final List nextStatus; + final List nextStatus; final bool preApproved; final String transactionId; final String description; final String location; + final Currency? currency; final List documents; final List expenseLogs; + final String? gstNumber; - final int noOfPersons; + final int? noOfPersons; final bool isActive; final dynamic expensesReimburse; + final String? expenseUId; + final String? paymentRequestUID; ExpenseDetailModel({ required this.id, @@ -40,51 +47,87 @@ class ExpenseDetailModel { required this.createdAt, required this.supplerName, required this.amount, + this.baseAmount, + this.taxAmount, + this.tdsPercentage, required this.status, required this.nextStatus, required this.preApproved, required this.transactionId, required this.description, required this.location, + this.currency, required this.documents, required this.expenseLogs, this.gstNumber, - required this.noOfPersons, + this.noOfPersons, required this.isActive, this.expensesReimburse, + this.expenseUId, + this.paymentRequestUID, }); factory ExpenseDetailModel.fromJson(Map json) { return ExpenseDetailModel( id: json['id'] ?? '', - project: json['project'] != null ? Project.fromJson(json['project']) : Project.empty(), - expensesType: json['expensesType'] != null - ? ExpensesType.fromJson(json['expensesType']) + project: json['project'] != null + ? Project.fromJson(json['project']) + : Project.empty(), + expensesType: json['expenseCategory'] != null + ? ExpensesType.fromJson(json['expenseCategory']) : ExpensesType.empty(), paymentMode: json['paymentMode'] != null ? PaymentMode.fromJson(json['paymentMode']) : PaymentMode.empty(), - paidBy: json['paidBy'] != null ? Person.fromJson(json['paidBy']) : Person.empty(), - createdBy: json['createdBy'] != null ? Person.fromJson(json['createdBy']) : Person.empty(), - reviewedBy: json['reviewedBy'] != null ? Person.fromJson(json['reviewedBy']) : null, - approvedBy: json['approvedBy'] != null ? Person.fromJson(json['approvedBy']) : null, - processedBy: json['processedBy'] != null ? Person.fromJson(json['processedBy']) : null, + paidBy: json['paidBy'] != null + ? Person.fromJson(json['paidBy']) + : Person.empty(), + createdBy: json['createdBy'] != null + ? Person.fromJson(json['createdBy']) + : Person.empty(), + reviewedBy: json['reviewedBy'] != null + ? Person.fromJson(json['reviewedBy']) + : null, + approvedBy: json['approvedBy'] != null + ? Person.fromJson(json['approvedBy']) + : null, + processedBy: json['processedBy'] != null + ? Person.fromJson(json['processedBy']) + : null, transactionDate: json['transactionDate'] ?? '', createdAt: json['createdAt'] ?? '', supplerName: json['supplerName'] ?? '', amount: (json['amount'] as num?)?.toDouble() ?? 0.0, - status: json['status'] != null ? ExpenseStatus.fromJson(json['status']) : ExpenseStatus.empty(), - nextStatus: (json['nextStatus'] as List?)?.map((e) => ExpenseStatus.fromJson(e)).toList() ?? [], + baseAmount: (json['baseAmount'] as num?)?.toDouble(), + taxAmount: (json['taxAmount'] as num?)?.toDouble(), + tdsPercentage: (json['tdsPercentage'] as num?)?.toDouble(), + status: json['status'] != null + ? ExpenseStatus.fromJson(json['status']) + : ExpenseStatus.empty(), + nextStatus: (json['nextStatus'] as List?) + ?.map((e) => ExpenseStatus.fromJson(e)) + .toList() ?? + [], preApproved: json['preApproved'] ?? false, transactionId: json['transactionId'] ?? '', description: json['description'] ?? '', location: json['location'] ?? '', - documents: (json['documents'] as List?)?.map((e) => ExpenseDocument.fromJson(e)).toList() ?? [], - expenseLogs: (json['expenseLogs'] as List?)?.map((e) => ExpenseLog.fromJson(e)).toList() ?? [], + currency: + json['currency'] != null ? Currency.fromJson(json['currency']) : null, + documents: (json['documents'] as List?) + ?.map((e) => ExpenseDocument.fromJson(e)) + .toList() ?? + [], + expenseLogs: (json['expenseLogs'] as List?) + ?.map((e) => ExpenseLog.fromJson(e)) + .toList() ?? + [], gstNumber: json['gstNumber']?.toString(), - noOfPersons: json['noOfPersons'] ?? 0, + noOfPersons: json['noOfPersons'] != null ? json['noOfPersons'] : null, isActive: json['isActive'] ?? true, expensesReimburse: json['expensesReimburse'], + expenseUId: json['expenseUId']?.toString(), + paymentRequestUID: json['paymentRequestUID']?.toString(), ); } } @@ -94,20 +137,20 @@ class Project { final String id; final String name; final String shortName; - final String projectAddress; - final String contactPerson; - final String startDate; - final String endDate; + final String? projectAddress; + final String? contactPerson; + final String? startDate; + final String? endDate; final String projectStatusId; Project({ required this.id, required this.name, required this.shortName, - required this.projectAddress, - required this.contactPerson, - required this.startDate, - required this.endDate, + this.projectAddress, + this.contactPerson, + this.startDate, + this.endDate, required this.projectStatusId, }); @@ -116,10 +159,10 @@ class Project { id: json['id'] ?? '', name: json['name'] ?? '', shortName: json['shortName'] ?? '', - projectAddress: json['projectAddress'] ?? '', - contactPerson: json['contactPerson'] ?? '', - startDate: json['startDate'] ?? '', - endDate: json['endDate'] ?? '', + projectAddress: json['projectAddress'], + contactPerson: json['contactPerson'], + startDate: json['startDate'], + endDate: json['endDate'], projectStatusId: json['projectStatusId'] ?? '', ); } @@ -128,10 +171,6 @@ class Project { id: '', name: '', shortName: '', - projectAddress: '', - contactPerson: '', - startDate: '', - endDate: '', projectStatusId: '', ); } @@ -141,12 +180,14 @@ class ExpensesType { final String id; final String name; final bool noOfPersonsRequired; + final bool isAttachmentRequried; final String description; ExpensesType({ required this.id, required this.name, required this.noOfPersonsRequired, + required this.isAttachmentRequried, required this.description, }); @@ -155,6 +196,7 @@ class ExpensesType { id: json['id'] ?? '', name: json['name'] ?? '', noOfPersonsRequired: json['noOfPersonsRequired'] ?? false, + isAttachmentRequried: json['isAttachmentRequried'] ?? false, description: json['description'] ?? '', ); } @@ -163,6 +205,7 @@ class ExpensesType { id: '', name: '', noOfPersonsRequired: false, + isAttachmentRequried: false, description: '', ); } @@ -199,6 +242,7 @@ class Person { final String id; final String firstName; final String lastName; + final String? email; final String photo; final String jobRoleId; final String jobRoleName; @@ -207,6 +251,7 @@ class Person { required this.id, required this.firstName, required this.lastName, + this.email, required this.photo, required this.jobRoleId, required this.jobRoleName, @@ -217,7 +262,8 @@ class Person { id: json['id'] ?? '', firstName: json['firstName'] ?? '', lastName: json['lastName'] ?? '', - photo: json['photo'] is String ? json['photo'] : '', + email: json['email'], + photo: json['photo'] ?? '', jobRoleId: json['jobRoleId'] ?? '', jobRoleName: json['jobRoleName'] ?? '', ); @@ -227,6 +273,7 @@ class Person { id: '', firstName: '', lastName: '', + email: null, photo: '', jobRoleId: '', jobRoleName: '', @@ -322,10 +369,39 @@ class ExpenseLog { factory ExpenseLog.fromJson(Map json) { return ExpenseLog( id: json['id'] ?? '', - updatedBy: json['updatedBy'] != null ? Person.fromJson(json['updatedBy']) : Person.empty(), + updatedBy: json['updatedBy'] != null + ? Person.fromJson(json['updatedBy']) + : Person.empty(), action: json['action'] ?? '', updateAt: json['updateAt'] ?? '', comment: json['comment'] ?? '', ); } } + +// ---------------- Currency ---------------- +class Currency { + 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, + }); + + factory Currency.fromJson(Map json) { + return Currency( + id: json['id'] ?? '', + currencyCode: json['currencyCode'] ?? '', + currencyName: json['currencyName'] ?? '', + symbol: json['symbol'] ?? '', + isActive: json['isActive'] ?? true, + ); + } +} diff --git a/lib/view/expense/expense_detail_screen.dart b/lib/view/expense/expense_detail_screen.dart index b9109c1..187edae 100644 --- a/lib/view/expense/expense_detail_screen.dart +++ b/lib/view/expense/expense_detail_screen.dart @@ -97,49 +97,93 @@ class _ExpenseDetailScreenState extends State final formattedAmount = formatExpenseAmount(expense.amount); return MyRefreshIndicator( - onRefresh: () async { - await controller.fetchExpenseDetails(); - }, - child: SingleChildScrollView( - padding: EdgeInsets.fromLTRB( - 8, 8, 8, 30 + MediaQuery.of(context).padding.bottom), - child: Center( - child: Container( - constraints: const BoxConstraints(maxWidth: 520), - child: Card( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(5)), - elevation: 3, - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 14, horizontal: 14), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _InvoiceHeader(expense: expense), - const Divider(height: 30, thickness: 1.2), - InvoiceLogs(logs: expense.expenseLogs), - const Divider(height: 30, thickness: 1.2), - _InvoiceParties(expense: expense), - const Divider(height: 30, thickness: 1.2), - _InvoiceDetailsTable(expense: expense), - const Divider(height: 30, thickness: 1.2), - _InvoiceDocuments(documents: expense.documents), - const Divider(height: 30, thickness: 1.2), - _InvoiceTotals( - expense: expense, - formattedAmount: formattedAmount, - statusColor: statusColor, - ), - const Divider(height: 30, thickness: 1.2), - ], + onRefresh: () async { + await controller.fetchExpenseDetails(); + }, + child: SingleChildScrollView( + padding: EdgeInsets.fromLTRB( + 12, 12, 12, 30 + MediaQuery.of(context).padding.bottom), + child: Center( + child: Container( + constraints: const BoxConstraints(maxWidth: 520), + child: Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5)), + elevation: 3, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 14, horizontal: 14), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // ---------------- Header & Status ---------------- + _InvoiceHeader(expense: expense), + const Divider(height: 30, thickness: 1.2), + + // ---------------- Activity Logs ---------------- + InvoiceLogs(logs: expense.expenseLogs), + const Divider(height: 30, thickness: 1.2), + // ---------------- Amount & Summary ---------------- + Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.bodyMedium('Amount', + fontWeight: 600), + const SizedBox(height: 4), + MyText.bodyLarge( + formattedAmount, + fontWeight: 700, + color: statusColor, + ), + ], + ), + const Spacer(), + // Optional: Pre-approved badge + if (expense.preApproved) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: Colors.green.withOpacity(0.15), + borderRadius: BorderRadius.circular(5), + ), + child: MyText.bodySmall( + 'Pre-Approved', + fontWeight: 600, + color: Colors.green, + ), + ), + ], + ), + const Divider(height: 30, thickness: 1.2), + + // ---------------- Parties ---------------- + _InvoicePartiesTable(expense: expense), + const Divider(height: 30, thickness: 1.2), + + // ---------------- Expense Details ---------------- + _InvoiceDetailsTable(expense: expense), + const Divider(height: 30, thickness: 1.2), + + // ---------------- Documents ---------------- + _InvoiceDocuments(documents: expense.documents), + const Divider(height: 30, thickness: 1.2), + + // ---------------- Totals ---------------- + _InvoiceTotals( + expense: expense, + formattedAmount: formattedAmount, + statusColor: statusColor, + ), + ], + ), ), ), ), ), - ), - ), - ); + )); }), ), floatingActionButton: Obx(() { @@ -498,24 +542,43 @@ class _InvoiceHeader extends StatelessWidget { } } -class _InvoiceParties extends StatelessWidget { +class _InvoicePartiesTable extends StatelessWidget { final ExpenseDetailModel expense; - const _InvoiceParties({required this.expense}); + const _InvoicePartiesTable({required this.expense}); + @override Widget build(BuildContext context) { + // List of label-value pairs + final parties = [ + {'label': 'Project', 'value': expense.project.name}, + { + 'label': 'Paid By', + 'value': '${expense.paidBy.firstName} ${expense.paidBy.lastName}' + }, + {'label': 'Supplier', 'value': expense.supplerName}, + { + 'label': 'Created By', + 'value': '${expense.createdBy.firstName} ${expense.createdBy.lastName}' + }, + ]; + return Column( crossAxisAlignment: CrossAxisAlignment.start, - children: [ - labelValueBlock('Project', expense.project.name), - MySpacing.height(16), - labelValueBlock('Paid By:', - '${expense.paidBy.firstName} ${expense.paidBy.lastName}'), - MySpacing.height(16), - labelValueBlock('Supplier', expense.supplerName), - MySpacing.height(16), - labelValueBlock('Created By:', - '${expense.createdBy.firstName} ${expense.createdBy.lastName}'), - ], + children: parties.map((item) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + MyText.bodySmall('${item['label']}:', fontWeight: 600), + const SizedBox(width: 6), + Expanded( + child: MyText.bodySmall(item['value']!, fontWeight: 500), + ), + ], + ), + ); + }).toList(), ); } } @@ -523,6 +586,7 @@ class _InvoiceParties extends StatelessWidget { class _InvoiceDetailsTable extends StatelessWidget { final ExpenseDetailModel expense; const _InvoiceDetailsTable({required this.expense}); + @override Widget build(BuildContext context) { final transactionDate = DateTimeUtils.convertUtcToLocal( @@ -531,36 +595,45 @@ class _InvoiceDetailsTable extends StatelessWidget { final createdAt = DateTimeUtils.convertUtcToLocal( expense.createdAt.toString(), format: 'dd MMM yyyy'); + + // List of all label-value pairs + final details = [ + {'label': 'Expense Type', 'value': expense.expensesType.name}, + {'label': 'Payment Mode', 'value': expense.paymentMode.name}, + {'label': 'Transaction Date', 'value': transactionDate}, + {'label': 'Created At', 'value': createdAt}, + {'label': 'Pre-Approved', 'value': expense.preApproved ? 'Yes' : 'No'}, + { + 'label': 'Description', + 'value': + expense.description.trim().isNotEmpty ? expense.description : '-' + }, + ]; + return Column( crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _detailItem("Expense Type:", expense.expensesType.name), - _detailItem("Payment Mode:", expense.paymentMode.name), - _detailItem("Transaction Date:", transactionDate), - _detailItem("Created At:", createdAt), - _detailItem("Pre-Approved:", expense.preApproved ? 'Yes' : 'No'), - _detailItem("Description:", - expense.description.trim().isNotEmpty ? expense.description : '-', - isDescription: true), - ], + children: details.map((item) { + final isDescription = item['label'] == 'Description'; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: Row( + crossAxisAlignment: isDescription + ? CrossAxisAlignment.start + : CrossAxisAlignment.center, + children: [ + MyText.bodySmall('${item['label']}:', fontWeight: 600), + const SizedBox(width: 6), + Expanded( + child: isDescription + ? ExpandableDescription(description: item['value']!) + : MyText.bodySmall(item['value']!, fontWeight: 500), + ), + ], + ), + ); + }).toList(), ); } - - Widget _detailItem(String title, String value, - {bool isDescription = false}) => - Padding( - padding: const EdgeInsets.symmetric(vertical: 10), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MyText.bodySmall(title, fontWeight: 600), - MySpacing.height(3), - isDescription - ? ExpandableDescription(description: value) - : MyText.bodySmall(value, fontWeight: 500), - ], - ), - ); } class _InvoiceDocuments extends StatelessWidget {