From fa3109e6068d7f86753212c75c251690e986ed53 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Mon, 18 Aug 2025 17:33:17 +0530 Subject: [PATCH] feat: enhance expense detail model and screen with activity logs and new dependencies --- lib/model/expense/expense_detail_model.dart | 159 ++++++++++++++------ lib/view/expense/expense_detail_screen.dart | 98 +++++++++++- pubspec.yaml | 1 + 3 files changed, 215 insertions(+), 43 deletions(-) diff --git a/lib/model/expense/expense_detail_model.dart b/lib/model/expense/expense_detail_model.dart index e056d3c..492e357 100644 --- a/lib/model/expense/expense_detail_model.dart +++ b/lib/model/expense/expense_detail_model.dart @@ -5,6 +5,9 @@ class ExpenseDetailModel { final PaymentMode paymentMode; final Person paidBy; final Person createdBy; + final Person? reviewedBy; + final Person? approvedBy; + final Person? processedBy; final String transactionDate; final String createdAt; final String supplerName; @@ -16,9 +19,11 @@ class ExpenseDetailModel { final String description; final String location; final List documents; + final List expenseLogs; final String? gstNumber; final int noOfPersons; final bool isActive; + final dynamic expensesReimburse; // can be replaced with model later ExpenseDetailModel({ required this.id, @@ -27,6 +32,9 @@ class ExpenseDetailModel { required this.paymentMode, required this.paidBy, required this.createdBy, + this.reviewedBy, + this.approvedBy, + this.processedBy, required this.transactionDate, required this.createdAt, required this.supplerName, @@ -38,37 +46,70 @@ class ExpenseDetailModel { required this.description, required this.location, required this.documents, + required this.expenseLogs, this.gstNumber, required this.noOfPersons, required this.isActive, + this.expensesReimburse, }); 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']) : 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(), + project: json['project'] != null + ? Project.fromJson(json['project']) + : Project.empty(), + expensesType: json['expensesType'] != null + ? ExpensesType.fromJson(json['expensesType']) + : 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, 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() ?? [], + 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() ?? [], + 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, isActive: json['isActive'] ?? true, + expensesReimburse: json['expensesReimburse'], ); } } +// ---------------- Project ---------------- class Project { final String id; final String name; @@ -104,17 +145,18 @@ class Project { } factory Project.empty() => Project( - id: '', - name: '', - shortName: '', - projectAddress: '', - contactPerson: '', - startDate: '', - endDate: '', - projectStatusId: '', - ); + id: '', + name: '', + shortName: '', + projectAddress: '', + contactPerson: '', + startDate: '', + endDate: '', + projectStatusId: '', + ); } +// ---------------- ExpensesType ---------------- class ExpensesType { final String id; final String name; @@ -138,13 +180,14 @@ class ExpensesType { } factory ExpensesType.empty() => ExpensesType( - id: '', - name: '', - noOfPersonsRequired: false, - description: '', - ); + id: '', + name: '', + noOfPersonsRequired: false, + description: '', + ); } +// ---------------- PaymentMode ---------------- class PaymentMode { final String id; final String name; @@ -165,12 +208,13 @@ class PaymentMode { } factory PaymentMode.empty() => PaymentMode( - id: '', - name: '', - description: '', - ); + id: '', + name: '', + description: '', + ); } +// ---------------- Person ---------------- class Person { final String id; final String firstName; @@ -200,21 +244,22 @@ class Person { } factory Person.empty() => Person( - id: '', - firstName: '', - lastName: '', - photo: '', - jobRoleId: '', - jobRoleName: '', - ); + id: '', + firstName: '', + lastName: '', + photo: '', + jobRoleId: '', + jobRoleName: '', + ); } +// ---------------- ExpenseStatus ---------------- class ExpenseStatus { final String id; final String name; final String displayName; final String description; - final String? permissionIds; + final String? permissionIds; // API sends list, but can stringify final String color; final bool isSystem; @@ -241,16 +286,17 @@ class ExpenseStatus { } factory ExpenseStatus.empty() => ExpenseStatus( - id: '', - name: '', - displayName: '', - description: '', - permissionIds: null, - color: '', - isSystem: false, - ); + id: '', + name: '', + displayName: '', + description: '', + permissionIds: null, + color: '', + isSystem: false, + ); } +// ---------------- ExpenseDocument ---------------- class ExpenseDocument { final String documentId; final String fileName; @@ -276,3 +322,32 @@ class ExpenseDocument { ); } } + +// ---------------- ExpenseLog ---------------- +class ExpenseLog { + final String id; + final Person updatedBy; + final String action; + final String updateAt; + final String comment; + + ExpenseLog({ + required this.id, + required this.updatedBy, + required this.action, + required this.updateAt, + required this.comment, + }); + + factory ExpenseLog.fromJson(Map json) { + return ExpenseLog( + id: json['id'] ?? '', + updatedBy: json['updatedBy'] != null + ? Person.fromJson(json['updatedBy']) + : Person.empty(), + action: json['action'] ?? '', + updateAt: json['updateAt'] ?? '', + comment: json['comment'] ?? '', + ); + } +} diff --git a/lib/view/expense/expense_detail_screen.dart b/lib/view/expense/expense_detail_screen.dart index f305beb..4a7e89e 100644 --- a/lib/view/expense/expense_detail_screen.dart +++ b/lib/view/expense/expense_detail_screen.dart @@ -20,7 +20,7 @@ import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/helpers/services/storage/local_storage.dart'; import 'package:marco/model/employee_info.dart'; - +import 'package:timeline_tile/timeline_tile.dart'; class ExpenseDetailScreen extends StatefulWidget { final String expenseId; const ExpenseDetailScreen({super.key, required this.expenseId}); @@ -121,11 +121,14 @@ class _ExpenseDetailScreenState extends State { 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), + InvoiceLogs(logs: expense.expenseLogs), ], ), ), @@ -629,6 +632,99 @@ class _InvoiceDocuments extends StatelessWidget { } } +class InvoiceLogs extends StatelessWidget { + final List logs; + const InvoiceLogs({required this.logs}); + + @override + Widget build(BuildContext context) { + if (logs.isEmpty) { + return Padding( + padding: const EdgeInsets.all(16.0), + child: MyText.bodyMedium('No Activity Logs', color: Colors.grey), + ); + } + + final displayedLogs = logs.reversed.toList(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.bodySmall("Activity Logs:", fontWeight: 600), + const SizedBox(height: 16), + ListView.builder( + itemCount: displayedLogs.length, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (_, index) { + final log = displayedLogs[index]; + final formattedDate = DateTimeUtils.convertUtcToLocal( + log.updateAt, + format: 'dd MMM yyyy hh:mm a', + ); + + return TimelineTile( + alignment: TimelineAlign.start, + isFirst: index == 0, + isLast: index == displayedLogs.length - 1, + indicatorStyle: IndicatorStyle( + width: 16, + height: 16, + indicator: Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.blue.shade700, + ), + ), + ), + beforeLineStyle: LineStyle(color: Colors.grey.shade300, thickness: 2), + endChild: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.bodyMedium( + "${log.updatedBy.firstName} ${log.updatedBy.lastName}", + fontWeight: 600, + ), + const SizedBox(height: 4), + MyText.bodyMedium( + log.comment.isNotEmpty ? log.comment : log.action, + fontWeight: 500, + color: Colors.black87, + ), + const SizedBox(height: 8), + Row( + children: [ + Icon(Icons.access_time, size: 14, color: Colors.grey[600]), + const SizedBox(width: 4), + MyText.bodySmall(formattedDate, color: Colors.grey[700]), + ], + ), + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(6), + ), + child: MyText.bodySmall( + log.action, + color: Colors.blue.shade700, + fontWeight: 600, + ), + ), + ], + ), + ), + ); + }, + ), + ], + ); + } +} + class ExpensePermissionHelper { static bool canEditExpense( EmployeeInfo? employee, ExpenseDetailModel expense) { diff --git a/pubspec.yaml b/pubspec.yaml index b8ca6b3..b70a1a0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -79,6 +79,7 @@ dependencies: quill_delta: ^3.0.0-nullsafety.2 connectivity_plus: ^6.1.4 geocoding: ^4.0.0 + timeline_tile: ^2.0.0 dev_dependencies: flutter_test: sdk: flutter