import 'dart:convert'; import 'package:flutter/material.dart'; import 'widgets/attachment_gallery_screen.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lucide_icons/lucide_icons.dart'; import 'package:intl/intl.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart' as flutter_secure_storage; import 'package:image_picker/image_picker.dart'; import 'package:flutter_image_compress/flutter_image_compress.dart'; import '../providers/providers.dart'; import '../data/models.dart'; import '../../../core/utils/snackbar_service.dart'; import '../../../core/services/ocr_service.dart'; import '../../../core/services/notification_service.dart'; class AddTransactionScreen extends ConsumerStatefulWidget { final Transaction? transaction; const AddTransactionScreen({super.key, this.transaction}); @override ConsumerState createState() => _AddTransactionScreenState(); } class _AddTransactionScreenState extends ConsumerState { final amountController = TextEditingController(); final descriptionController = TextEditingController(); DateTime selectedDate = DateTime.now(); Category? selectedCategory; Wallet? fromWallet; Wallet? toWallet; bool isSaving = false; bool isRecurring = false; String recurringFrequency = 'MONTHLY'; final List frequencies = ['DAILY', 'WEEKLY', 'MONTHLY']; DateTime? dueDate; String alertSchedule = 'NONE'; TimeOfDay alertTime = const TimeOfDay(hour: 9, minute: 0); final List lineItems = []; final List existingAttachments = []; final List deletedAttachmentIds = []; final List> base64Attachments = []; final ImagePicker _picker = ImagePicker(); String? _jwtToken; final OcrService _ocrService = OcrService(); void _handleOcrResult(OcrResult? res) { if (res == null) return; if (res.amount != null) { setState(() { amountController.text = res.amount.toString(); }); ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Found amount: Rs. ${res.amount}'))); } else { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Could not find an amount in the receipt.'))); } } @override void initState() { super.initState(); const flutter_secure_storage.FlutterSecureStorage().read(key: 'jwt_token').then((value) { if (mounted) setState(() => _jwtToken = value); }); if (widget.transaction != null) { final t = widget.transaction!; amountController.text = t.amount.toString(); if (t.description != null) descriptionController.text = t.description!; selectedDate = t.date; if (t.items != null) lineItems.addAll(t.items!); if (t.attachments != null) existingAttachments.addAll(t.attachments!); dueDate = t.dueDate; alertSchedule = t.alertSchedule ?? 'NONE'; if (t.alertTime != null) { final parts = t.alertTime!.split(':'); if (parts.length >= 2) { alertTime = TimeOfDay(hour: int.tryParse(parts[0]) ?? 9, minute: int.tryParse(parts[1]) ?? 0); } } } } void _resolveInitialSelections(List? categories, List? wallets) { if (widget.transaction != null && selectedCategory == null && categories != null) { final matches = categories.where((c) => c.id == widget.transaction!.categoryId); if (matches.isNotEmpty) { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) setState(() => selectedCategory = matches.first); }); } } if (wallets != null && wallets.isNotEmpty && (fromWallet == null || toWallet == null)) { if (widget.transaction != null) { final fMatches = wallets.where((w) => w.id == widget.transaction!.fromWalletId); if (fMatches.isNotEmpty && fromWallet == null) { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) setState(() => fromWallet = fMatches.first); }); } final tMatches = wallets.where((w) => w.id == widget.transaction!.toWalletId); if (tMatches.isNotEmpty && toWallet == null) { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) setState(() => toWallet = tMatches.first); }); } } } if (widget.transaction == null) { if (categories != null && categories.length == 1 && selectedCategory == null) { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) setState(() => selectedCategory = categories.first); }); } if (wallets != null && wallets.length == 1 && toWallet == null) { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) setState(() => toWallet = wallets.first); }); } } } Future _pickImage(ImageSource source) async { if ((base64Attachments.length + existingAttachments.length) >= 3) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Max 3 attachments allowed'))); return; } final XFile? image = await _picker.pickImage(source: source); if (image != null) { final bytes = await image.readAsBytes(); final compressedBytes = await FlutterImageCompress.compressWithList( bytes, minWidth: 600, quality: 80, ); final base64String = base64Encode(compressedBytes); setState(() { base64Attachments.add({ 'fileName': image.name, 'contentType': 'image/jpeg', 'base64Content': base64String, }); }); } } void _addLineItem() { final nameCtrl = TextEditingController(); final amountCtrl = TextEditingController(); showDialog( context: context, builder: (ctx) => Dialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(28)), elevation: 0, backgroundColor: Colors.transparent, child: GestureDetector( onTap: () => FocusScope.of(context).unfocus(), child: Container( padding: const EdgeInsets.all(24), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(28), boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.1), blurRadius: 20, offset: const Offset(0, 10))], ), child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Add Item', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), const SizedBox(height: 24), TextField( controller: nameCtrl, style: const TextStyle(fontWeight: FontWeight.w500), decoration: InputDecoration( labelText: 'Item Name', filled: true, fillColor: Colors.grey.shade100, border: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: BorderSide.none), focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: const BorderSide(color: Color(0xFF6C63FF), width: 2)), ) ), const SizedBox(height: 16), TextField( controller: amountCtrl, keyboardType: const TextInputType.numberWithOptions(decimal: true, signed: false), textInputAction: TextInputAction.done, style: const TextStyle(fontWeight: FontWeight.w500), decoration: InputDecoration( labelText: 'Amount', prefixText: 'Rs. ', prefixStyle: const TextStyle(color: Colors.black87, fontWeight: FontWeight.w500, fontSize: 16), filled: true, fillColor: Colors.grey.shade100, border: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: BorderSide.none), focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: const BorderSide(color: Color(0xFF6C63FF), width: 2)), ) ), const SizedBox(height: 24), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ TextButton( onPressed: () => Navigator.pop(ctx), style: TextButton.styleFrom(foregroundColor: Colors.grey.shade700, padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12))), child: const Text('Cancel', style: TextStyle(fontWeight: FontWeight.bold)) ), const SizedBox(width: 12), ElevatedButton( onPressed: () { if (nameCtrl.text.isNotEmpty && amountCtrl.text.isNotEmpty) { setState(() { lineItems.add(TransactionItem(name: nameCtrl.text, amount: double.parse(amountCtrl.text))); double currentTotal = double.tryParse(amountController.text) ?? 0; amountController.text = (currentTotal + double.parse(amountCtrl.text)).toString(); }); Navigator.pop(ctx); } }, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF6C63FF), foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), elevation: 0, ), child: const Text('Add', style: TextStyle(fontWeight: FontWeight.bold)) ), ], ), ], ), ), ), ), ), ); } Future _showCreateWalletDialog() async { final ctrl = TextEditingController(); String selectedNature = 'CASH'; final natures = ['CASH', 'SAVINGS', 'INVESTMENTS', 'LOAN', 'LENDING', 'PAYABLES', 'INCOME', 'EXPENSE']; await showDialog( context: context, builder: (ctx) => StatefulBuilder( builder: (context, setStateDialog) { return Dialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(28)), elevation: 0, backgroundColor: Colors.transparent, child: GestureDetector( onTap: () => FocusScope.of(context).unfocus(), child: Container( padding: const EdgeInsets.all(24), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(28), boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.1), blurRadius: 20, offset: const Offset(0, 10))], ), child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration(color: const Color(0xFF6C63FF).withValues(alpha: 0.1), shape: BoxShape.circle), child: const Icon(LucideIcons.wallet, color: Color(0xFF6C63FF), size: 24), ), const SizedBox(width: 16), const Text('New Account', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), ], ), const SizedBox(height: 24), TextField( controller: ctrl, style: const TextStyle(fontWeight: FontWeight.w500), decoration: InputDecoration( hintText: 'Account Name (e.g. Household)', filled: true, fillColor: Colors.grey.shade100, border: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: BorderSide.none), focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: const BorderSide(color: Color(0xFF6C63FF), width: 2)), ), ), const SizedBox(height: 16), DropdownButtonFormField( value: selectedNature, style: const TextStyle(fontWeight: FontWeight.w500, color: Colors.black87), decoration: InputDecoration( labelText: 'Account Nature', filled: true, fillColor: Colors.grey.shade100, border: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: BorderSide.none), focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: const BorderSide(color: Color(0xFF6C63FF), width: 2)), ), items: natures.map((n) => DropdownMenuItem(value: n, child: Text(n))).toList(), onChanged: (val) { if (val != null) setStateDialog(() => selectedNature = val); }, ), const SizedBox(height: 24), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ TextButton( onPressed: () => Navigator.pop(ctx), style: TextButton.styleFrom(foregroundColor: Colors.grey.shade700, padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12))), child: const Text('Cancel', style: TextStyle(fontWeight: FontWeight.bold)), ), const SizedBox(width: 12), ElevatedButton( onPressed: () async { if (ctrl.text.isNotEmpty) { await ref.read(walletProvider.notifier).createWallet(name: ctrl.text, nature: selectedNature); if (context.mounted) Navigator.pop(ctx); } }, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF6C63FF), foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), elevation: 0, ), child: const Text('Create', style: TextStyle(fontWeight: FontWeight.bold)), ) ], ), ], ), ), ), ), ); } ), ); } void _showCategoryPicker() { showModalBottomSheet( context: context, builder: (ctx) { return Consumer(builder: (context, ref, child) { final categoriesState = ref.watch(categoryProvider); return Column( children: [ ListTile( title: const Text('Select Category', style: TextStyle(fontWeight: FontWeight.bold)), trailing: IconButton( icon: const Icon(LucideIcons.plus), onPressed: () => _showQuickAdd('Category'), ), ), Expanded( child: categoriesState.when( loading: () => const Center(child: CircularProgressIndicator()), error: (e, st) => Center(child: Text('Error: $e')), data: (categories) => ListView.builder( itemCount: categories.length, itemBuilder: (context, index) { final c = categories[index]; return ListTile( leading: const Icon(LucideIcons.tag), title: Text(c.name), onTap: () { setState(() => selectedCategory = c); Navigator.pop(context); }, ); }, ), ), ) ], ); }); }, ); } void _showWalletPicker(bool isDestination) { showModalBottomSheet( context: context, builder: (ctx) { return Consumer(builder: (context, ref, child) { final walletsState = ref.watch(walletProvider); return Column( children: [ ListTile( title: Text(isDestination ? 'Select To Account' : 'Select From Account', style: const TextStyle(fontWeight: FontWeight.bold)), leading: IconButton( icon: const Icon(LucideIcons.plus), tooltip: 'Create New Account', onPressed: () { Navigator.pop(context); // Close picker _showCreateWalletDialog(); }, ), trailing: IconButton( icon: const Icon(LucideIcons.x), onPressed: () { if (isDestination) { setState(() => toWallet = null); } else { setState(() => fromWallet = null); } Navigator.pop(context); }, ), ), Expanded( child: walletsState.when( loading: () => const Center(child: CircularProgressIndicator()), error: (e, st) => Center(child: Text('Error: $e')), data: (wallets) { return ListView.builder( itemCount: wallets.length, itemBuilder: (context, index) { final w = wallets[index]; return ListTile( leading: const Icon(LucideIcons.wallet), title: Text(w.name), subtitle: Text('${w.nature ?? "CASH"} • Balance: Rs. ${w.balance}'), onTap: () { if (isDestination) { setState(() => toWallet = w); } else { setState(() => fromWallet = w); } Navigator.pop(context); }, ); }, ); } ), ) ], ); }); }, ); } Future _pickDate() async { final picked = await showDatePicker( context: context, initialDate: selectedDate, firstDate: DateTime(2000), lastDate: DateTime(2101), ); if (picked != null && picked != selectedDate) { setState(() { selectedDate = picked; }); } } void _showQuickAdd(String type) { Navigator.pop(context); final ctrl = TextEditingController(); showDialog( context: context, builder: (ctx) => Dialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(28)), elevation: 0, backgroundColor: Colors.transparent, child: GestureDetector( onTap: () => FocusScope.of(context).unfocus(), child: Container( padding: const EdgeInsets.all(24), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(28), boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.1), blurRadius: 20, offset: const Offset(0, 10))], ), child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Add New $type', style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), const SizedBox(height: 24), TextField( controller: ctrl, textInputAction: TextInputAction.done, style: const TextStyle(fontWeight: FontWeight.w500), decoration: InputDecoration( hintText: 'Name', filled: true, fillColor: Colors.grey.shade100, border: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: BorderSide.none), focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: const BorderSide(color: Color(0xFF6C63FF), width: 2)), ), ), const SizedBox(height: 24), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ TextButton( onPressed: () => Navigator.pop(ctx), style: TextButton.styleFrom(foregroundColor: Colors.grey.shade700, padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12))), child: const Text('Cancel', style: TextStyle(fontWeight: FontWeight.bold)), ), const SizedBox(width: 12), ElevatedButton( onPressed: () async { if (ctrl.text.isEmpty) return; if (type == 'Category') { await ref.read(categoryProvider.notifier).addCategory(ctrl.text, 'tag'); } if (mounted) Navigator.pop(ctx); }, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF6C63FF), foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), elevation: 0, ), child: const Text('Add', style: TextStyle(fontWeight: FontWeight.bold)), ) ], ), ], ), ), ), ), ), ); } Future _saveTransaction() async { final amountText = amountController.text.trim(); if (amountText.isEmpty) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Please enter an amount'))); return; } if (toWallet == null) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Please select a To Account'))); return; } // Check if From Account is mandatory bool fromAccountOptional = (toWallet?.nature == 'SAVINGS' || toWallet?.nature == 'INCOME'); if (fromWallet == null && !fromAccountOptional) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Please select a From Account'))); return; } if (fromWallet != null && toWallet != null && fromWallet?.id == toWallet?.id) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('From and To accounts cannot be the same'))); return; } final amount = double.tryParse(amountText); if (amount == null) return; if (fromWallet != null && fromWallet!.balance < amount) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Insufficient balance in From Account'))); return; } setState(() => isSaving = true); // Determine internal type based on natures String type = 'TRANSFER'; if (fromWallet == null) { type = 'INCOME'; } else if (toWallet == null) { // Though toWallet is mandatory, just in case type = 'EXPENSE'; } else if (fromWallet?.nature == 'INCOME') { type = 'INCOME'; } else if (toWallet?.nature == 'PAYABLES' || toWallet?.nature == 'EXPENSE') { type = 'EXPENSE'; } else if (toWallet?.nature == 'INVESTMENTS') { type = 'INVESTMENT'; } if (isRecurring && widget.transaction == null) { DateTime nextDate = selectedDate; if (recurringFrequency == 'DAILY') { nextDate = nextDate.add(const Duration(days: 1)); } else if (recurringFrequency == 'WEEKLY') { nextDate = nextDate.add(const Duration(days: 7)); } else if (recurringFrequency == 'MONTHLY') { nextDate = DateTime(nextDate.year, nextDate.month + 1, nextDate.day); } final rt = RecurringTransaction( id: 0, type: type, amount: amount, categoryId: selectedCategory?.id, fromWalletId: fromWallet?.id, toWalletId: toWallet?.id, frequency: recurringFrequency, description: descriptionController.text.trim(), nextExecutionDate: nextDate, status: 'ACTIVE', ); await ref.read(recurringTransactionProvider.notifier).addRecurringTransaction(rt); final tx = Transaction( id: 0, type: type, amount: amount, date: selectedDate, description: descriptionController.text.trim(), categoryId: selectedCategory?.id, fromWalletId: fromWallet?.id, toWalletId: toWallet?.id, dueDate: dueDate, alertSchedule: alertSchedule == 'NONE' ? null : alertSchedule, alertTime: alertSchedule == 'NONE' ? null : '${alertTime.hour.toString().padLeft(2, '0')}:${alertTime.minute.toString().padLeft(2, '0')}:00', items: lineItems.isNotEmpty ? lineItems : null, ); await ref.read(transactionProvider.notifier).addTransaction(tx, base64Attachments: base64Attachments); } else { final tx = Transaction( id: widget.transaction?.id ?? 0, type: type, amount: amount, date: selectedDate, description: descriptionController.text.trim(), categoryId: selectedCategory?.id, fromWalletId: fromWallet?.id, toWalletId: toWallet?.id, dueDate: dueDate, alertSchedule: alertSchedule == 'NONE' ? null : alertSchedule, alertTime: alertSchedule == 'NONE' ? null : '${alertTime.hour.toString().padLeft(2, '0')}:${alertTime.minute.toString().padLeft(2, '0')}:00', items: lineItems.isNotEmpty ? lineItems : null, ); if (widget.transaction == null) { await ref.read(transactionProvider.notifier).addTransaction(tx, base64Attachments: base64Attachments); } else { await ref.read(transactionProvider.notifier).updateTransaction( tx, base64Attachments: base64Attachments, deletedAttachmentIds: deletedAttachmentIds, ); } if (alertSchedule != 'NONE' && dueDate != null) { DateTime alertDate = dueDate!; if (alertSchedule == '1_DAY_BEFORE') { alertDate = dueDate!.subtract(const Duration(days: 1)); } else if (alertSchedule == '2_DAYS_BEFORE') { alertDate = dueDate!.subtract(const Duration(days: 2)); } else if (alertSchedule == '1_WEEK_BEFORE') { alertDate = dueDate!.subtract(const Duration(days: 7)); } final notifyDate = DateTime(alertDate.year, alertDate.month, alertDate.day, alertTime.hour, alertTime.minute); if (notifyDate.isAfter(DateTime.now())) { NotificationService().scheduleNotification( id: DateTime.now().millisecondsSinceEpoch.remainder(100000), title: 'Payment Due!', body: 'Rs. ${amount.toStringAsFixed(0)} is due for ${toWallet?.name}', scheduledDate: notifyDate, ); } } } if (mounted) { SnackBarService.showSuccess(context, 'Transaction saved successfully'); Navigator.pop(context); } } @override Widget build(BuildContext context) { final categoriesState = ref.watch(categoryProvider); final walletsState = ref.watch(walletProvider); _resolveInitialSelections(categoriesState.value, walletsState.value); return Scaffold( appBar: AppBar( title: Text(widget.transaction == null ? 'Add Transaction' : 'Edit Transaction'), backgroundColor: Colors.transparent, elevation: 0, actions: [ IconButton( icon: const Icon(LucideIcons.camera), tooltip: 'Scan Receipt', onPressed: () async { showModalBottomSheet( context: context, builder: (ctx) => SafeArea( child: Column( mainAxisSize: MainAxisSize.min, children: [ ListTile( leading: const Icon(LucideIcons.camera), title: const Text('Take a Photo'), onTap: () async { Navigator.pop(ctx); final res = await _ocrService.scanReceiptFromCamera(); _handleOcrResult(res); }, ), ListTile( leading: const Icon(LucideIcons.image), title: const Text('Choose from Gallery'), onTap: () async { Navigator.pop(ctx); final res = await _ocrService.scanReceiptFromGallery(); _handleOcrResult(res); }, ), ], ), ), ); }, ) ], ), body: SafeArea( child: SingleChildScrollView( padding: const EdgeInsets.all(24.0), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ TextField( controller: amountController, keyboardType: const TextInputType.numberWithOptions(decimal: true, signed: false), textInputAction: TextInputAction.next, style: const TextStyle(fontSize: 32, fontWeight: FontWeight.bold, color: Color(0xFF6C63FF)), textAlign: TextAlign.center, decoration: InputDecoration( hintText: '0.00', hintStyle: TextStyle(color: Colors.grey.shade400), prefixText: 'Rs. ', prefixStyle: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.black87), filled: true, fillColor: const Color(0xFF6C63FF).withValues(alpha: 0.05), contentPadding: const EdgeInsets.symmetric(vertical: 24, horizontal: 24), border: OutlineInputBorder(borderRadius: BorderRadius.circular(24), borderSide: BorderSide.none), focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(24), borderSide: const BorderSide(color: Color(0xFF6C63FF), width: 2)), ), ), const SizedBox(height: 24), TextField( controller: descriptionController, textInputAction: TextInputAction.done, style: const TextStyle(fontWeight: FontWeight.w500), decoration: InputDecoration( hintText: 'Description (Optional)', prefixIcon: const Icon(LucideIcons.alignLeft, color: Colors.grey), filled: true, fillColor: Colors.grey.shade100, contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), border: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: BorderSide.none), focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: const BorderSide(color: Color(0xFF6C63FF), width: 2)), ), ), const SizedBox(height: 16), ListTile( title: Text(DateFormat('dd MMM yyyy').format(selectedDate)), trailing: const Icon(LucideIcons.calendar), onTap: _pickDate, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12), side: BorderSide(color: Colors.grey.shade300)), ), const SizedBox(height: 12), Tooltip( message: 'The destination account where the money is going to.', child: ListTile( title: Text(toWallet?.name ?? 'To Account'), subtitle: toWallet != null ? Text(toWallet!.nature ?? 'CASH') : null, trailing: const Icon(LucideIcons.chevronRight), onTap: () => _showWalletPicker(true), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12), side: BorderSide(color: Colors.grey.shade300)), ), ), const SizedBox(height: 12), Tooltip( message: 'The source account where the money is coming from.', child: ListTile( title: Text(fromWallet?.name ?? 'From Account${(toWallet?.nature == "SAVINGS" || toWallet?.nature == "INCOME") ? " (Optional)" : ""}'), subtitle: fromWallet != null ? Text(fromWallet!.nature ?? 'CASH') : null, trailing: const Icon(LucideIcons.chevronRight), onTap: () => _showWalletPicker(false), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12), side: BorderSide(color: Colors.grey.shade300)), ), ), const SizedBox(height: 12), ListTile( title: Text(selectedCategory?.name ?? 'Select Category (Optional)'), trailing: const Icon(LucideIcons.chevronRight), onTap: _showCategoryPicker, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12), side: BorderSide(color: Colors.grey.shade300)), ), const SizedBox(height: 12), if (toWallet?.nature == 'PAYABLES' || toWallet?.nature == 'LOAN') ...[ ListTile( title: Text(dueDate == null ? 'Set Due Date (Optional)' : 'Due Date: ${DateFormat('dd MMM yyyy').format(dueDate!)}'), trailing: const Icon(LucideIcons.calendar), onTap: () async { final picked = await showDatePicker( context: context, initialDate: dueDate ?? DateTime.now(), firstDate: DateTime.now().subtract(const Duration(days: 30)), lastDate: DateTime.now().add(const Duration(days: 365 * 5)), ); if (picked != null) { setState(() => dueDate = picked); } }, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12), side: BorderSide(color: Colors.grey.shade300)), ), if (dueDate != null) ...[ const SizedBox(height: 8), DropdownButtonFormField( value: alertSchedule, decoration: InputDecoration( labelText: 'Alert Notification', border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), ), items: const [ DropdownMenuItem(value: 'NONE', child: Text('No Alert')), DropdownMenuItem(value: 'ON_DUE_DATE', child: Text('On Due Date')), DropdownMenuItem(value: '1_DAY_BEFORE', child: Text('1 Day Before')), DropdownMenuItem(value: '2_DAYS_BEFORE', child: Text('2 Days Before')), DropdownMenuItem(value: '1_WEEK_BEFORE', child: Text('1 Week Before')), ], onChanged: (val) { if (val != null) setState(() => alertSchedule = val); }, ), if (alertSchedule != 'NONE') ...[ const SizedBox(height: 8), ListTile( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12), side: BorderSide(color: Colors.grey.shade300)), title: const Text('Alert Time'), trailing: Text(alertTime.format(context), style: const TextStyle(fontWeight: FontWeight.bold)), onTap: () async { final time = await showTimePicker( context: context, initialTime: alertTime, ); if (time != null) { setState(() => alertTime = time); } }, ), ], ], const SizedBox(height: 12), ], // Line Items Section Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('Items', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), TextButton.icon( onPressed: _addLineItem, icon: const Icon(LucideIcons.plus, size: 16), label: const Text('Add Item'), ) ], ), if (lineItems.isNotEmpty) Container( decoration: BoxDecoration(border: Border.all(color: Colors.grey.shade300), borderRadius: BorderRadius.circular(12)), child: ListView.separated( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: lineItems.length, separatorBuilder: (c, i) => const Divider(height: 1), itemBuilder: (c, i) { final item = lineItems[i]; return ListTile( title: Text(item.name), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ Text('Rs. ${item.amount}', style: const TextStyle(fontWeight: FontWeight.bold)), IconButton( icon: const Icon(LucideIcons.trash2, color: Colors.red, size: 18), onPressed: () => setState(() => lineItems.removeAt(i)), ) ], ), ); }, ), ), const SizedBox(height: 12), // Attachments Section Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('Attachments (${base64Attachments.length}/3)', style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), if (base64Attachments.length < 3) TextButton.icon( onPressed: () { showModalBottomSheet( context: context, builder: (ctx) => SafeArea( child: Column( mainAxisSize: MainAxisSize.min, children: [ ListTile( leading: const Icon(LucideIcons.camera), title: const Text('Camera'), onTap: () { Navigator.pop(ctx); _pickImage(ImageSource.camera); }, ), ListTile( leading: const Icon(LucideIcons.image), title: const Text('Gallery'), onTap: () { Navigator.pop(ctx); _pickImage(ImageSource.gallery); }, ), ], ), ), ); }, icon: const Icon(LucideIcons.paperclip, size: 16), label: const Text('Attach'), ) ], ), if (existingAttachments.isNotEmpty) SizedBox( height: 100, child: ListView.builder( scrollDirection: Axis.horizontal, itemCount: existingAttachments.length, itemBuilder: (c, i) { final att = existingAttachments[i]; return Stack( children: [ GestureDetector( onTap: () => _openGallery(i), child: Container( margin: const EdgeInsets.only(right: 8, top: 8), width: 80, decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), image: DecorationImage( image: _jwtToken != null ? NetworkImage('https://app.technobeesolutions.in/api/kifi/transactions/${widget.transaction!.id}/attachments/${att.id}/content', headers: {'Authorization': 'Bearer $_jwtToken'}) : const AssetImage('assets/images/placeholder.png') as ImageProvider, fit: BoxFit.cover, ), ), ), ), Positioned( top: 0, right: 0, child: GestureDetector( onTap: () { setState(() { deletedAttachmentIds.add(att.id); existingAttachments.removeAt(i); }); }, child: Container( padding: const EdgeInsets.all(2), decoration: const BoxDecoration(color: Colors.red, shape: BoxShape.circle), child: const Icon(LucideIcons.x, size: 12, color: Colors.white), ), ), ), ], ); }, ), ), if (base64Attachments.isNotEmpty) SizedBox( height: 100, child: ListView.builder( scrollDirection: Axis.horizontal, itemCount: base64Attachments.length, itemBuilder: (c, i) { final att = base64Attachments[i]; return Stack( children: [ GestureDetector( onTap: () => _openGallery(existingAttachments.length + i), child: Container( margin: const EdgeInsets.only(right: 8, top: 8), width: 80, decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), image: DecorationImage( image: MemoryImage(base64Decode(att['base64Content']!)), fit: BoxFit.cover, ), ), ), ), Positioned( top: 0, right: 0, child: GestureDetector( onTap: () => setState(() => base64Attachments.removeAt(i)), child: Container( padding: const EdgeInsets.all(2), decoration: const BoxDecoration(color: Colors.red, shape: BoxShape.circle), child: const Icon(LucideIcons.x, size: 12, color: Colors.white), ), ), ), ], ); }, ), ), const SizedBox(height: 24), if (widget.transaction == null) SwitchListTile( title: const Text('Recurring Transaction'), value: isRecurring, activeColor: Theme.of(context).colorScheme.primary, onChanged: (val) => setState(() => isRecurring = val), ), if (isRecurring && widget.transaction == null) DropdownButtonFormField( value: recurringFrequency, decoration: InputDecoration( labelText: 'Frequency', border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), ), items: frequencies.map((f) => DropdownMenuItem(value: f, child: Text(f))).toList(), onChanged: (val) { if (val != null) setState(() => recurringFrequency = val); }, ), const SizedBox(height: 32), SizedBox( height: 56, child: ElevatedButton( onPressed: isSaving ? null : _saveTransaction, style: ElevatedButton.styleFrom( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), ), child: isSaving ? const CircularProgressIndicator() : const Text('Save Transaction', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), ), ), const SizedBox(height: 32), ], ), ), ), ); } void _openGallery(int index) { final List allImages = []; // Add existing attachments for (final att in existingAttachments) { if (_jwtToken != null) { allImages.add(NetworkImage('https://app.technobeesolutions.in/api/kifi/transactions/${widget.transaction!.id}/attachments/${att.id}/content', headers: {'Authorization': 'Bearer $_jwtToken'})); } else { allImages.add(const AssetImage('assets/images/placeholder.png')); } } // Add new attachments for (final att in base64Attachments) { allImages.add(MemoryImage(base64Decode(att['base64Content']!))); } Navigator.push( context, MaterialPageRoute( builder: (context) => AttachmentGalleryScreen( images: allImages, initialIndex: index, ), ), ); } }