feat: Add camera functionality to expense attachment; update logger configuration for improved performance
This commit is contained in:
parent
61acbb019b
commit
fd7f108a20
@ -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,66 +370,75 @@ 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
|
||||||
|
.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
|
// --- New Attachments Payload (always include if attachments exist) ---
|
||||||
final existingAttachmentPayloads = attachmentsChanged
|
final List<Map<String, dynamic>> newAttachmentPayloads =
|
||||||
? existingAttachments.map((e) => {
|
attachments.isNotEmpty
|
||||||
"documentId": e['documentId'],
|
? await Future.wait(attachments.map((file) async {
|
||||||
"fileName": e['fileName'],
|
final bytes = await file.readAsBytes();
|
||||||
"contentType": e['contentType'],
|
final length = await file.length();
|
||||||
"fileSize": 0,
|
return <String, dynamic>{
|
||||||
"description": "",
|
"fileName": file.path.split('/').last,
|
||||||
"url": e['url'],
|
"base64Data": base64Encode(bytes),
|
||||||
"isActive": e['isActive'] ?? true,
|
"contentType":
|
||||||
// If attachment removed, base64Data should be empty array
|
lookupMimeType(file.path) ?? 'application/octet-stream',
|
||||||
"base64Data": e['isActive'] == false ? "" : e['base64Data'],
|
"fileSize": length,
|
||||||
}).toList()
|
"description": "",
|
||||||
: [];
|
};
|
||||||
|
}))
|
||||||
|
: <Map<String, dynamic>>[];
|
||||||
|
|
||||||
// New attachments payload
|
// --- Selected Expense Type ---
|
||||||
final newAttachmentPayloads = attachmentsChanged
|
final type = selectedExpenseType.value!;
|
||||||
? 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": "",
|
|
||||||
};
|
|
||||||
}))
|
|
||||||
: [];
|
|
||||||
|
|
||||||
final type = selectedExpenseType.value!;
|
// --- Combine all attachments ---
|
||||||
|
final List<Map<String, dynamic>> combinedAttachments = [
|
||||||
|
...existingAttachmentPayloads,
|
||||||
|
...newAttachmentPayloads
|
||||||
|
];
|
||||||
|
|
||||||
return {
|
// --- Build Payload ---
|
||||||
if (isEditMode.value && editingExpenseId != null) "id": editingExpenseId,
|
final payload = <String, dynamic>{
|
||||||
"projectId": projectsMap[selectedProject.value]!,
|
if (isEditMode.value && editingExpenseId != null) "id": editingExpenseId,
|
||||||
"expensesTypeId": type.id,
|
"projectId": projectsMap[selectedProject.value]!,
|
||||||
"paymentModeId": selectedPaymentMode.value!.id,
|
"expensesTypeId": type.id,
|
||||||
"paidById": selectedPaidBy.value!.id,
|
"paymentModeId": selectedPaymentMode.value!.id,
|
||||||
"transactionDate": (selectedTransactionDate.value?.toUtc() ?? now.toUtc())
|
"paidById": selectedPaidBy.value!.id,
|
||||||
.toIso8601String(),
|
"transactionDate": (selectedTransactionDate.value?.toUtc() ?? now.toUtc())
|
||||||
"transactionId": transactionIdController.text,
|
.toIso8601String(),
|
||||||
"description": descriptionController.text,
|
"transactionId": transactionIdController.text,
|
||||||
"location": locationController.text,
|
"description": descriptionController.text,
|
||||||
"supplerName": supplierController.text,
|
"location": locationController.text,
|
||||||
"amount": double.parse(amountController.text.trim()),
|
"supplerName": supplierController.text,
|
||||||
"noOfPersons": type.noOfPersonsRequired == true
|
"amount": double.parse(amountController.text.trim()),
|
||||||
? int.tryParse(noOfPersonsController.text.trim()) ?? 0
|
"noOfPersons": type.noOfPersonsRequired == true
|
||||||
: 0,
|
? int.tryParse(noOfPersonsController.text.trim()) ?? 0
|
||||||
// Attachments logic
|
: 0,
|
||||||
"billAttachments": isEditMode.value && !attachmentsChanged
|
"billAttachments":
|
||||||
? null
|
combinedAttachments.isEmpty ? null : combinedAttachments,
|
||||||
: [...existingAttachmentPayloads, ...newAttachmentPayloads],
|
};
|
||||||
};
|
|
||||||
}
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
String validateForm() {
|
String validateForm() {
|
||||||
final missing = <String>[];
|
final missing = <String>[];
|
||||||
|
@ -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 = {
|
||||||
|
@ -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),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
Loading…
x
Reference in New Issue
Block a user