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/expense_type_model.dart';
import 'package:marco/model/expense/payment_types_model.dart'; import 'package:marco/model/expense/payment_types_model.dart';
import 'package:mime/mime.dart'; import 'package:mime/mime.dart';
import 'package:image_picker/image_picker.dart';
class AddExpenseController extends GetxController { class AddExpenseController extends GetxController {
// --- Text Controllers --- // --- Text Controllers ---
@ -57,7 +58,7 @@ class AddExpenseController extends GetxController {
String? editingExpenseId; String? editingExpenseId;
final expenseController = Get.find<ExpenseController>(); final expenseController = Get.find<ExpenseController>();
final ImagePicker _picker = ImagePicker();
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
@ -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 --- // --- Submission ---
Future<void> submitOrUpdateExpense() async { Future<void> submitOrUpdateExpense() async {
if (isSubmitting.value) return; if (isSubmitting.value) return;
@ -358,16 +370,14 @@ class AddExpenseController extends GetxController {
} }
} }
Future<Map<String, dynamic>> _buildExpensePayload() async { Future<Map<String, dynamic>> _buildExpensePayload() async {
final now = DateTime.now(); final now = DateTime.now();
// Determine if attachments were changed // --- Existing Attachments Payload (for edit mode only) ---
bool attachmentsChanged = final List<Map<String, dynamic>> existingAttachmentPayloads =
attachments.isNotEmpty || existingAttachments.any((e) => e['isActive'] == false); isEditMode.value
? existingAttachments
// Existing attachments payload .map<Map<String, dynamic>>((e) => {
final existingAttachmentPayloads = attachmentsChanged
? existingAttachments.map((e) => {
"documentId": e['documentId'], "documentId": e['documentId'],
"fileName": e['fileName'], "fileName": e['fileName'],
"contentType": e['contentType'], "contentType": e['contentType'],
@ -375,28 +385,39 @@ Future<Map<String, dynamic>> _buildExpensePayload() async {
"description": "", "description": "",
"url": e['url'], "url": e['url'],
"isActive": e['isActive'] ?? true, "isActive": e['isActive'] ?? true,
// If attachment removed, base64Data should be empty array "base64Data": "", // <-- always empty now
"base64Data": e['isActive'] == false ? "" : e['base64Data'], })
}).toList() .toList()
: []; : <Map<String, dynamic>>[];
// New attachments payload // --- New Attachments Payload (always include if attachments exist) ---
final newAttachmentPayloads = attachmentsChanged final List<Map<String, dynamic>> newAttachmentPayloads =
attachments.isNotEmpty
? await Future.wait(attachments.map((file) async { ? await Future.wait(attachments.map((file) async {
final bytes = await file.readAsBytes(); final bytes = await file.readAsBytes();
return { final length = await file.length();
return <String, dynamic>{
"fileName": file.path.split('/').last, "fileName": file.path.split('/').last,
"base64Data": base64Encode(bytes), "base64Data": base64Encode(bytes),
"contentType": lookupMimeType(file.path) ?? 'application/octet-stream', "contentType":
"fileSize": await file.length(), lookupMimeType(file.path) ?? 'application/octet-stream',
"fileSize": length,
"description": "", "description": "",
}; };
})) }))
: []; : <Map<String, dynamic>>[];
// --- Selected Expense Type ---
final type = selectedExpenseType.value!; final type = selectedExpenseType.value!;
return { // --- Combine all attachments ---
final List<Map<String, dynamic>> combinedAttachments = [
...existingAttachmentPayloads,
...newAttachmentPayloads
];
// --- Build Payload ---
final payload = <String, dynamic>{
if (isEditMode.value && editingExpenseId != null) "id": editingExpenseId, if (isEditMode.value && editingExpenseId != null) "id": editingExpenseId,
"projectId": projectsMap[selectedProject.value]!, "projectId": projectsMap[selectedProject.value]!,
"expensesTypeId": type.id, "expensesTypeId": type.id,
@ -412,12 +433,12 @@ Future<Map<String, dynamic>> _buildExpensePayload() async {
"noOfPersons": type.noOfPersonsRequired == true "noOfPersons": type.noOfPersonsRequired == true
? int.tryParse(noOfPersonsController.text.trim()) ?? 0 ? int.tryParse(noOfPersonsController.text.trim()) ?? 0
: 0, : 0,
// Attachments logic "billAttachments":
"billAttachments": isEditMode.value && !attachmentsChanged combinedAttachments.isEmpty ? null : combinedAttachments,
? null
: [...existingAttachmentPayloads, ...newAttachmentPayloads],
}; };
}
return payload;
}
String validateForm() { String validateForm() {
final missing = <String>[]; final missing = <String>[];

View File

@ -18,10 +18,10 @@ bool _isPosting = false;
bool _canPostLogs = false; bool _canPostLogs = false;
/// Maximum number of logs before triggering API post /// Maximum number of logs before triggering API post
const int _maxLogsBeforePost = 50; const int _maxLogsBeforePost = 100;
/// Maximum logs in memory buffer /// Maximum logs in memory buffer
const int _maxBufferSize = 50; const int _maxBufferSize = 500;
/// Enum logger level mapping /// Enum logger level mapping
const _levelMap = { const _levelMap = {

View File

@ -791,6 +791,8 @@ class _AttachmentsSection extends StatelessWidget {
), ),
); );
}), }),
// 📎 File Picker Button
GestureDetector( GestureDetector(
onTap: onAdd, onTap: onAdd,
child: Container( child: Container(
@ -801,7 +803,24 @@ class _AttachmentsSection extends StatelessWidget {
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
color: Colors.grey.shade100, 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),
), ),
), ),
], ],