corrected the attachment issue
This commit is contained in:
parent
fd861b3adb
commit
fd57686c8a
@ -156,6 +156,28 @@ class AddPaymentRequestController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> pickFromCamera() async {
|
||||
try {
|
||||
final pickedFile = await _picker.pickImage(source: ImageSource.camera);
|
||||
if (pickedFile != null) {
|
||||
isProcessingAttachment.value = true;
|
||||
File imageFile = File(pickedFile.path);
|
||||
|
||||
// Add timestamp to the captured image
|
||||
File timestampedFile = await TimestampImageHelper.addTimestamp(
|
||||
imageFile: imageFile,
|
||||
);
|
||||
|
||||
attachments.add(timestampedFile);
|
||||
attachments.refresh(); // refresh UI
|
||||
}
|
||||
} catch (e) {
|
||||
_errorSnackbar("Camera error: $e");
|
||||
} finally {
|
||||
isProcessingAttachment.value = false; // stop loading
|
||||
}
|
||||
}
|
||||
|
||||
/// Selection handlers
|
||||
void selectProject(Map<String, dynamic> project) =>
|
||||
selectedProject.value = project;
|
||||
|
||||
@ -479,7 +479,6 @@ class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet>
|
||||
message: 'Attachment has been removed.',
|
||||
type: SnackbarType.success,
|
||||
);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@ -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<T?> showPaymentRequestBottomSheet<T>({bool isEdit = false}) {
|
||||
Future<T?> showPaymentRequestBottomSheet<T>({
|
||||
bool isEdit = false,
|
||||
Map<String, dynamic>? existingData,
|
||||
VoidCallback? onUpdated,
|
||||
}) {
|
||||
return Get.bottomSheet<T>(
|
||||
_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<String, dynamic>? 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<String, dynamic>>()
|
||||
.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,
|
||||
);
|
||||
}
|
||||
@ -360,7 +434,6 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet>
|
||||
title: 'Removed',
|
||||
message: 'Attachment has been removed.',
|
||||
type: SnackbarType.success);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
);
|
||||
@ -425,12 +498,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;
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
@ -53,17 +54,7 @@ class _PaymentRequestDetailScreenState extends State<PaymentRequestDetailScreen>
|
||||
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<void> _loadEmployeeInfo() async {
|
||||
@ -77,6 +68,38 @@ class _PaymentRequestDetailScreenState extends State<PaymentRequestDetailScreen>
|
||||
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(
|
||||
@ -125,6 +148,7 @@ class _PaymentRequestDetailScreenState extends State<PaymentRequestDetailScreen>
|
||||
_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<PaymentRequestDetailScreen>
|
||||
);
|
||||
}),
|
||||
),
|
||||
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,101 +177,134 @@ class _PaymentRequestDetailScreenState extends State<PaymentRequestDetailScreen>
|
||||
});
|
||||
}
|
||||
|
||||
// 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();
|
||||
|
||||
// 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 is reimbursement, show reimbursement bottom sheet
|
||||
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: () {},
|
||||
),
|
||||
);
|
||||
|
||||
// If status is b8586f67-dc19-49c3-b4af-224149efe1d3, open create expense
|
||||
} else if (status.id ==
|
||||
'b8586f67-dc19-49c3-b4af-224149efe1d3') {
|
||||
showCreateExpenseBottomSheet(
|
||||
statusId: status.id,
|
||||
);
|
||||
|
||||
// Normal status flow
|
||||
} 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();
|
||||
|
||||
// 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 is reimbursement, show reimbursement bottom sheet
|
||||
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: () {},
|
||||
),
|
||||
);
|
||||
|
||||
// If status is b8586f67-dc19-49c3-b4af-224149efe1d3, open create expense
|
||||
} else if (status.id ==
|
||||
'b8586f67-dc19-49c3-b4af-224149efe1d3') {
|
||||
showCreateExpenseBottomSheet(
|
||||
statusId: status.id,
|
||||
);
|
||||
|
||||
// Normal status flow
|
||||
} 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),
|
||||
@ -306,6 +365,29 @@ class _PaymentRequestDetailScreenState extends State<PaymentRequestDetailScreen>
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user