handelled the update expense

This commit is contained in:
Vaibhav Surve 2025-08-06 11:46:12 +05:30
parent aa76ec60cb
commit 63e5caae24
2 changed files with 171 additions and 149 deletions

View File

@ -170,8 +170,14 @@ class AddExpenseController extends GetxController {
// --- Existing Attachments --- // --- Existing Attachments ---
existingAttachments.clear(); existingAttachments.clear();
if (data['attachments'] != null && data['attachments'] is List) { if (data['attachments'] != null && data['attachments'] is List) {
existingAttachments existingAttachments.addAll(
.addAll(List<Map<String, dynamic>>.from(data['attachments'])); List<Map<String, dynamic>>.from(data['attachments']).map((e) {
return {
...e,
'isActive': true, // default
};
}),
);
} }
_logPrefilledData(); _logPrefilledData();
@ -369,15 +375,20 @@ class AddExpenseController extends GetxController {
final projectId = projectsMap[selectedProject.value]!; final projectId = projectsMap[selectedProject.value]!;
final selectedDate = final selectedDate =
selectedTransactionDate.value?.toUtc() ?? DateTime.now().toUtc(); selectedTransactionDate.value?.toUtc() ?? DateTime.now().toUtc();
final existingAttachmentPayloads = existingAttachments final existingAttachmentPayloads = existingAttachments.map((e) {
.map((e) => { final isActive = e['isActive'] ?? true;
"fileName": e['fileName'],
"contentType": e['contentType'], return {
"fileSize": 0, // optional or populate if known "documentId": e['documentId'],
"description": "", "fileName": e['fileName'],
"url": e['url'], // custom field if your backend accepts "contentType": e['contentType'],
}) "fileSize": 0,
.toList(); "description": "",
"url": e['url'],
"isActive": isActive,
"base64Data": isActive ? e['base64Data'] : null,
};
}).toList();
final newAttachmentPayloads = final newAttachmentPayloads =
await Future.wait(attachments.map((file) async { await Future.wait(attachments.map((file) async {

View File

@ -41,22 +41,20 @@ class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet> {
final AddExpenseController controller = Get.put(AddExpenseController()); final AddExpenseController controller = Get.put(AddExpenseController());
void _showEmployeeList() async { void _showEmployeeList() async {
await showModalBottomSheet( await showModalBottomSheet(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
shape: const RoundedRectangleBorder(
shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ),
), backgroundColor: Colors.transparent,
backgroundColor: Colors.transparent, builder: (_) => EmployeeSelectorBottomSheet(),
builder: (_) => EmployeeSelectorBottomSheet(), );
);
// Optional cleanup
controller.employeeSearchController.clear();
controller.employeeSearchResults.clear();
}
// Optional cleanup
controller.employeeSearchController.clear();
controller.employeeSearchResults.clear();
}
Future<void> _showOptionList<T>( Future<void> _showOptionList<T>(
List<T> options, List<T> options,
@ -277,8 +275,13 @@ class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet> {
attachments: controller.attachments, attachments: controller.attachments,
existingAttachments: controller.existingAttachments, existingAttachments: controller.existingAttachments,
onRemoveNew: controller.removeAttachment, onRemoveNew: controller.removeAttachment,
onRemoveExisting: (item) => onRemoveExisting: (item) {
controller.existingAttachments.remove(item), final index = controller.existingAttachments.indexOf(item);
if (index != -1) {
controller.existingAttachments[index]['isActive'] = false;
controller.existingAttachments.refresh();
}
},
onAdd: controller.pickAttachments, onAdd: controller.pickAttachments,
), ),
MySpacing.height(16), MySpacing.height(16),
@ -458,7 +461,7 @@ class _TileContainer extends StatelessWidget {
class _AttachmentsSection extends StatelessWidget { class _AttachmentsSection extends StatelessWidget {
final RxList<File> attachments; final RxList<File> attachments;
final List<Map<String, dynamic>> existingAttachments; final RxList<Map<String, dynamic>> existingAttachments;
final ValueChanged<File> onRemoveNew; final ValueChanged<File> onRemoveNew;
final ValueChanged<Map<String, dynamic>>? onRemoveExisting; final ValueChanged<Map<String, dynamic>>? onRemoveExisting;
final VoidCallback onAdd; final VoidCallback onAdd;
@ -473,133 +476,141 @@ class _AttachmentsSection extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Obx(() => Column( return Obx(() {
crossAxisAlignment: CrossAxisAlignment.start, final activeExistingAttachments =
children: [ existingAttachments.where((doc) => doc['isActive'] != false).toList();
if (existingAttachments.isNotEmpty) ...[
Text(
"Existing Attachments",
style: const TextStyle(fontWeight: FontWeight.w600),
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: existingAttachments.map((doc) {
final isImage =
doc['contentType']?.toString().startsWith('image/') ??
false;
final url = doc['url'];
final fileName = doc['fileName'] ?? 'Unnamed';
return Stack( return Column(
clipBehavior: Clip.none, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
GestureDetector( if (activeExistingAttachments.isNotEmpty) ...[
onTap: () async { Text(
if (isImage) { "Existing Attachments",
final imageDocs = existingAttachments style: const TextStyle(fontWeight: FontWeight.w600),
.where((d) => (d['contentType'] ),
?.toString() const SizedBox(height: 8),
.startsWith('image/') ??
false))
.toList();
final initialIndex =
imageDocs.indexWhere((d) => d == doc);
showDialog(
context: context,
builder: (_) => ImageViewerDialog(
imageSources:
imageDocs.map((e) => e['url']).toList(),
initialIndex: initialIndex,
),
);
} else {
if (url != null && await canLaunchUrlString(url)) {
await launchUrlString(url,
mode: LaunchMode.externalApplication);
} else {
showAppSnackbar(
title: 'Error',
message: 'Could not open the document.',
type: SnackbarType.error,
);
}
}
},
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 8),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(6),
color: Colors.grey.shade100,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
isImage ? Icons.image : Icons.insert_drive_file,
size: 20,
color: Colors.grey[600],
),
const SizedBox(width: 7),
ConstrainedBox(
constraints:
const BoxConstraints(maxWidth: 120),
child: Text(
fileName,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 12),
),
),
],
),
),
),
if (onRemoveExisting != null)
Positioned(
top: -6,
right: -6,
child: IconButton(
icon: const Icon(Icons.close,
color: Colors.red, size: 18),
onPressed: () => onRemoveExisting!(doc),
),
),
],
);
}).toList(),
),
const SizedBox(height: 16),
],
// New attachments section - shows preview tiles
Wrap( Wrap(
spacing: 8, spacing: 8,
runSpacing: 8, runSpacing: 8,
children: [ children: activeExistingAttachments.map((doc) {
...attachments.map((file) => _AttachmentTile( final isImage =
file: file, doc['contentType']?.toString().startsWith('image/') ??
onRemove: () => onRemoveNew(file), false;
)), final url = doc['url'];
GestureDetector( final fileName = doc['fileName'] ?? 'Unnamed';
onTap: onAdd,
child: Container( return Stack(
width: 80, clipBehavior: Clip.none,
height: 80, children: [
decoration: BoxDecoration( GestureDetector(
border: Border.all(color: Colors.grey.shade400), onTap: () async {
borderRadius: BorderRadius.circular(8), if (isImage) {
color: Colors.grey.shade100, final imageDocs = activeExistingAttachments
.where((d) => (d['contentType']
?.toString()
.startsWith('image/') ??
false))
.toList();
final initialIndex =
imageDocs.indexWhere((d) => d == doc);
showDialog(
context: context,
builder: (_) => ImageViewerDialog(
imageSources:
imageDocs.map((e) => e['url']).toList(),
initialIndex: initialIndex,
),
);
} else {
if (url != null && await canLaunchUrlString(url)) {
await launchUrlString(
url,
mode: LaunchMode.externalApplication,
);
} else {
showAppSnackbar(
title: 'Error',
message: 'Could not open the document.',
type: SnackbarType.error,
);
}
}
},
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 8),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(6),
color: Colors.grey.shade100,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
isImage ? Icons.image : Icons.insert_drive_file,
size: 20,
color: Colors.grey[600],
),
const SizedBox(width: 7),
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 120),
child: Text(
fileName,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 12),
),
),
],
),
),
), ),
child: const Icon(Icons.add, size: 30, color: Colors.grey), if (onRemoveExisting != null)
), Positioned(
), top: -6,
], right: -6,
child: IconButton(
icon: const Icon(Icons.close,
color: Colors.red, size: 18),
onPressed: () {
onRemoveExisting?.call(doc);
},
),
),
],
);
}).toList(),
), ),
const SizedBox(height: 16),
], ],
));
// New attachments section
Wrap(
spacing: 8,
runSpacing: 8,
children: [
...attachments.map((file) => _AttachmentTile(
file: file,
onRemove: () => onRemoveNew(file),
)),
GestureDetector(
onTap: onAdd,
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.add, size: 30, color: Colors.grey),
),
),
],
),
],
);
});
} }
} }