diff --git a/lib/model/finance/add_payment_request_bottom_sheet.dart b/lib/model/finance/add_payment_request_bottom_sheet.dart index 07a0db5..f4a15a0 100644 --- a/lib/model/finance/add_payment_request_bottom_sheet.dart +++ b/lib/model/finance/add_payment_request_bottom_sheet.dart @@ -1,4 +1,3 @@ -// payment_request_bottom_sheet.dart import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:marco/controller/finance/add_payment_request_controller.dart'; @@ -10,16 +9,31 @@ import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:marco/helpers/widgets/expense/expense_form_widgets.dart'; import 'package:marco/helpers/widgets/my_confirmation_dialog.dart'; -Future showPaymentRequestBottomSheet({bool isEdit = false}) { +Future showPaymentRequestBottomSheet({ + bool isEdit = false, + Map? existingData, + VoidCallback? onUpdated, +}) { return Get.bottomSheet( - _PaymentRequestBottomSheet(isEdit: isEdit), + _PaymentRequestBottomSheet( + isEdit: isEdit, + existingData: existingData, + onUpdated: onUpdated, + ), isScrollControlled: true, ); } class _PaymentRequestBottomSheet extends StatefulWidget { final bool isEdit; - const _PaymentRequestBottomSheet({this.isEdit = false}); + final Map? existingData; + final VoidCallback? onUpdated; + + const _PaymentRequestBottomSheet({ + this.isEdit = false, + this.existingData, + this.onUpdated, + }); @override State<_PaymentRequestBottomSheet> createState() => @@ -35,6 +49,64 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> final _categoryDropdownKey = GlobalKey(); final _currencyDropdownKey = GlobalKey(); + @override + void initState() { + super.initState(); + + WidgetsBinding.instance.addPostFrameCallback((_) async { + if (widget.isEdit && widget.existingData != null) { + final data = widget.existingData!; + + // 🧩 Prefill basic text fields + controller.titleController.text = data["title"] ?? ""; + controller.amountController.text = data["amount"]?.toString() ?? ""; + controller.descriptionController.text = data["description"] ?? ""; + controller.dueDateController.text = + data["dueDate"]?.toString().split(" ")[0] ?? ""; + + // 🧩 Prefill dropdowns & toggles + controller.selectedProject.value = { + 'id': data["projectId"], + 'name': data["projectName"], + }; + controller.selectedPayee.value = data["payee"] ?? ""; + controller.isAdvancePayment.value = data["isAdvancePayment"] ?? false; + + // 🕒 Wait until categories & currencies are loaded before setting them + everAll([ + controller.categories, + controller.currencies, + ], (_) { + controller.selectedCategory.value = controller.categories + .firstWhereOrNull((c) => c.id == data["expenseCategoryId"]); + controller.selectedCurrency.value = controller.currencies + .firstWhereOrNull((c) => c.id == data["currencyId"]); + }); + + // 🖇 Attachments - Safe parsing (avoids null or wrong type) + final attachmentsData = data["attachments"]; + if (attachmentsData != null && + attachmentsData is List && + attachmentsData.isNotEmpty) { + final attachments = attachmentsData + .whereType>() + .map((a) => { + "id": a["id"], + "fileName": a["fileName"], + "url": a["url"], + "thumbUrl": a["thumbUrl"], + "fileSize": a["fileSize"] ?? 0, + "contentType": a["contentType"] ?? "", + }) + .toList(); + controller.existingAttachments.assignAll(attachments); + } else { + controller.existingAttachments.clear(); + } + } + }); + } + @override Widget build(BuildContext context) { return Obx(() => Form( @@ -49,12 +121,14 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> if (_formKey.currentState!.validate() && _validateSelections()) { final success = await controller.submitPaymentRequest(); if (success) { - // First close the BottomSheet Get.back(); - // Then show Snackbar + if (widget.onUpdated != null) widget.onUpdated!(); + showAppSnackbar( title: "Success", - message: "Payment request created successfully!", + message: widget.isEdit + ? "Payment request updated successfully!" + : "Payment request created successfully!", type: SnackbarType.success, ); } @@ -129,6 +203,8 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> )); } + // ---------------- Helper Widgets ---------------- + Widget _buildDropdown(String title, IconData icon, String value, List options, String Function(T) getLabel, ValueChanged onSelected, {required GlobalKey key}) { @@ -258,7 +334,6 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> displayStringForOption: (option) => option, fieldViewBuilder: (context, fieldController, focusNode, onFieldSubmitted) { - // Avoid updating during build WidgetsBinding.instance.addPostFrameCallback((_) { if (fieldController.text != controller.selectedPayee.value) { fieldController.text = controller.selectedPayee.value; @@ -425,12 +500,15 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> controller.selectedProject.value!['id'].toString().isEmpty) { return _showError("Please select a project"); } - if (controller.selectedCategory.value == null) + if (controller.selectedCategory.value == null) { return _showError("Please select a category"); - if (controller.selectedPayee.value.isEmpty) + } + if (controller.selectedPayee.value.isEmpty) { return _showError("Please select a payee"); - if (controller.selectedCurrency.value == null) + } + if (controller.selectedCurrency.value == null) { return _showError("Please select currency"); + } return true; } diff --git a/lib/model/finance/payment_request_details_model.dart b/lib/model/finance/payment_request_details_model.dart index a1c9820..e424cf0 100644 --- a/lib/model/finance/payment_request_details_model.dart +++ b/lib/model/finance/payment_request_details_model.dart @@ -118,8 +118,7 @@ class PaymentRequestData { expenseStatus: ExpenseStatus.fromJson(json['expenseStatus']), paidTransactionId: json['paidTransactionId'], paidAt: json['paidAt'] != null ? DateTime.parse(json['paidAt']) : null, - paidBy: - json['paidBy'] != null ? User.fromJson(json['paidBy']) : null, + paidBy: json['paidBy'] != null ? User.fromJson(json['paidBy']) : null, isAdvancePayment: json['isAdvancePayment'], createdAt: DateTime.parse(json['createdAt']), createdBy: User.fromJson(json['createdBy']), @@ -373,7 +372,7 @@ class NextStatus { class UpdateLog { String id; - ExpenseStatus status; + ExpenseStatus? status; ExpenseStatus nextStatus; String comment; DateTime updatedAt; @@ -381,7 +380,7 @@ class UpdateLog { UpdateLog({ required this.id, - required this.status, + this.status, required this.nextStatus, required this.comment, required this.updatedAt, @@ -390,7 +389,9 @@ class UpdateLog { factory UpdateLog.fromJson(Map json) => UpdateLog( id: json['id'], - status: ExpenseStatus.fromJson(json['status']), + status: json['status'] != null + ? ExpenseStatus.fromJson(json['status']) + : null, nextStatus: ExpenseStatus.fromJson(json['nextStatus']), comment: json['comment'], updatedAt: DateTime.parse(json['updatedAt']), @@ -399,7 +400,7 @@ class UpdateLog { Map toJson() => { 'id': id, - 'status': status.toJson(), + 'status': status?.toJson(), 'nextStatus': nextStatus.toJson(), 'comment': comment, 'updatedAt': updatedAt.toIso8601String(), @@ -441,4 +442,4 @@ class Attachment { 'fileSize': fileSize, 'contentType': contentType, }; -} +} \ No newline at end of file diff --git a/lib/view/faq/faq_screen.dart b/lib/view/faq/faq_screen.dart index 1f33a67..db364ed 100644 --- a/lib/view/faq/faq_screen.dart +++ b/lib/view/faq/faq_screen.dart @@ -115,7 +115,7 @@ class _FAQScreenState extends State with UIMixin { color: contentTheme.primary.withOpacity(0.1), shape: BoxShape.circle, ), - child: Icon(LucideIcons.badge_help, + child: Icon(LucideIcons.badge_alert, color: contentTheme.primary, size: 24), ), const SizedBox(width: 16), diff --git a/lib/view/finance/payment_request_detail_screen.dart b/lib/view/finance/payment_request_detail_screen.dart index f9b86a2..04a3a6b 100644 --- a/lib/view/finance/payment_request_detail_screen.dart +++ b/lib/view/finance/payment_request_detail_screen.dart @@ -20,6 +20,7 @@ import 'package:marco/model/employees/employee_info.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:marco/model/finance/payment_request_rembursement_bottom_sheet.dart'; import 'package:marco/model/finance/make_expense_bottom_sheet.dart'; +import 'package:marco/model/finance/add_payment_request_bottom_sheet.dart'; class PaymentRequestDetailScreen extends StatefulWidget { final String paymentRequestId; @@ -49,21 +50,10 @@ class _PaymentRequestDetailScreenState extends State void _checkPermissionToSubmit(PaymentRequestData request) { const draftStatusId = '6537018f-f4e9-4cb3-a210-6c3b2da999d7'; - final isCreatedByCurrentUser = employeeInfo?.id == request.createdBy.id; final hasDraftNextStatus = request.nextStatus.any((s) => s.id == draftStatusId); - - final result = isCreatedByCurrentUser && hasDraftNextStatus; - - // Debug log - print('🔐 Submit Permission Check:\n' - 'Logged-in employee: ${employeeInfo?.id}\n' - 'Created by: ${request.createdBy.id}\n' - 'Has Draft Next Status: $hasDraftNextStatus\n' - 'Can Submit: $result'); - - canSubmit.value = result; + canSubmit.value = isCreatedByCurrentUser && hasDraftNextStatus; } Future _loadEmployeeInfo() async { @@ -77,6 +67,38 @@ class _PaymentRequestDetailScreenState extends State return Color(int.parse(hex, radix: 16)); } + void _openEditPaymentRequestBottomSheet(request) { + showPaymentRequestBottomSheet( + isEdit: true, + existingData: { + "paymentRequestId": request.paymentRequestUID, + "title": request.title, + "projectId": request.project.id, + "projectName": request.project.name, + "expenseCategoryId": request.expenseCategory.id, + "expenseCategoryName": request.expenseCategory.name, + "amount": request.amount.toString(), + "currencyId": request.currency.id, + "currencySymbol": request.currency.symbol, + "payee": request.payee, + "description": request.description, + "isAdvancePayment": request.isAdvancePayment, + "dueDate": request.dueDate, + "attachments": request.attachments + .map((a) => { + "url": a.url, + "fileName": a.fileName, + "documentId": a.id, + "contentType": a.contentType, + }) + .toList(), + }, + onUpdated: () async { + await controller.fetchPaymentRequestDetail(); + }, + ); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -87,6 +109,7 @@ class _PaymentRequestDetailScreenState extends State if (controller.isLoading.value) { return SkeletonLoaders.paymentRequestDetailSkeletonLoader(); } + final request = controller.paymentRequest.value; if (controller.errorMessage.isNotEmpty || request == null) { return Center(child: MyText.bodyMedium("No data to display.")); @@ -125,6 +148,7 @@ class _PaymentRequestDetailScreenState extends State _DetailsTable(request: request), const Divider(height: 30, thickness: 1.2), _Documents(documents: request.attachments), + MySpacing.height(24), ], ), ), @@ -135,15 +159,17 @@ class _PaymentRequestDetailScreenState extends State ); }), ), - bottomNavigationBar: Obx(() { + bottomNavigationBar: _buildBottomActionBar(), + + // ✅ Added Floating Action Button for Edit + floatingActionButton: Obx(() { + if (controller.isLoading.value) return const SizedBox.shrink(); + final request = controller.paymentRequest.value; - if (request == null || - controller.isLoading.value || - employeeInfo == null) { + if (controller.errorMessage.isNotEmpty || request == null) { return const SizedBox.shrink(); } - // Check permissions once if (!_checkedPermission) { _checkedPermission = true; WidgetsBinding.instance.addPostFrameCallback((_) { @@ -151,121 +177,150 @@ class _PaymentRequestDetailScreenState extends State }); } - // Filter statuses - const reimbursementStatusId = '61578360-3a49-4c34-8604-7b35a3787b95'; - const draftStatusId = '6537018f-f4e9-4cb3-a210-6c3b2da999d7'; + final canEdit = PaymentRequestPermissionHelper.canEditPaymentRequest( + employeeInfo, + request, + ); - final availableStatuses = request.nextStatus.where((status) { - if (status.id == draftStatusId) { - return employeeInfo?.id == request.createdBy.id; - } - return permissionController - .hasAnyPermission(status.permissionIds ?? []); - }).toList(); + if (!canEdit) return const SizedBox.shrink(); - // If there are no next statuses, show "Create Expense" button - if (availableStatuses.isEmpty) { - return SafeArea( - child: Container( - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), - decoration: BoxDecoration( - color: Colors.white, - border: Border(top: BorderSide(color: Colors.grey.shade300)), - ), - child: ElevatedButton( - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 14), - backgroundColor: Colors.blue, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - onPressed: () { - showCreateExpenseBottomSheet(); - }, - child: const Text( - "Create Expense", - style: TextStyle( - color: Colors.white, fontWeight: FontWeight.bold), - ), - ), - ), - ); - } - - // Normal status buttons - return SafeArea( - child: Container( - decoration: BoxDecoration( - color: Colors.white, - border: Border(top: BorderSide(color: Colors.grey.shade300)), - ), - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), - child: Wrap( - alignment: WrapAlignment.center, - spacing: 10, - runSpacing: 10, - children: availableStatuses.map((status) { - final color = _parseColor(status.color); - - return ElevatedButton( - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric( - horizontal: 16, vertical: 10), - backgroundColor: color, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - onPressed: () async { - if (status.id == reimbursementStatusId) { - showModalBottomSheet( - context: context, - isScrollControlled: true, - shape: const RoundedRectangleBorder( - borderRadius: - BorderRadius.vertical(top: Radius.circular(5)), - ), - builder: (ctx) => UpdatePaymentRequestWithReimbursement( - expenseId: request.paymentRequestUID, - statusId: status.id, - onClose: () {}, - ), - ); - } else { - final comment = await showCommentBottomSheet( - context, status.displayName); - if (comment == null || comment.trim().isEmpty) return; - - final success = - await controller.updatePaymentRequestStatus( - statusId: status.id, - comment: comment.trim(), - ); - - showAppSnackbar( - title: success ? 'Success' : 'Error', - message: success - ? 'Status updated successfully' - : 'Failed to update status', - type: - success ? SnackbarType.success : SnackbarType.error, - ); - - if (success) await controller.fetchPaymentRequestDetail(); - } - }, - child: Text(status.displayName, - style: const TextStyle(color: Colors.white)), - ); - }).toList(), - ), + return FloatingActionButton.extended( + onPressed: () => _openEditPaymentRequestBottomSheet(request), + backgroundColor: contentTheme.primary, + icon: const Icon(Icons.edit), + label: MyText.bodyMedium( + "Edit Payment Request", + fontWeight: 600, + color: Colors.white, ), ); }), ); } + Widget _buildBottomActionBar() { + return Obx(() { + final request = controller.paymentRequest.value; + if (request == null || + controller.isLoading.value || + employeeInfo == null) { + return const SizedBox.shrink(); + } + + if (!_checkedPermission) { + _checkedPermission = true; + WidgetsBinding.instance.addPostFrameCallback((_) { + _checkPermissionToSubmit(request); + }); + } + + const reimbursementStatusId = '61578360-3a49-4c34-8604-7b35a3787b95'; + const draftStatusId = '6537018f-f4e9-4cb3-a210-6c3b2da999d7'; + + final availableStatuses = request.nextStatus.where((status) { + if (status.id == draftStatusId) { + return employeeInfo?.id == request.createdBy.id; + } + return permissionController + .hasAnyPermission(status.permissionIds ?? []); + }).toList(); + + if (availableStatuses.isEmpty) { + return SafeArea( + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), + decoration: BoxDecoration( + color: Colors.white, + border: Border(top: BorderSide(color: Colors.grey.shade300)), + ), + child: ElevatedButton( + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 14), + backgroundColor: Colors.blue, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + onPressed: () => showCreateExpenseBottomSheet(), + child: const Text( + "Create Expense", + style: + TextStyle(color: Colors.white, fontWeight: FontWeight.bold), + ), + ), + ), + ); + } + + return SafeArea( + child: Container( + decoration: BoxDecoration( + color: Colors.white, + border: Border(top: BorderSide(color: Colors.grey.shade300)), + ), + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), + child: Wrap( + alignment: WrapAlignment.center, + spacing: 10, + runSpacing: 10, + children: availableStatuses.map((status) { + final color = _parseColor(status.color); + + return ElevatedButton( + style: ElevatedButton.styleFrom( + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + backgroundColor: color, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + onPressed: () async { + if (status.id == reimbursementStatusId) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + shape: const RoundedRectangleBorder( + borderRadius: + BorderRadius.vertical(top: Radius.circular(5)), + ), + builder: (ctx) => UpdatePaymentRequestWithReimbursement( + expenseId: request.paymentRequestUID, + statusId: status.id, + onClose: () {}, + ), + ); + } else { + final comment = await showCommentBottomSheet( + context, status.displayName); + if (comment == null || comment.trim().isEmpty) return; + + final success = await controller.updatePaymentRequestStatus( + statusId: status.id, + comment: comment.trim(), + ); + + showAppSnackbar( + title: success ? 'Success' : 'Error', + message: success + ? 'Status updated successfully' + : 'Failed to update status', + type: success ? SnackbarType.success : SnackbarType.error, + ); + + if (success) await controller.fetchPaymentRequestDetail(); + } + }, + child: Text(status.displayName, + style: const TextStyle(color: Colors.white)), + ); + }).toList(), + ), + ), + ); + }); + } + PreferredSizeWidget _buildAppBar() { return PreferredSize( preferredSize: const Size.fromHeight(72), @@ -326,6 +381,29 @@ class _PaymentRequestDetailScreenState extends State } } +class PaymentRequestPermissionHelper { + static bool canEditPaymentRequest( + EmployeeInfo? employee, PaymentRequestData request) { + return employee?.id == request.createdBy.id && + _isInAllowedEditStatus(request.expenseStatus.id); + } + + static bool canSubmitPaymentRequest( + EmployeeInfo? employee, PaymentRequestData request) { + return employee?.id == request.createdBy.id && + request.nextStatus.isNotEmpty; + } + + static bool _isInAllowedEditStatus(String statusId) { + const editableStatusIds = [ + "d1ee5eec-24b6-4364-8673-a8f859c60729", + "965eda62-7907-4963-b4a1-657fb0b2724b", + "297e0d8f-f668-41b5-bfea-e03b354251c8", + ]; + return editableStatusIds.contains(statusId); + } +} + class _Header extends StatelessWidget { final PaymentRequestData request; final Color Function(String) colorParser; @@ -383,124 +461,128 @@ class _Header extends StatelessWidget { } class _Logs extends StatelessWidget { - final List logs; - final Color Function(String) colorParser; - const _Logs({required this.logs, required this.colorParser}); +final List logs; +final Color Function(String) colorParser; +const _Logs({required this.logs, required this.colorParser}); - DateTime _parseTimestamp(DateTime ts) => ts; +DateTime _parseTimestamp(DateTime ts) => ts; - @override - Widget build(BuildContext context) { - if (logs.isEmpty) { - return MyText.bodyMedium('No Timeline', color: Colors.grey); - } - - final reversedLogs = logs.reversed.toList(); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MyText.bodySmall("Timeline:", fontWeight: 600), - ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: reversedLogs.length, - itemBuilder: (_, index) { - final log = reversedLogs[index]; - - final status = log.status.name; - final description = log.status.description; - final comment = log.comment; - final nextStatusName = log.nextStatus.name; - - final updatedBy = log.updatedBy; - final initials = - '${updatedBy.firstName.isNotEmpty == true ? updatedBy.firstName[0] : ''}' - '${updatedBy.lastName.isNotEmpty == true ? updatedBy.lastName[0] : ''}'; - final name = '${updatedBy.firstName} ${updatedBy.lastName}'; - - final timestamp = _parseTimestamp(log.updatedAt); - final timeAgo = timeago.format(timestamp); - - final statusColor = colorParser(log.status.color); - final nextStatusColor = colorParser(log.nextStatus.color); - - return TimelineTile( - alignment: TimelineAlign.start, - isFirst: index == 0, - isLast: index == reversedLogs.length - 1, - indicatorStyle: IndicatorStyle( - width: 16, - height: 16, - indicator: Container( - decoration: - BoxDecoration(shape: BoxShape.circle, color: statusColor), - ), - ), - beforeLineStyle: - LineStyle(color: Colors.grey.shade300, thickness: 2), - endChild: Padding( - padding: const EdgeInsets.all(12.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - MyText.bodyMedium(status, - fontWeight: 600, color: statusColor), - MyText.bodySmall(timeAgo, - color: Colors.grey[600], - textAlign: TextAlign.right), - ], - ), - if (description.isNotEmpty) ...[ - const SizedBox(height: 4), - MyText.bodySmall(description, color: Colors.grey[800]), - ], - if (comment.isNotEmpty) ...[ - const SizedBox(height: 8), - MyText.bodyMedium(comment, fontWeight: 500), - ], - const SizedBox(height: 8), - Row( - children: [ - Container( - padding: const EdgeInsets.symmetric( - horizontal: 6, vertical: 2), - decoration: BoxDecoration( - color: Colors.grey.shade300, - borderRadius: BorderRadius.circular(4), - ), - child: MyText.bodySmall(initials, fontWeight: 600), - ), - const SizedBox(width: 6), - Expanded( - child: MyText.bodySmall(name, - overflow: TextOverflow.ellipsis)), - if (nextStatusName.isNotEmpty) - Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: nextStatusColor.withOpacity(0.15), - borderRadius: BorderRadius.circular(4), - ), - child: MyText.bodySmall(nextStatusName, - fontWeight: 600, color: nextStatusColor), - ), - ], - ), - ], - ), - ), - ); - }, - ) - ], - ); - } +@override +Widget build(BuildContext context) { +if (logs.isEmpty) { +return MyText.bodyMedium('No Timeline', color: Colors.grey); } +final reversedLogs = logs.reversed.toList(); +return Column( +crossAxisAlignment: CrossAxisAlignment.start, +children: [ +MyText.bodySmall("Timeline:", fontWeight: 600), +ListView.builder( +shrinkWrap: true, +physics: const NeverScrollableScrollPhysics(), +itemCount: reversedLogs.length, +itemBuilder: (_, index) { +final log = reversedLogs[index]; + +final status = log.status?.name ?? 'Unknown'; +final description = log.status?.description ?? ''; +final statusColor = log.status != null +? colorParser(log.status!.color) +: Colors.grey; + +final comment = log.comment; +final nextStatusName = log.nextStatus.name; + +final updatedBy = log.updatedBy; +final initials = +'${updatedBy.firstName.isNotEmpty == true ? updatedBy.firstName[0] : ''}' +'${updatedBy.lastName.isNotEmpty == true ? updatedBy.lastName[0] : ''}'; +final name = '${updatedBy.firstName} ${updatedBy.lastName}'; + +final timestamp = _parseTimestamp(log.updatedAt); +final timeAgo = timeago.format(timestamp); + +final nextStatusColor = colorParser(log.nextStatus.color); + +return TimelineTile( +alignment: TimelineAlign.start, +isFirst: index == 0, +isLast: index == reversedLogs.length - 1, +indicatorStyle: IndicatorStyle( +width: 16, +height: 16, +indicator: Container( +decoration: +BoxDecoration(shape: BoxShape.circle, color: statusColor), +), +), +beforeLineStyle: +LineStyle(color: Colors.grey.shade300, thickness: 2), +endChild: Padding( +padding: const EdgeInsets.all(12.0), +child: Column( +crossAxisAlignment: CrossAxisAlignment.start, +children: [ +Row( +mainAxisAlignment: MainAxisAlignment.spaceBetween, +children: [ +MyText.bodyMedium(status, +fontWeight: 600, color: statusColor), +MyText.bodySmall(timeAgo, +color: Colors.grey[600], +textAlign: TextAlign.right), +], +), +if (description.isNotEmpty) ...[ +const SizedBox(height: 4), +MyText.bodySmall(description, color: Colors.grey[800]), +], +if (comment.isNotEmpty) ...[ +const SizedBox(height: 8), +MyText.bodyMedium(comment, fontWeight: 500), +], +const SizedBox(height: 8), +Row( +children: [ +Container( +padding: const EdgeInsets.symmetric( +horizontal: 6, vertical: 2), +decoration: BoxDecoration( +color: Colors.grey.shade300, +borderRadius: BorderRadius.circular(4), +), +child: MyText.bodySmall(initials, fontWeight: 600), +), +const SizedBox(width: 6), +Expanded( +child: MyText.bodySmall(name, +overflow: TextOverflow.ellipsis)), +if (nextStatusName.isNotEmpty) +Container( +padding: const EdgeInsets.symmetric( +horizontal: 8, vertical: 4), +decoration: BoxDecoration( +color: nextStatusColor.withOpacity(0.15), +borderRadius: BorderRadius.circular(4), +), +child: MyText.bodySmall(nextStatusName, +fontWeight: 600, color: nextStatusColor), +), +], +), +], +), +), +); +}, +) +], +); +} +} + + class _Parties extends StatelessWidget { final PaymentRequestData request; const _Parties({required this.request}); diff --git a/lib/view/layouts/user_profile_right_bar.dart b/lib/view/layouts/user_profile_right_bar.dart index b55a2ef..d0d7d77 100644 --- a/lib/view/layouts/user_profile_right_bar.dart +++ b/lib/view/layouts/user_profile_right_bar.dart @@ -215,7 +215,7 @@ class _UserProfileBarState extends State ), SizedBox(height: spacingHeight), _menuItemRow( - icon: LucideIcons.badge_help, + icon: LucideIcons.badge_alert, label: 'FAQ', onTap: () { Get.to(() => FAQScreen());