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 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'] ?? '',
);
}
}

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/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) {

View File

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