import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:cashumit/models/receipt_item.dart'; import 'package:shared_preferences/shared_preferences.dart'; class AddItemScreen extends StatefulWidget { const AddItemScreen({super.key, this.item}); // Constructor untuk mode edit const AddItemScreen.fromItem(this.item, {super.key}); final ReceiptItem? item; @override State createState() => _AddItemScreenState(); } class _AddItemScreenState extends State { final _formKey = GlobalKey(); late final TextEditingController _descriptionController; late final TextEditingController _quantityController; late final TextEditingController _priceController; final FocusNode _priceFocusNode = FocusNode(); List> _recentItems = []; static const String _recentItemsKey = 'recent_receipt_items'; @override void initState() { super.initState(); _descriptionController = TextEditingController(text: widget.item?.description ?? ''); _quantityController = TextEditingController( text: widget.item != null ? (widget.item!.quantity % 1 == 0 ? widget.item!.quantity.toInt().toString() : widget.item!.quantity.toString()) : '1'); _priceController = TextEditingController( text: widget.item != null ? _formatNumber(widget.item!.price.toInt().toString()) : ''); _loadRecentItems(); } @override void dispose() { _descriptionController.dispose(); _quantityController.dispose(); _priceController.dispose(); _priceFocusNode.dispose(); super.dispose(); } Future _loadRecentItems() async { try { final prefs = await SharedPreferences.getInstance(); final list = prefs.getStringList(_recentItemsKey) ?? []; if (list.isNotEmpty && mounted) { setState(() { _recentItems = list .map((e) => jsonDecode(e) as Map) .take(6) .toList(); }); } } catch (_) {} } Future _saveRecentItem(String desc, double price) async { try { final prefs = await SharedPreferences.getInstance(); final list = prefs.getStringList(_recentItemsKey) ?? []; final currentList = list .map((e) => jsonDecode(e) as Map) .where((e) => (e['desc'] as String).toLowerCase() != desc.toLowerCase()) .toList(); currentList.insert(0, {'desc': desc, 'price': price}); final encoded = currentList.take(8).map((e) => jsonEncode(e)).toList(); await prefs.setStringList(_recentItemsKey, encoded); } catch (_) {} } String _formatNumber(String s) { final clean = s.replaceAll(RegExp(r'[^\d]'), ''); if (clean.isEmpty) return ''; final val = int.tryParse(clean); if (val == null) return clean; return val.toString().replaceAllMapped( RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'), (Match m) => '${m[1]}.'); } double _parsePrice(String text) { final clean = text.replaceAll('.', '').replaceAll(',', '').trim(); return double.tryParse(clean) ?? 0.0; } void _onPriceChanged(String value) { final formatted = _formatNumber(value); if (formatted != value) { _priceController.value = TextEditingValue( text: formatted, selection: TextSelection.collapsed(offset: formatted.length), ); } } void _addNominalToPrice(int nominal) { final current = _parsePrice(_priceController.text); final next = current + nominal; final formatted = _formatNumber(next.toInt().toString()); setState(() { _priceController.text = formatted; }); } void _stepQuantity(int delta) { final current = double.tryParse(_quantityController.text) ?? 1.0; final next = current + delta; if (next >= 1) { setState(() { _quantityController.text = next % 1 == 0 ? next.toInt().toString() : next.toString(); }); } } void _selectRecentItem(Map item) { setState(() { _descriptionController.text = item['desc']?.toString() ?? ''; final price = (item['price'] as num?)?.toDouble() ?? 0.0; if (price > 0) { _priceController.text = _formatNumber(price.toInt().toString()); } }); _priceFocusNode.requestFocus(); } void _saveItem() { if (_formKey.currentState!.validate()) { final desc = _descriptionController.text.trim(); final qty = double.tryParse(_quantityController.text) ?? 1.0; final price = _parsePrice(_priceController.text); _saveRecentItem(desc, price); final item = ReceiptItem( description: desc, quantity: qty, price: price, ); Navigator.pop(context, item); } } @override Widget build(BuildContext context) { final primaryColor = Theme.of(context).colorScheme.primary; return AlertDialog( insetPadding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 24.0), title: Row( children: [ Icon( widget.item == null ? Icons.add_shopping_cart : Icons.edit_note, color: primaryColor, size: 22, ), const SizedBox(width: 8), Text(widget.item == null ? 'Tambah Item' : 'Edit Item'), ], ), content: SizedBox( width: MediaQuery.of(context).size.width, child: SingleChildScrollView( child: Form( key: _formKey, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ // Kolom Deskripsi Item TextFormField( controller: _descriptionController, autofocus: widget.item == null, textInputAction: TextInputAction.next, decoration: const InputDecoration( labelText: 'Deskripsi / Nama Item', hintText: 'Misal: Kopi Susu, Nasi Goreng', border: OutlineInputBorder(), isDense: true, ), validator: (value) { if (value == null || value.trim().isEmpty) { return 'Mohon masukkan nama item'; } return null; }, onFieldSubmitted: (_) => _priceFocusNode.requestFocus(), ), // Saran Item Cepat (Riwayat) if (widget.item == null && _recentItems.isNotEmpty) ...[ const SizedBox(height: 8), SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: _recentItems.map((recent) { return Padding( padding: const EdgeInsets.only(right: 6.0), child: ActionChip( avatar: const Icon(Icons.history, size: 13), label: Text( recent['desc']?.toString() ?? '', style: const TextStyle(fontSize: 11), ), onPressed: () => _selectRecentItem(recent), visualDensity: VisualDensity.compact, padding: const EdgeInsets.symmetric(horizontal: 2), ), ); }).toList(), ), ), ], const SizedBox(height: 14), // Baris Jumlah (dengan stepper) & Harga Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Stepper Jumlah Expanded( flex: 4, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Jumlah (Qty)', style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold), ), const SizedBox(height: 4), Container( decoration: BoxDecoration( border: Border.all(color: Colors.grey.shade400), borderRadius: BorderRadius.circular(4), ), child: Row( children: [ InkWell( onTap: () => _stepQuantity(-1), child: const Padding( padding: EdgeInsets.symmetric(horizontal: 6, vertical: 8), child: Icon(Icons.remove, size: 16), ), ), Expanded( child: TextFormField( controller: _quantityController, textAlign: TextAlign.center, decoration: const InputDecoration( border: InputBorder.none, isDense: true, contentPadding: EdgeInsets.zero, ), keyboardType: TextInputType.number, validator: (value) { if (value == null || value.isEmpty) { return 'Wajib diisi'; } if (double.tryParse(value) == null) { return 'Harus angka'; } return null; }, ), ), InkWell( onTap: () => _stepQuantity(1), child: const Padding( padding: EdgeInsets.symmetric(horizontal: 6, vertical: 8), child: Icon(Icons.add, size: 16), ), ), ], ), ), ], ), ), const SizedBox(width: 12), // Input Harga dengan pemisah ribuan Expanded( flex: 6, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Harga Satuan', style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold), ), const SizedBox(height: 4), TextFormField( controller: _priceController, focusNode: _priceFocusNode, textInputAction: TextInputAction.done, decoration: const InputDecoration( prefixText: 'Rp ', hintText: '0', border: OutlineInputBorder(), isDense: true, ), keyboardType: TextInputType.number, onChanged: _onPriceChanged, onFieldSubmitted: (_) => _saveItem(), validator: (value) { if (value == null || value.isEmpty) { return 'Mohon masukkan harga'; } if (_parsePrice(value) <= 0) { return 'Harga harus > 0'; } return null; }, ), ], ), ), ], ), const SizedBox(height: 8), // Chip Bantuan Nominal Cepat (+5k, +10k, +25k, +50k) Wrap( spacing: 6, children: [ ActionChip( label: const Text('+5rb', style: TextStyle(fontSize: 11)), onPressed: () => _addNominalToPrice(5000), visualDensity: VisualDensity.compact, ), ActionChip( label: const Text('+10rb', style: TextStyle(fontSize: 11)), onPressed: () => _addNominalToPrice(10000), visualDensity: VisualDensity.compact, ), ActionChip( label: const Text('+25rb', style: TextStyle(fontSize: 11)), onPressed: () => _addNominalToPrice(25000), visualDensity: VisualDensity.compact, ), ActionChip( label: const Text('+50rb', style: TextStyle(fontSize: 11)), onPressed: () => _addNominalToPrice(50000), visualDensity: VisualDensity.compact, ), ], ), ], ), ), ), ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('Batal'), ), ElevatedButton( onPressed: _saveItem, child: Text(widget.item == null ? 'Tambah' : 'Simpan'), ), ], ); } }