feat: enhance ExpenseDetailModel and ExpenseDetailScreen with additional fields and improved UI structure
This commit is contained in:
parent
8edd189479
commit
5dd09869ad
@ -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<ExpenseStatus> nextStatus;
|
||||
final List nextStatus;
|
||||
final bool preApproved;
|
||||
final String transactionId;
|
||||
final String description;
|
||||
final String location;
|
||||
final Currency? currency;
|
||||
final List<ExpenseDocument> documents;
|
||||
final List<ExpenseLog> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> json) {
|
||||
return Currency(
|
||||
id: json['id'] ?? '',
|
||||
currencyCode: json['currencyCode'] ?? '',
|
||||
currencyName: json['currencyName'] ?? '',
|
||||
symbol: json['symbol'] ?? '',
|
||||
isActive: json['isActive'] ?? true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -97,49 +97,93 @@ class _ExpenseDetailScreenState extends State<ExpenseDetailScreen>
|
||||
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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user