feat: enhance expense detail model and screen with activity logs and new dependencies
This commit is contained in:
parent
1e48c686b2
commit
fa3109e606
@ -5,6 +5,9 @@ class ExpenseDetailModel {
|
|||||||
final PaymentMode paymentMode;
|
final PaymentMode paymentMode;
|
||||||
final Person paidBy;
|
final Person paidBy;
|
||||||
final Person createdBy;
|
final Person createdBy;
|
||||||
|
final Person? reviewedBy;
|
||||||
|
final Person? approvedBy;
|
||||||
|
final Person? processedBy;
|
||||||
final String transactionDate;
|
final String transactionDate;
|
||||||
final String createdAt;
|
final String createdAt;
|
||||||
final String supplerName;
|
final String supplerName;
|
||||||
@ -16,9 +19,11 @@ class ExpenseDetailModel {
|
|||||||
final String description;
|
final String description;
|
||||||
final String location;
|
final String location;
|
||||||
final List<ExpenseDocument> documents;
|
final List<ExpenseDocument> documents;
|
||||||
|
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; // can be replaced with model later
|
||||||
|
|
||||||
ExpenseDetailModel({
|
ExpenseDetailModel({
|
||||||
required this.id,
|
required this.id,
|
||||||
@ -27,6 +32,9 @@ class ExpenseDetailModel {
|
|||||||
required this.paymentMode,
|
required this.paymentMode,
|
||||||
required this.paidBy,
|
required this.paidBy,
|
||||||
required this.createdBy,
|
required this.createdBy,
|
||||||
|
this.reviewedBy,
|
||||||
|
this.approvedBy,
|
||||||
|
this.processedBy,
|
||||||
required this.transactionDate,
|
required this.transactionDate,
|
||||||
required this.createdAt,
|
required this.createdAt,
|
||||||
required this.supplerName,
|
required this.supplerName,
|
||||||
@ -38,37 +46,70 @@ class ExpenseDetailModel {
|
|||||||
required this.description,
|
required this.description,
|
||||||
required this.location,
|
required this.location,
|
||||||
required this.documents,
|
required this.documents,
|
||||||
|
required this.expenseLogs,
|
||||||
this.gstNumber,
|
this.gstNumber,
|
||||||
required this.noOfPersons,
|
required this.noOfPersons,
|
||||||
required this.isActive,
|
required this.isActive,
|
||||||
|
this.expensesReimburse,
|
||||||
});
|
});
|
||||||
|
|
||||||
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 ? ExpensesType.fromJson(json['expensesType']) : ExpensesType.empty(),
|
? Project.fromJson(json['project'])
|
||||||
paymentMode: json['paymentMode'] != null ? PaymentMode.fromJson(json['paymentMode']) : PaymentMode.empty(),
|
: Project.empty(),
|
||||||
paidBy: json['paidBy'] != null ? Person.fromJson(json['paidBy']) : Person.empty(),
|
expensesType: json['expensesType'] != null
|
||||||
createdBy: json['createdBy'] != null ? Person.fromJson(json['createdBy']) : Person.empty(),
|
? 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'] ?? '',
|
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(),
|
status: json['status'] != null
|
||||||
nextStatus: (json['nextStatus'] as List?)?.map((e) => ExpenseStatus.fromJson(e)).toList() ?? [],
|
? 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() ?? [],
|
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'] ?? 0,
|
||||||
isActive: json['isActive'] ?? true,
|
isActive: json['isActive'] ?? true,
|
||||||
|
expensesReimburse: json['expensesReimburse'],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------- Project ----------------
|
||||||
class Project {
|
class Project {
|
||||||
final String id;
|
final String id;
|
||||||
final String name;
|
final String name;
|
||||||
@ -104,17 +145,18 @@ class Project {
|
|||||||
}
|
}
|
||||||
|
|
||||||
factory Project.empty() => Project(
|
factory Project.empty() => Project(
|
||||||
id: '',
|
id: '',
|
||||||
name: '',
|
name: '',
|
||||||
shortName: '',
|
shortName: '',
|
||||||
projectAddress: '',
|
projectAddress: '',
|
||||||
contactPerson: '',
|
contactPerson: '',
|
||||||
startDate: '',
|
startDate: '',
|
||||||
endDate: '',
|
endDate: '',
|
||||||
projectStatusId: '',
|
projectStatusId: '',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------- ExpensesType ----------------
|
||||||
class ExpensesType {
|
class ExpensesType {
|
||||||
final String id;
|
final String id;
|
||||||
final String name;
|
final String name;
|
||||||
@ -138,13 +180,14 @@ class ExpensesType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
factory ExpensesType.empty() => ExpensesType(
|
factory ExpensesType.empty() => ExpensesType(
|
||||||
id: '',
|
id: '',
|
||||||
name: '',
|
name: '',
|
||||||
noOfPersonsRequired: false,
|
noOfPersonsRequired: false,
|
||||||
description: '',
|
description: '',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------- PaymentMode ----------------
|
||||||
class PaymentMode {
|
class PaymentMode {
|
||||||
final String id;
|
final String id;
|
||||||
final String name;
|
final String name;
|
||||||
@ -165,12 +208,13 @@ class PaymentMode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
factory PaymentMode.empty() => PaymentMode(
|
factory PaymentMode.empty() => PaymentMode(
|
||||||
id: '',
|
id: '',
|
||||||
name: '',
|
name: '',
|
||||||
description: '',
|
description: '',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------- Person ----------------
|
||||||
class Person {
|
class Person {
|
||||||
final String id;
|
final String id;
|
||||||
final String firstName;
|
final String firstName;
|
||||||
@ -200,21 +244,22 @@ class Person {
|
|||||||
}
|
}
|
||||||
|
|
||||||
factory Person.empty() => Person(
|
factory Person.empty() => Person(
|
||||||
id: '',
|
id: '',
|
||||||
firstName: '',
|
firstName: '',
|
||||||
lastName: '',
|
lastName: '',
|
||||||
photo: '',
|
photo: '',
|
||||||
jobRoleId: '',
|
jobRoleId: '',
|
||||||
jobRoleName: '',
|
jobRoleName: '',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------- ExpenseStatus ----------------
|
||||||
class ExpenseStatus {
|
class ExpenseStatus {
|
||||||
final String id;
|
final String id;
|
||||||
final String name;
|
final String name;
|
||||||
final String displayName;
|
final String displayName;
|
||||||
final String description;
|
final String description;
|
||||||
final String? permissionIds;
|
final String? permissionIds; // API sends list, but can stringify
|
||||||
final String color;
|
final String color;
|
||||||
final bool isSystem;
|
final bool isSystem;
|
||||||
|
|
||||||
@ -241,16 +286,17 @@ class ExpenseStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
factory ExpenseStatus.empty() => ExpenseStatus(
|
factory ExpenseStatus.empty() => ExpenseStatus(
|
||||||
id: '',
|
id: '',
|
||||||
name: '',
|
name: '',
|
||||||
displayName: '',
|
displayName: '',
|
||||||
description: '',
|
description: '',
|
||||||
permissionIds: null,
|
permissionIds: null,
|
||||||
color: '',
|
color: '',
|
||||||
isSystem: false,
|
isSystem: false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------- ExpenseDocument ----------------
|
||||||
class ExpenseDocument {
|
class ExpenseDocument {
|
||||||
final String documentId;
|
final String documentId;
|
||||||
final String fileName;
|
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<String, dynamic> 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'] ?? '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -20,7 +20,7 @@ import 'package:marco/helpers/widgets/my_spacing.dart';
|
|||||||
import 'package:marco/helpers/widgets/my_text.dart';
|
import 'package:marco/helpers/widgets/my_text.dart';
|
||||||
import 'package:marco/helpers/services/storage/local_storage.dart';
|
import 'package:marco/helpers/services/storage/local_storage.dart';
|
||||||
import 'package:marco/model/employee_info.dart';
|
import 'package:marco/model/employee_info.dart';
|
||||||
|
import 'package:timeline_tile/timeline_tile.dart';
|
||||||
class ExpenseDetailScreen extends StatefulWidget {
|
class ExpenseDetailScreen extends StatefulWidget {
|
||||||
final String expenseId;
|
final String expenseId;
|
||||||
const ExpenseDetailScreen({super.key, required this.expenseId});
|
const ExpenseDetailScreen({super.key, required this.expenseId});
|
||||||
@ -121,11 +121,14 @@ class _ExpenseDetailScreenState extends State<ExpenseDetailScreen> {
|
|||||||
const Divider(height: 30, thickness: 1.2),
|
const Divider(height: 30, thickness: 1.2),
|
||||||
_InvoiceDocuments(documents: expense.documents),
|
_InvoiceDocuments(documents: expense.documents),
|
||||||
const Divider(height: 30, thickness: 1.2),
|
const Divider(height: 30, thickness: 1.2),
|
||||||
|
|
||||||
_InvoiceTotals(
|
_InvoiceTotals(
|
||||||
expense: expense,
|
expense: expense,
|
||||||
formattedAmount: formattedAmount,
|
formattedAmount: formattedAmount,
|
||||||
statusColor: statusColor,
|
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<ExpenseLog> 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 {
|
class ExpensePermissionHelper {
|
||||||
static bool canEditExpense(
|
static bool canEditExpense(
|
||||||
EmployeeInfo? employee, ExpenseDetailModel expense) {
|
EmployeeInfo? employee, ExpenseDetailModel expense) {
|
||||||
|
@ -79,6 +79,7 @@ dependencies:
|
|||||||
quill_delta: ^3.0.0-nullsafety.2
|
quill_delta: ^3.0.0-nullsafety.2
|
||||||
connectivity_plus: ^6.1.4
|
connectivity_plus: ^6.1.4
|
||||||
geocoding: ^4.0.0
|
geocoding: ^4.0.0
|
||||||
|
timeline_tile: ^2.0.0
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
Loading…
x
Reference in New Issue
Block a user