feat: enhance ExpenseDetailModel and ExpenseDetailScreen with additional fields and improved UI structure

This commit is contained in:
Vaibhav Surve 2025-11-19 17:17:04 +05:30
parent 8edd189479
commit 5dd09869ad
2 changed files with 262 additions and 113 deletions

View File

@ -2,7 +2,7 @@
class ExpenseDetailModel { class ExpenseDetailModel {
final String id; final String id;
final Project project; final Project project;
final ExpensesType expensesType; final ExpensesType expensesType;
final PaymentMode paymentMode; final PaymentMode paymentMode;
final Person paidBy; final Person paidBy;
final Person createdBy; final Person createdBy;
@ -13,18 +13,25 @@ class ExpenseDetailModel {
final String createdAt; final String createdAt;
final String supplerName; final String supplerName;
final double amount; final double amount;
final double? baseAmount;
final double? taxAmount;
final double? tdsPercentage;
final ExpenseStatus status; final ExpenseStatus status;
final List<ExpenseStatus> nextStatus; final List nextStatus;
final bool preApproved; final bool preApproved;
final String transactionId; final String transactionId;
final String description; final String description;
final String location; final String location;
final Currency? currency;
final List<ExpenseDocument> documents; final List<ExpenseDocument> documents;
final List<ExpenseLog> expenseLogs; final List<ExpenseLog> expenseLogs;
final String? gstNumber; final String? gstNumber;
final int noOfPersons; final int? noOfPersons;
final bool isActive; final bool isActive;
final dynamic expensesReimburse; final dynamic expensesReimburse;
final String? expenseUId;
final String? paymentRequestUID;
ExpenseDetailModel({ ExpenseDetailModel({
required this.id, required this.id,
@ -40,51 +47,87 @@ class ExpenseDetailModel {
required this.createdAt, required this.createdAt,
required this.supplerName, required this.supplerName,
required this.amount, required this.amount,
this.baseAmount,
this.taxAmount,
this.tdsPercentage,
required this.status, required this.status,
required this.nextStatus, required this.nextStatus,
required this.preApproved, required this.preApproved,
required this.transactionId, required this.transactionId,
required this.description, required this.description,
required this.location, required this.location,
this.currency,
required this.documents, required this.documents,
required this.expenseLogs, required this.expenseLogs,
this.gstNumber, this.gstNumber,
required this.noOfPersons, this.noOfPersons,
required this.isActive, required this.isActive,
this.expensesReimburse, this.expensesReimburse,
this.expenseUId,
this.paymentRequestUID,
}); });
factory ExpenseDetailModel.fromJson(Map<String, dynamic> json) { factory ExpenseDetailModel.fromJson(Map<String, dynamic> json) {
return ExpenseDetailModel( return ExpenseDetailModel(
id: json['id'] ?? '', id: json['id'] ?? '',
project: json['project'] != null ? Project.fromJson(json['project']) : Project.empty(), project: json['project'] != null
expensesType: json['expensesType'] != null ? Project.fromJson(json['project'])
? ExpensesType.fromJson(json['expensesType']) : Project.empty(),
expensesType: json['expenseCategory'] != null
? ExpensesType.fromJson(json['expenseCategory'])
: ExpensesType.empty(), : ExpensesType.empty(),
paymentMode: json['paymentMode'] != null paymentMode: json['paymentMode'] != null
? PaymentMode.fromJson(json['paymentMode']) ? PaymentMode.fromJson(json['paymentMode'])
: PaymentMode.empty(), : PaymentMode.empty(),
paidBy: json['paidBy'] != null ? Person.fromJson(json['paidBy']) : Person.empty(), paidBy: json['paidBy'] != null
createdBy: json['createdBy'] != null ? Person.fromJson(json['createdBy']) : Person.empty(), ? Person.fromJson(json['paidBy'])
reviewedBy: json['reviewedBy'] != null ? Person.fromJson(json['reviewedBy']) : null, : Person.empty(),
approvedBy: json['approvedBy'] != null ? Person.fromJson(json['approvedBy']) : null, createdBy: json['createdBy'] != null
processedBy: json['processedBy'] != null ? Person.fromJson(json['processedBy']) : 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'] ?? '', transactionDate: json['transactionDate'] ?? '',
createdAt: json['createdAt'] ?? '', createdAt: json['createdAt'] ?? '',
supplerName: json['supplerName'] ?? '', supplerName: json['supplerName'] ?? '',
amount: (json['amount'] as num?)?.toDouble() ?? 0.0, amount: (json['amount'] as num?)?.toDouble() ?? 0.0,
status: json['status'] != null ? ExpenseStatus.fromJson(json['status']) : ExpenseStatus.empty(), baseAmount: (json['baseAmount'] as num?)?.toDouble(),
nextStatus: (json['nextStatus'] as List?)?.map((e) => ExpenseStatus.fromJson(e)).toList() ?? [], 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, preApproved: json['preApproved'] ?? false,
transactionId: json['transactionId'] ?? '', transactionId: json['transactionId'] ?? '',
description: json['description'] ?? '', description: json['description'] ?? '',
location: json['location'] ?? '', location: json['location'] ?? '',
documents: (json['documents'] as List?)?.map((e) => ExpenseDocument.fromJson(e)).toList() ?? [], currency:
expenseLogs: (json['expenseLogs'] as List?)?.map((e) => ExpenseLog.fromJson(e)).toList() ?? [], 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(), gstNumber: json['gstNumber']?.toString(),
noOfPersons: json['noOfPersons'] ?? 0, noOfPersons: json['noOfPersons'] != null ? json['noOfPersons'] : null,
isActive: json['isActive'] ?? true, isActive: json['isActive'] ?? true,
expensesReimburse: json['expensesReimburse'], expensesReimburse: json['expensesReimburse'],
expenseUId: json['expenseUId']?.toString(),
paymentRequestUID: json['paymentRequestUID']?.toString(),
); );
} }
} }
@ -94,20 +137,20 @@ class Project {
final String id; final String id;
final String name; final String name;
final String shortName; final String shortName;
final String projectAddress; final String? projectAddress;
final String contactPerson; final String? contactPerson;
final String startDate; final String? startDate;
final String endDate; final String? endDate;
final String projectStatusId; final String projectStatusId;
Project({ Project({
required this.id, required this.id,
required this.name, required this.name,
required this.shortName, required this.shortName,
required this.projectAddress, this.projectAddress,
required this.contactPerson, this.contactPerson,
required this.startDate, this.startDate,
required this.endDate, this.endDate,
required this.projectStatusId, required this.projectStatusId,
}); });
@ -116,10 +159,10 @@ class Project {
id: json['id'] ?? '', id: json['id'] ?? '',
name: json['name'] ?? '', name: json['name'] ?? '',
shortName: json['shortName'] ?? '', shortName: json['shortName'] ?? '',
projectAddress: json['projectAddress'] ?? '', projectAddress: json['projectAddress'],
contactPerson: json['contactPerson'] ?? '', contactPerson: json['contactPerson'],
startDate: json['startDate'] ?? '', startDate: json['startDate'],
endDate: json['endDate'] ?? '', endDate: json['endDate'],
projectStatusId: json['projectStatusId'] ?? '', projectStatusId: json['projectStatusId'] ?? '',
); );
} }
@ -128,10 +171,6 @@ class Project {
id: '', id: '',
name: '', name: '',
shortName: '', shortName: '',
projectAddress: '',
contactPerson: '',
startDate: '',
endDate: '',
projectStatusId: '', projectStatusId: '',
); );
} }
@ -141,12 +180,14 @@ class ExpensesType {
final String id; final String id;
final String name; final String name;
final bool noOfPersonsRequired; final bool noOfPersonsRequired;
final bool isAttachmentRequried;
final String description; final String description;
ExpensesType({ ExpensesType({
required this.id, required this.id,
required this.name, required this.name,
required this.noOfPersonsRequired, required this.noOfPersonsRequired,
required this.isAttachmentRequried,
required this.description, required this.description,
}); });
@ -155,6 +196,7 @@ class ExpensesType {
id: json['id'] ?? '', id: json['id'] ?? '',
name: json['name'] ?? '', name: json['name'] ?? '',
noOfPersonsRequired: json['noOfPersonsRequired'] ?? false, noOfPersonsRequired: json['noOfPersonsRequired'] ?? false,
isAttachmentRequried: json['isAttachmentRequried'] ?? false,
description: json['description'] ?? '', description: json['description'] ?? '',
); );
} }
@ -163,6 +205,7 @@ class ExpensesType {
id: '', id: '',
name: '', name: '',
noOfPersonsRequired: false, noOfPersonsRequired: false,
isAttachmentRequried: false,
description: '', description: '',
); );
} }
@ -199,6 +242,7 @@ class Person {
final String id; final String id;
final String firstName; final String firstName;
final String lastName; final String lastName;
final String? email;
final String photo; final String photo;
final String jobRoleId; final String jobRoleId;
final String jobRoleName; final String jobRoleName;
@ -207,6 +251,7 @@ class Person {
required this.id, required this.id,
required this.firstName, required this.firstName,
required this.lastName, required this.lastName,
this.email,
required this.photo, required this.photo,
required this.jobRoleId, required this.jobRoleId,
required this.jobRoleName, required this.jobRoleName,
@ -217,7 +262,8 @@ class Person {
id: json['id'] ?? '', id: json['id'] ?? '',
firstName: json['firstName'] ?? '', firstName: json['firstName'] ?? '',
lastName: json['lastName'] ?? '', lastName: json['lastName'] ?? '',
photo: json['photo'] is String ? json['photo'] : '', email: json['email'],
photo: json['photo'] ?? '',
jobRoleId: json['jobRoleId'] ?? '', jobRoleId: json['jobRoleId'] ?? '',
jobRoleName: json['jobRoleName'] ?? '', jobRoleName: json['jobRoleName'] ?? '',
); );
@ -227,6 +273,7 @@ class Person {
id: '', id: '',
firstName: '', firstName: '',
lastName: '', lastName: '',
email: null,
photo: '', photo: '',
jobRoleId: '', jobRoleId: '',
jobRoleName: '', jobRoleName: '',
@ -322,10 +369,39 @@ class ExpenseLog {
factory ExpenseLog.fromJson(Map<String, dynamic> json) { factory ExpenseLog.fromJson(Map<String, dynamic> json) {
return ExpenseLog( return ExpenseLog(
id: json['id'] ?? '', 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'] ?? '', action: json['action'] ?? '',
updateAt: json['updateAt'] ?? '', updateAt: json['updateAt'] ?? '',
comment: json['comment'] ?? '', 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<String, dynamic> json) {
return Currency(
id: json['id'] ?? '',
currencyCode: json['currencyCode'] ?? '',
currencyName: json['currencyName'] ?? '',
symbol: json['symbol'] ?? '',
isActive: json['isActive'] ?? true,
);
}
}

View File

@ -97,49 +97,93 @@ class _ExpenseDetailScreenState extends State<ExpenseDetailScreen>
final formattedAmount = formatExpenseAmount(expense.amount); final formattedAmount = formatExpenseAmount(expense.amount);
return MyRefreshIndicator( return MyRefreshIndicator(
onRefresh: () async { onRefresh: () async {
await controller.fetchExpenseDetails(); await controller.fetchExpenseDetails();
}, },
child: SingleChildScrollView( child: SingleChildScrollView(
padding: EdgeInsets.fromLTRB( padding: EdgeInsets.fromLTRB(
8, 8, 8, 30 + MediaQuery.of(context).padding.bottom), 12, 12, 12, 30 + MediaQuery.of(context).padding.bottom),
child: Center( child: Center(
child: Container( child: Container(
constraints: const BoxConstraints(maxWidth: 520), constraints: const BoxConstraints(maxWidth: 520),
child: Card( child: Card(
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5)), borderRadius: BorderRadius.circular(5)),
elevation: 3, elevation: 3,
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
vertical: 14, horizontal: 14), vertical: 14, horizontal: 14),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_InvoiceHeader(expense: expense), // ---------------- Header & Status ----------------
const Divider(height: 30, thickness: 1.2), _InvoiceHeader(expense: expense),
InvoiceLogs(logs: expense.expenseLogs), const Divider(height: 30, thickness: 1.2),
const Divider(height: 30, thickness: 1.2),
_InvoiceParties(expense: expense), // ---------------- Activity Logs ----------------
const Divider(height: 30, thickness: 1.2), InvoiceLogs(logs: expense.expenseLogs),
_InvoiceDetailsTable(expense: expense), const Divider(height: 30, thickness: 1.2),
const Divider(height: 30, thickness: 1.2), // ---------------- Amount & Summary ----------------
_InvoiceDocuments(documents: expense.documents), Row(
const Divider(height: 30, thickness: 1.2), children: [
_InvoiceTotals( Column(
expense: expense, crossAxisAlignment: CrossAxisAlignment.start,
formattedAmount: formattedAmount, children: [
statusColor: statusColor, MyText.bodyMedium('Amount',
), fontWeight: 600),
const Divider(height: 30, thickness: 1.2), 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(() { floatingActionButton: Obx(() {
@ -498,24 +542,43 @@ class _InvoiceHeader extends StatelessWidget {
} }
} }
class _InvoiceParties extends StatelessWidget { class _InvoicePartiesTable extends StatelessWidget {
final ExpenseDetailModel expense; final ExpenseDetailModel expense;
const _InvoiceParties({required this.expense}); const _InvoicePartiesTable({required this.expense});
@override @override
Widget build(BuildContext context) { 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( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: parties.map((item) {
labelValueBlock('Project', expense.project.name), return Padding(
MySpacing.height(16), padding: const EdgeInsets.symmetric(vertical: 6),
labelValueBlock('Paid By:', child: Row(
'${expense.paidBy.firstName} ${expense.paidBy.lastName}'), crossAxisAlignment: CrossAxisAlignment.center,
MySpacing.height(16), children: [
labelValueBlock('Supplier', expense.supplerName), MyText.bodySmall('${item['label']}:', fontWeight: 600),
MySpacing.height(16), const SizedBox(width: 6),
labelValueBlock('Created By:', Expanded(
'${expense.createdBy.firstName} ${expense.createdBy.lastName}'), child: MyText.bodySmall(item['value']!, fontWeight: 500),
], ),
],
),
);
}).toList(),
); );
} }
} }
@ -523,6 +586,7 @@ class _InvoiceParties extends StatelessWidget {
class _InvoiceDetailsTable extends StatelessWidget { class _InvoiceDetailsTable extends StatelessWidget {
final ExpenseDetailModel expense; final ExpenseDetailModel expense;
const _InvoiceDetailsTable({required this.expense}); const _InvoiceDetailsTable({required this.expense});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final transactionDate = DateTimeUtils.convertUtcToLocal( final transactionDate = DateTimeUtils.convertUtcToLocal(
@ -531,36 +595,45 @@ class _InvoiceDetailsTable extends StatelessWidget {
final createdAt = DateTimeUtils.convertUtcToLocal( final createdAt = DateTimeUtils.convertUtcToLocal(
expense.createdAt.toString(), expense.createdAt.toString(),
format: 'dd MMM yyyy'); 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( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: details.map((item) {
_detailItem("Expense Type:", expense.expensesType.name), final isDescription = item['label'] == 'Description';
_detailItem("Payment Mode:", expense.paymentMode.name), return Padding(
_detailItem("Transaction Date:", transactionDate), padding: const EdgeInsets.symmetric(vertical: 6),
_detailItem("Created At:", createdAt), child: Row(
_detailItem("Pre-Approved:", expense.preApproved ? 'Yes' : 'No'), crossAxisAlignment: isDescription
_detailItem("Description:", ? CrossAxisAlignment.start
expense.description.trim().isNotEmpty ? expense.description : '-', : CrossAxisAlignment.center,
isDescription: true), 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 { class _InvoiceDocuments extends StatelessWidget {