Compare commits

...

1 Commits

3 changed files with 215 additions and 43 deletions

View File

@ -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<ExpenseDocument> documents;
final List<ExpenseLog> 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<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']) : 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<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'] ?? '',
);
}
}

View File

@ -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<ExpenseDetailScreen> {
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<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 {
static bool canEditExpense(
EmployeeInfo? employee, ExpenseDetailModel expense) {

View File

@ -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