diff --git a/lib/controller/expense/add_expense_controller.dart b/lib/controller/expense/add_expense_controller.dart index e2b09e0..9f3727d 100644 --- a/lib/controller/expense/add_expense_controller.dart +++ b/lib/controller/expense/add_expense_controller.dart @@ -16,6 +16,7 @@ import 'package:marco/model/employees/employee_model.dart'; import 'package:marco/model/expense/expense_type_model.dart'; import 'package:marco/model/expense/payment_types_model.dart'; import 'package:mime/mime.dart'; +import 'package:image_picker/image_picker.dart'; class AddExpenseController extends GetxController { // --- Text Controllers --- @@ -57,7 +58,7 @@ class AddExpenseController extends GetxController { String? editingExpenseId; final expenseController = Get.find(); - + final ImagePicker _picker = ImagePicker(); @override void onInit() { super.onInit(); @@ -189,7 +190,7 @@ class AddExpenseController extends GetxController { ); if (pickedDate != null) { - final now = DateTime.now(); + final now = DateTime.now(); final finalDateTime = DateTime( pickedDate.year, pickedDate.month, @@ -308,6 +309,17 @@ class AddExpenseController extends GetxController { } } + Future pickFromCamera() async { + try { + final pickedFile = await _picker.pickImage(source: ImageSource.camera); + if (pickedFile != null) { + attachments.add(File(pickedFile.path)); + } + } catch (e) { + _errorSnackbar("Camera error: $e"); + } + } + // --- Submission --- Future submitOrUpdateExpense() async { if (isSubmitting.value) return; @@ -358,66 +370,75 @@ class AddExpenseController extends GetxController { } } -Future> _buildExpensePayload() async { - final now = DateTime.now(); + Future> _buildExpensePayload() async { + final now = DateTime.now(); - // Determine if attachments were changed - bool attachmentsChanged = - attachments.isNotEmpty || existingAttachments.any((e) => e['isActive'] == false); + // --- Existing Attachments Payload (for edit mode only) --- + final List> existingAttachmentPayloads = + isEditMode.value + ? existingAttachments + .map>((e) => { + "documentId": e['documentId'], + "fileName": e['fileName'], + "contentType": e['contentType'], + "fileSize": 0, + "description": "", + "url": e['url'], + "isActive": e['isActive'] ?? true, + "base64Data": "", // <-- always empty now + }) + .toList() + : >[]; - // Existing attachments payload - final existingAttachmentPayloads = attachmentsChanged - ? existingAttachments.map((e) => { - "documentId": e['documentId'], - "fileName": e['fileName'], - "contentType": e['contentType'], - "fileSize": 0, - "description": "", - "url": e['url'], - "isActive": e['isActive'] ?? true, - // If attachment removed, base64Data should be empty array - "base64Data": e['isActive'] == false ? "" : e['base64Data'], - }).toList() - : []; + // --- New Attachments Payload (always include if attachments exist) --- + final List> newAttachmentPayloads = + attachments.isNotEmpty + ? await Future.wait(attachments.map((file) async { + final bytes = await file.readAsBytes(); + final length = await file.length(); + return { + "fileName": file.path.split('/').last, + "base64Data": base64Encode(bytes), + "contentType": + lookupMimeType(file.path) ?? 'application/octet-stream', + "fileSize": length, + "description": "", + }; + })) + : >[]; - // New attachments payload - final newAttachmentPayloads = attachmentsChanged - ? await Future.wait(attachments.map((file) async { - final bytes = await file.readAsBytes(); - return { - "fileName": file.path.split('/').last, - "base64Data": base64Encode(bytes), - "contentType": lookupMimeType(file.path) ?? 'application/octet-stream', - "fileSize": await file.length(), - "description": "", - }; - })) - : []; + // --- Selected Expense Type --- + final type = selectedExpenseType.value!; - final type = selectedExpenseType.value!; + // --- Combine all attachments --- + final List> combinedAttachments = [ + ...existingAttachmentPayloads, + ...newAttachmentPayloads + ]; - return { - if (isEditMode.value && editingExpenseId != null) "id": editingExpenseId, - "projectId": projectsMap[selectedProject.value]!, - "expensesTypeId": type.id, - "paymentModeId": selectedPaymentMode.value!.id, - "paidById": selectedPaidBy.value!.id, - "transactionDate": (selectedTransactionDate.value?.toUtc() ?? now.toUtc()) - .toIso8601String(), - "transactionId": transactionIdController.text, - "description": descriptionController.text, - "location": locationController.text, - "supplerName": supplierController.text, - "amount": double.parse(amountController.text.trim()), - "noOfPersons": type.noOfPersonsRequired == true - ? int.tryParse(noOfPersonsController.text.trim()) ?? 0 - : 0, - // Attachments logic - "billAttachments": isEditMode.value && !attachmentsChanged - ? null - : [...existingAttachmentPayloads, ...newAttachmentPayloads], - }; -} + // --- Build Payload --- + final payload = { + if (isEditMode.value && editingExpenseId != null) "id": editingExpenseId, + "projectId": projectsMap[selectedProject.value]!, + "expensesTypeId": type.id, + "paymentModeId": selectedPaymentMode.value!.id, + "paidById": selectedPaidBy.value!.id, + "transactionDate": (selectedTransactionDate.value?.toUtc() ?? now.toUtc()) + .toIso8601String(), + "transactionId": transactionIdController.text, + "description": descriptionController.text, + "location": locationController.text, + "supplerName": supplierController.text, + "amount": double.parse(amountController.text.trim()), + "noOfPersons": type.noOfPersonsRequired == true + ? int.tryParse(noOfPersonsController.text.trim()) ?? 0 + : 0, + "billAttachments": + combinedAttachments.isEmpty ? null : combinedAttachments, + }; + + return payload; + } String validateForm() { final missing = []; diff --git a/lib/helpers/services/app_logger.dart b/lib/helpers/services/app_logger.dart index 9e855e9..1e631ea 100644 --- a/lib/helpers/services/app_logger.dart +++ b/lib/helpers/services/app_logger.dart @@ -18,10 +18,10 @@ bool _isPosting = false; bool _canPostLogs = false; /// Maximum number of logs before triggering API post -const int _maxLogsBeforePost = 50; +const int _maxLogsBeforePost = 100; /// Maximum logs in memory buffer -const int _maxBufferSize = 50; +const int _maxBufferSize = 500; /// Enum → logger level mapping const _levelMap = { diff --git a/lib/model/expense/add_expense_bottom_sheet.dart b/lib/model/expense/add_expense_bottom_sheet.dart index 03f7c4f..49713c3 100644 --- a/lib/model/expense/add_expense_bottom_sheet.dart +++ b/lib/model/expense/add_expense_bottom_sheet.dart @@ -791,6 +791,8 @@ class _AttachmentsSection extends StatelessWidget { ), ); }), + + // 📎 File Picker Button GestureDetector( onTap: onAdd, child: Container( @@ -801,7 +803,24 @@ class _AttachmentsSection extends StatelessWidget { borderRadius: BorderRadius.circular(8), color: Colors.grey.shade100, ), - child: const Icon(Icons.add, size: 30, color: Colors.grey), + child: const Icon(Icons.attach_file, + size: 30, color: Colors.grey), + ), + ), + + // 📷 Camera Button + GestureDetector( + onTap: () => Get.find().pickFromCamera(), + child: Container( + width: 80, + height: 80, + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade400), + borderRadius: BorderRadius.circular(8), + color: Colors.grey.shade100, + ), + child: const Icon(Icons.camera_alt, + size: 30, color: Colors.grey), ), ), ],