feat: Add camera functionality to expense attachment; update logger configuration for improved performance

This commit is contained in:
Vaibhav Surve 2025-09-12 15:38:42 +05:30
parent 61acbb019b
commit fd7f108a20
3 changed files with 100 additions and 60 deletions

View File

@ -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<ExpenseController>();
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<void> 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<void> submitOrUpdateExpense() async {
if (isSubmitting.value) return;
@ -358,66 +370,75 @@ class AddExpenseController extends GetxController {
}
}
Future<Map<String, dynamic>> _buildExpensePayload() async {
final now = DateTime.now();
Future<Map<String, dynamic>> _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<Map<String, dynamic>> existingAttachmentPayloads =
isEditMode.value
? existingAttachments
.map<Map<String, dynamic>>((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()
: <Map<String, dynamic>>[];
// 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<Map<String, dynamic>> newAttachmentPayloads =
attachments.isNotEmpty
? await Future.wait(attachments.map((file) async {
final bytes = await file.readAsBytes();
final length = await file.length();
return <String, dynamic>{
"fileName": file.path.split('/').last,
"base64Data": base64Encode(bytes),
"contentType":
lookupMimeType(file.path) ?? 'application/octet-stream',
"fileSize": length,
"description": "",
};
}))
: <Map<String, dynamic>>[];
// 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<Map<String, dynamic>> 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 = <String, dynamic>{
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 = <String>[];

View File

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

View File

@ -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<AddExpenseController>().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),
),
),
],