import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'dart:async'; import '../models/product.dart'; import '../models/sale.dart'; import '../state/app_state.dart'; import '../services/printer_service.dart'; import '../utils/colors.dart'; import '../utils/format.dart'; import '../utils/rupiah_input.dart'; class KasirScreen extends StatefulWidget { const KasirScreen({super.key}); @override State createState() => _KasirScreenState(); } class _KasirScreenState extends State { final TextEditingController _search = TextEditingController(); Timer? _searchDebounce; @override void dispose() { _searchDebounce?.cancel(); _search.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final state = context.watch(); if (state.loading) { return const Center(child: CircularProgressIndicator(color: kAccent)); } final query = _search.text.toLowerCase(); final isReseller = state.customerType == 'reseller'; final products = state.products .where((p) => p.name.toLowerCase().contains(query) || p.barcode.toLowerCase().contains(query)) .toList(); return Column( children: [ Padding( padding: const EdgeInsets.fromLTRB(12, 12, 12, 6), child: TextField( controller: _search, onChanged: (_) { _searchDebounce?.cancel(); _searchDebounce = Timer(const Duration(milliseconds: 300), () { if (mounted) setState(() {}); }); }, style: const TextStyle(color: kText), decoration: InputDecoration( hintText: 'Cari produk...', prefixIcon: const Icon(Icons.search, color: kMuted), suffixIcon: _search.text.isNotEmpty ? IconButton( icon: const Icon(Icons.close, color: kMuted), onPressed: () { _search.clear(); setState(() {}); }, ) : null, ), ), ), if (state.resellerEnabled) Padding( padding: const EdgeInsets.fromLTRB(12, 0, 12, 6), child: Row( children: [ const Text('Pembeli:', style: TextStyle(color: kMuted, fontSize: 13)), const SizedBox(width: 10), Expanded( child: SegmentedButton( segments: const [ ButtonSegment(value: 'umum', label: Text('Umum')), ButtonSegment( value: 'reseller', label: Text('Reseller')), ], selected: {state.customerType}, onSelectionChanged: (set) => state.setCustomerType(set.first), style: SegmentedButton.styleFrom( visualDensity: VisualDensity.compact, ), ), ), ], ), ), Expanded( child: products.isEmpty ? Center( child: Column( mainAxisSize: MainAxisSize.min, children: const [ Icon(Icons.search_off, size: 48, color: kMuted), SizedBox(height: 8), Text('Produk tidak ditemukan', style: TextStyle(color: kMuted)), ], ), ) : RefreshIndicator( onRefresh: () async { context.read().recordManualRefresh(); await context.read().refresh(); }, color: kAccent, child: GridView.builder( padding: const EdgeInsets.fromLTRB(12, 6, 12, 12), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, mainAxisSpacing: 12, crossAxisSpacing: 12, childAspectRatio: 1.1, ), itemCount: products.length, itemBuilder: (context, i) => _ProductCard( product: products[i], displayPrice: isReseller ? products[i].priceReseller : products[i].price, isReseller: isReseller, onTap: () { HapticFeedback.lightImpact(); state.addToCart(products[i]); }, ), ), ), ), if (state.cart.isNotEmpty) _CartBar( onTap: () => _openCartSheet(context, state), itemCount: state.cart.fold(0, (s, c) => s + c.qty), ), ], ); } void _openCartSheet(BuildContext context, AppState state) { showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (sheetContext) => DraggableScrollableSheet( initialChildSize: 0.6, minChildSize: 0.35, maxChildSize: 0.92, builder: (_, controller) => Container( decoration: const BoxDecoration( color: kSurface, borderRadius: BorderRadius.vertical(top: Radius.circular(24)), border: Border(top: BorderSide(color: kBorder)), ), child: _CartSheetContent( scrollController: controller, onPay: () => _showPaymentDialog(sheetContext, state), ), ), ), ); } void _showPaymentDialog(BuildContext sheetContext, AppState state) { final paidCtrl = TextEditingController(); double change = 0; final total = state.total; String paymentMethod = 'tunai'; final Map splitPayment = {}; showDialog( context: sheetContext, builder: (dialogContext) { return StatefulBuilder( builder: (context, setDialog) { final isPayingRef = [false]; DateTime? payingStart; final messenger = ScaffoldMessenger.of(context); void updatePaid(String v) { if (isPayingRef[0]) return; final paid = (RupiahInputFormatter.getRawValue(v) ?? 0).toDouble(); setDialog(() { change = paid - total; }); } Future handlePay() async { if (isPayingRef[0]) return; final paid = (RupiahInputFormatter.getRawValue(paidCtrl.text) ?? 0) .toDouble(); if (paymentMethod == 'tunai' && paid < total) { messenger.showSnackBar( const SnackBar( content: Text('Uang dibayar kurang dari total'), backgroundColor: kDanger, ), ); return; } payingStart = DateTime.now(); setDialog(() => isPayingRef[0] = true); try { final isBayarNanti = paymentMethod == 'bayar_nanti'; final sale = await state.checkout( isBayarNanti ? 0 : paid, paymentMethod: isBayarNanti ? 'tunai' : paymentMethod, splitPayment: paymentMethod == 'split' && splitPayment.isNotEmpty ? Map.from(splitPayment) : null, paymentStatus: isBayarNanti ? 'unpaid' : 'paid', dueAmount: isBayarNanti ? total : 0.0, ); if (!dialogContext.mounted) return; final elapsed = DateTime.now().difference(payingStart!); if (elapsed < const Duration(milliseconds: 300)) { await Future.delayed(const Duration(milliseconds: 300) - elapsed); } if (!dialogContext.mounted) return; Navigator.pop(dialogContext); Navigator.pop(sheetContext); if (mounted) { HapticFeedback.mediumImpact(); _showReceipt(dialogContext, sale); } } catch (e) { if (!dialogContext.mounted) return; Navigator.pop(dialogContext); Navigator.pop(sheetContext); messenger.showSnackBar( const SnackBar( content: Text( 'Transaksi disimpan offline dan akan disinkronkan saat koneksi kembali'), backgroundColor: kSuccess, duration: Duration(seconds: 3), ), ); } finally { if (dialogContext.mounted) { setDialog(() => isPayingRef[0] = false); } } } return AlertDialog( contentPadding: const EdgeInsets.all(20), content: SizedBox( width: double.maxFinite, child: Stack( children: [ SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ Row( children: [ const Icon(Icons.payments_outlined, color: kAccent), const SizedBox(width: 8), const Text('Pembayaran', style: TextStyle( fontSize: 18, fontWeight: FontWeight.w700)), const Spacer(), IconButton( icon: const Icon(Icons.close, color: kMuted), onPressed: isPayingRef[0] ? null : () => Navigator.pop(dialogContext), padding: EdgeInsets.zero, constraints: const BoxConstraints(), ), ], ), const SizedBox(height: 16), Container( width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: kSurface2, borderRadius: BorderRadius.circular(14), ), child: Column( children: [ const Text('TOTAL BELANJA', style: TextStyle( fontSize: 11, color: kMuted, letterSpacing: 1)), const SizedBox(height: 4), Text(formatRupiah(total), style: const TextStyle( fontSize: 26, fontWeight: FontWeight.w800, color: kAccent)), ], ), ), const SizedBox(height: 16), const Align( alignment: Alignment.centerLeft, child: Text('Metode Pembayaran', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600)), ), const SizedBox(height: 8), Column( children: [ Row( children: [ Expanded( child: _PaymentMethodChip( label: 'Tunai', icon: Icons.money_outlined, selected: paymentMethod == 'tunai', onTap: isPayingRef[0] ? null : () => setDialog(() => paymentMethod = 'tunai'), ), ), const SizedBox(width: 8), Expanded( child: _PaymentMethodChip( label: 'Bank', icon: Icons.account_balance_outlined, selected: paymentMethod == 'transfer', onTap: isPayingRef[0] ? null : () => setDialog(() => paymentMethod = 'transfer'), ), ), const SizedBox(width: 8), Expanded( child: _PaymentMethodChip( label: 'QRIS', icon: Icons.qr_code_2_outlined, selected: paymentMethod == 'qris', onTap: isPayingRef[0] ? null : () => setDialog(() => paymentMethod = 'qris'), ), ), ], ), const SizedBox(height: 8), const Divider(height: 1, thickness: 1), const SizedBox(height: 8), Row( children: [ Expanded( child: _PaymentMethodChip( label: 'Bayar Nanti', icon: Icons.watch_later_outlined, selected: paymentMethod == 'bayar_nanti', onTap: isPayingRef[0] ? null : () => setDialog(() => paymentMethod = 'bayar_nanti'), ), ), const SizedBox(width: 8), Expanded( child: _PaymentMethodChip( label: 'Split', icon: Icons.call_split_outlined, selected: paymentMethod == 'split', onTap: isPayingRef[0] ? null : () => setDialog(() => paymentMethod = 'split'), ), ), ], ), ], ), const SizedBox(height: 16), if (paymentMethod == 'tunai' || paymentMethod == 'bayar_nanti') ...[ AbsorbPointer( absorbing: isPayingRef[0] || paymentMethod == 'bayar_nanti', child: TextField( controller: paidCtrl, keyboardType: TextInputType.number, inputFormatters: paymentMethod == 'bayar_nanti' ? [] : [RupiahInputFormatter()], style: const TextStyle( fontSize: 18, fontWeight: FontWeight.w600), decoration: InputDecoration( labelText: paymentMethod == 'bayar_nanti' ? 'Belum Bayar' : 'Uang Dibayar', prefixText: 'Rp ', helperText: paymentMethod == 'bayar_nanti' ? 'Pembayaran dilakukan saat customer datang' : null, ), onChanged: paymentMethod == 'bayar_nanti' ? null : updatePaid, ), ), const SizedBox(height: 10), AbsorbPointer( absorbing: isPayingRef[0], child: Wrap( spacing: 8, runSpacing: 8, children: [ _Chip('Uang Pas', () { paidCtrl.text = RupiahInputFormatter.formatNumber(total.round()); updatePaid(paidCtrl.text); }), _Chip('+5rb', () { final current = RupiahInputFormatter.getRawValue(paidCtrl.text) ?? 0; paidCtrl.text = RupiahInputFormatter.formatNumber(current + 5000); updatePaid(paidCtrl.text); }), _Chip('+10rb', () { final current = RupiahInputFormatter.getRawValue(paidCtrl.text) ?? 0; paidCtrl.text = RupiahInputFormatter.formatNumber(current + 10000); updatePaid(paidCtrl.text); }), _Chip('+20rb', () { final current = RupiahInputFormatter.getRawValue(paidCtrl.text) ?? 0; paidCtrl.text = RupiahInputFormatter.formatNumber(current + 20000); updatePaid(paidCtrl.text); }), _Chip('+50rb', () { final current = RupiahInputFormatter.getRawValue(paidCtrl.text) ?? 0; paidCtrl.text = RupiahInputFormatter.formatNumber(current + 50000); updatePaid(paidCtrl.text); }), _Chip('+100rb', () { final current = RupiahInputFormatter.getRawValue(paidCtrl.text) ?? 0; paidCtrl.text = RupiahInputFormatter.formatNumber(current + 100000); updatePaid(paidCtrl.text); }), ], ), ), const SizedBox(height: 16), Container( width: double.infinity, padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 12), decoration: BoxDecoration( color: paymentMethod == 'bayar_nanti' ? kMuted.withValues(alpha: 0.12) : change >= 0 ? kSuccess.withValues(alpha: 0.12) : kDanger.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(12), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(paymentMethod == 'bayar_nanti' ? 'Status' : 'Kembalian', style: const TextStyle(color: kMuted)), Text( paymentMethod == 'bayar_nanti' ? 'Belum Bayar' : formatRupiah(change > 0 ? change : 0), style: TextStyle( fontSize: 18, fontWeight: FontWeight.w800, color: paymentMethod == 'bayar_nanti' ? kMuted : change >= 0 ? kSuccess : kDanger, ), ), ], ), ), ] else if (paymentMethod == 'split') ...[ Container( width: double.infinity, padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: kSurface2, borderRadius: BorderRadius.circular(12), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Pembayaran Gabungan', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600)), const SizedBox(height: 8), ...['tunai', 'transfer', 'qris'].map((method) { final label = method == 'tunai' ? 'Tunai' : method == 'transfer' ? 'Bank' : 'QRIS'; return Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Row( children: [ Expanded( child: Text(label, style: const TextStyle(fontSize: 13)), ), SizedBox( width: 120, child: TextField( keyboardType: TextInputType.number, inputFormatters: [RupiahInputFormatter()], decoration: InputDecoration( labelText: 'Rp $label', isDense: true, contentPadding: const EdgeInsets.symmetric( horizontal: 10, vertical: 8), ), onChanged: (v) { final raw = RupiahInputFormatter.getRawValue(v) ?? 0; setDialog(() { if (raw > 0) { splitPayment[method] = raw.toDouble(); } else { splitPayment.remove(method); } }); }, ), ), ], ), ); }), const SizedBox(height: 8), Text( 'Total split: ${formatRupiah(splitPayment.values.fold(0.0, (s, v) => s + v))} / ${formatRupiah(total)}', style: TextStyle( fontSize: 12, color: splitPayment.values.fold(0.0, (s, v) => s + v) >= total ? kSuccess : kDanger, ), ), ], ), ), ] else ...[ Container( width: double.infinity, padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: kSurface2, borderRadius: BorderRadius.circular(12), ), child: Text( paymentMethod == 'transfer' ? 'Transfer ke rekening toko. Konfirmasi pembayaran sebelum lanjut.' : 'Tampilkan kode QRIS ke customer untuk scan.', style: const TextStyle(fontSize: 13, color: kMuted), ), ), const SizedBox(height: 12), AbsorbPointer( absorbing: isPayingRef[0], child: TextField( keyboardType: TextInputType.number, inputFormatters: [RupiahInputFormatter()], style: const TextStyle( fontSize: 18, fontWeight: FontWeight.w600), decoration: InputDecoration( labelText: paymentMethod == 'transfer' ? 'Jumlah Bank' : 'Jumlah QRIS', prefixText: 'Rp ', ), onChanged: (v) { final paid = (RupiahInputFormatter.getRawValue(v) ?? 0).toDouble(); setDialog(() { change = paid - total; }); }, ), ), const SizedBox(height: 16), Container( width: double.infinity, padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 12), decoration: BoxDecoration( color: change >= 0 ? kSuccess.withValues(alpha: 0.12) : kDanger.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(12), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('Status', style: TextStyle(color: kMuted)), Text( change >= 0 ? 'Lunas' : 'Kurang ${formatRupiah(change.abs())}', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w700, color: change >= 0 ? kSuccess : kDanger, ), ), ], ), ), ], const SizedBox(height: 18), SizedBox( width: double.infinity, child: FilledButton( onPressed: isPayingRef[0] ? null : handlePay, style: FilledButton.styleFrom( backgroundColor: kSuccess, foregroundColor: const Color(0xFF06210F), ), child: isPayingRef[0] ? const Row( mainAxisAlignment: MainAxisAlignment.center, children: [ SizedBox( height: 18, width: 18, child: CircularProgressIndicator( strokeWidth: 2, color: Color(0xFF06210F), ), ), SizedBox(width: 10), Text('Memproses...', style: TextStyle( fontWeight: FontWeight.w700)), ], ) : Text(paymentMethod == 'bayar_nanti' ? 'Pesanan Sekarang' : paymentMethod == 'tunai' ? 'Selesai & Cetak' : 'Selesai'), ), ), ], ), ), ], ), ), ); }, ); }, ); } void _showReceipt(BuildContext context, Sale sale) { showDialog( context: context, builder: (context) { return AlertDialog( contentPadding: const EdgeInsets.all(20), content: SizedBox( width: double.maxFinite, child: Column( mainAxisSize: MainAxisSize.min, children: [ const CircleAvatar( radius: 26, backgroundColor: kSuccess, child: Icon(Icons.check, color: Colors.white, size: 30), ), const SizedBox(height: 10), const Text('Pembayaran Berhasil', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w700)), const SizedBox(height: 2), Text( '${formatTanggal(sale.date)} ${formatWaktu(sale.date)}', style: const TextStyle(color: kMuted, fontSize: 12), ), const SizedBox(height: 4), Text('#${sale.id.toUpperCase()}', style: const TextStyle(color: kMuted, fontSize: 11)), const Divider(height: 24), ...sale.items.map((it) => Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Text('${it.name} x${it.qty}', style: const TextStyle(fontSize: 14)), ), Text(formatRupiah(it.subtotal), style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600)), ], ), const SizedBox(height: 2), Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 1), decoration: BoxDecoration( color: it.isReseller ? kAccent.withValues(alpha: 0.15) : kMuted.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(4), ), child: Text( it.isReseller ? 'Reseller' : 'Umum', style: TextStyle( fontSize: 10, fontWeight: FontWeight.w600, color: it.isReseller ? kAccent : kMuted, ), ), ), ], ), )), const SizedBox(height: 4), Text( 'Kasir: ${context.read().getProfileName(sale.userId)}', style: const TextStyle(color: kMuted, fontSize: 12), ), if (sale.discount > 0) Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('Diskon', style: TextStyle(color: kMuted)), Text('-${formatRupiah(sale.discount)}', style: const TextStyle(color: kMuted)), ], ), ), const Divider(height: 24), _ReceiptRow('Total', formatRupiah(sale.total), true), _ReceiptRow('Metode', _paymentMethodLabel(sale.paymentMethod), false), if (sale.paymentStatus == 'unpaid') ...[ _ReceiptRow('Status', 'Belum Lunas', false), _ReceiptRow('Sisa', formatRupiah(sale.dueAmount), false), ] else if (sale.paymentMethod == 'tunai') ...[ _ReceiptRow('Bayar', formatRupiah(sale.paid), false), _ReceiptRow('Kembali', formatRupiah(sale.change), false), ], if (sale.splitPayment != null && sale.splitPayment!.isNotEmpty) ...sale.splitPayment!.entries.map((e) { final label = e.key == 'tunai' ? 'Tunai' : e.key == 'transfer' ? 'Bank' : 'QRIS'; return _ReceiptRow('Split $label', formatRupiah(e.value), false); }), const SizedBox(height: 16), const Text('Terima kasih!', style: TextStyle(color: kMuted)), ], ), ), actions: [ SizedBox( width: double.infinity, child: FilledButton( onPressed: () => Navigator.pop(context), child: const Text('Tutup'), ), ), const SizedBox(height: 8), SizedBox( width: double.infinity, child: OutlinedButton.icon( onPressed: () async { HapticFeedback.lightImpact(); final messenger = ScaffoldMessenger.of(context); final prefs = await SharedPreferences.getInstance(); final storeName = prefs.getString('store_name') ?? 'Toko Grosir'; final err = await printerService.printSale( sale, storeName, kasirName: context.read().getProfileName(sale.userId), whatsapp: prefs.getString('store_whatsapp'), notes: prefs.getString('store_notes'), ); if (mounted) { messenger.showSnackBar( SnackBar( content: Text(err == null ? 'Struk dikirim ke printer' : 'Gagal cetak: $err'), backgroundColor: err == null ? kSuccess : kDanger, ), ); } }, icon: const Icon(Icons.print_outlined), label: const Text('Cetak Struk'), ), ), ], ); }, ); } } class _CartBar extends StatefulWidget { final VoidCallback onTap; final int itemCount; const _CartBar({required this.onTap, required this.itemCount}); @override State<_CartBar> createState() => _CartBarState(); } class _CartBarState extends State<_CartBar> with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation _scaleAnim; @override void initState() { super.initState(); _controller = AnimationController( duration: const Duration(milliseconds: 180), vsync: this, ); _scaleAnim = Tween(begin: 1.0, end: 0.9).animate( CurvedAnimation(parent: _controller, curve: Curves.easeInOut), ); } @override void didUpdateWidget(covariant _CartBar oldWidget) { super.didUpdateWidget(oldWidget); if (widget.itemCount > oldWidget.itemCount) { _controller.forward().then((_) => _controller.reverse()); } } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final state = context.watch(); return Material( color: kSurface, child: InkWell( onTap: widget.onTap, child: ScaleTransition( scale: _scaleAnim, child: Container( padding: const EdgeInsets.fromLTRB(16, 12, 16, 16), decoration: const BoxDecoration( border: Border(top: BorderSide(color: kBorder)), ), child: Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: kAccent.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(12), ), child: const Icon(Icons.shopping_cart_outlined, color: kAccent, size: 20), ), const SizedBox(width: 10), Text('${widget.itemCount} item', style: const TextStyle( fontWeight: FontWeight.w700, fontSize: 15)), const Spacer(), Text(formatRupiah(state.total), style: const TextStyle( fontSize: 17, fontWeight: FontWeight.w800, color: kAccent)), const SizedBox(width: 8), Icon(Icons.expand_less, color: kMuted.withValues(alpha: 0.7)), ], ), ), ), ), ); } } class _CartSheetContent extends StatelessWidget { final ScrollController scrollController; final VoidCallback onPay; const _CartSheetContent( {required this.scrollController, required this.onPay}); @override Widget build(BuildContext context) { final state = context.watch(); final items = state.cart; final children = [ Container( margin: const EdgeInsets.only(top: 12), width: 40, height: 4, decoration: BoxDecoration( color: kBorder, borderRadius: BorderRadius.circular(4), ), ), Padding( padding: const EdgeInsets.fromLTRB(16, 12, 16, 4), child: Row( children: [ const Icon(Icons.shopping_cart_outlined, color: kAccent), const SizedBox(width: 8), const Text('Keranjang', style: TextStyle(fontWeight: FontWeight.w700, fontSize: 17)), const Spacer(), if (items.isNotEmpty) OutlinedButton( onPressed: () async { final confirm = await showDialog( context: context, builder: (ctx) => AlertDialog( icon: Icon(Icons.delete_forever_outlined, color: kDanger, size: 28), title: const Text('Kosongkan Keranjang?'), content: const Text( 'Semua item di keranjang akan dihapus. Tindakan ini tidak dapat dibatalkan.', ), actions: [ SizedBox( width: double.infinity, child: OutlinedButton( onPressed: () => Navigator.pop(ctx, false), style: OutlinedButton.styleFrom( foregroundColor: kMuted, side: const BorderSide(color: kBorder), ), child: const Text('Batal'), ), ), const SizedBox(height: 8), SizedBox( width: double.infinity, child: FilledButton( onPressed: () => Navigator.pop(ctx, true), style: FilledButton.styleFrom( backgroundColor: kDanger, ), child: const Text('Kosongkan'), ), ), ], ), ); if (confirm == true && context.mounted) { state.clearCart(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Keranjang dikosongkan'), backgroundColor: kSuccess, ), ); } }, style: OutlinedButton.styleFrom( foregroundColor: kDanger, side: const BorderSide(color: kDanger), ), child: const Text('Kosongkan'), ), ], ), ), const Divider(height: 12), ]; if (items.isEmpty) { children.add(Padding( padding: const EdgeInsets.symmetric(vertical: 40), child: Center( child: Column( children: [ Icon(Icons.shopping_cart_outlined, size: 48, color: kMuted), const SizedBox(height: 12), Text('Keranjang kosong', style: TextStyle(color: kMuted)), ], ), ), )); } else { for (var i = 0; i < items.length; i++) { final line = items[i]; children.add(Padding( padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), child: Row( children: [ CircleAvatar( radius: 18, backgroundColor: kAccent.withValues(alpha: 0.15), child: Text( line.product.name.isNotEmpty ? line.product.name[0].toUpperCase() : '?', style: TextStyle( color: kAccent, fontWeight: FontWeight.w700, fontSize: 13), ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(line.product.name, maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600)), const SizedBox(height: 2), Text( '${formatRupiah(line.price)} /pcs', style: const TextStyle(fontSize: 11, color: kMuted), ), ], ), ), if (state.resellerEnabled) InkWell( onTap: () => state.toggleLineMode(line.product.id), borderRadius: BorderRadius.circular(8), child: Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 4), decoration: BoxDecoration( color: line.isReseller ? kAccent.withValues(alpha: 0.15) : kSurface2, borderRadius: BorderRadius.circular(8), border: Border.all( color: line.isReseller ? kAccent : kBorder, ), ), child: Text( line.isReseller ? 'Reseller' : 'Umum', style: TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: line.isReseller ? kAccent : kMuted), ), ), ), const SizedBox(width: 8), Container( decoration: BoxDecoration( color: kSurface2, borderRadius: BorderRadius.circular(20), border: Border.all(color: kBorder), ), child: Row( children: [ _QtyButton( icon: Icons.remove, onTap: () => state.changeQty(line.product.id, -1), ), SizedBox( width: 32, child: Text('${line.qty}', textAlign: TextAlign.center, style: const TextStyle(fontWeight: FontWeight.w700)), ), _QtyButton( icon: Icons.add, onTap: () => state.changeQty(line.product.id, 1), ), ], ), ), ], ), )); if (i < items.length - 1) { children.add(const Divider(height: 1, indent: 16, endIndent: 16)); } } children.add(const SizedBox(height: 12)); children.add(Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), child: Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('Subtotal', style: TextStyle(color: kMuted)), Text(formatRupiah(state.subtotal), style: const TextStyle(fontWeight: FontWeight.w600)), ], ), const SizedBox(height: 10), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('Diskon', style: TextStyle(color: kMuted)), SizedBox( width: 140, child: TextField( keyboardType: TextInputType.number, textAlign: TextAlign.right, inputFormatters: [RupiahInputFormatter()], style: const TextStyle(fontSize: 14), onChanged: (v) => state.setDiscount((RupiahInputFormatter.getRawValue(v) ?? 0).toDouble()), decoration: const InputDecoration( isDense: true, prefixText: 'Rp ', contentPadding: EdgeInsets.symmetric(horizontal: 10), ), ), ), ], ), const SizedBox(height: 12), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('Total', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w800)), Text(formatRupiah(state.total), style: const TextStyle( fontSize: 18, fontWeight: FontWeight.w800, color: kAccent)), ], ), const SizedBox(height: 14), SizedBox( width: double.infinity, child: FilledButton.icon( onPressed: state.canCheckout ? onPay : null, icon: const Icon(Icons.point_of_sale), label: const Text('Bayar'), style: FilledButton.styleFrom( backgroundColor: kSuccess, foregroundColor: const Color(0xFF06210F), ), ), ), ], ), )); } return SingleChildScrollView( controller: scrollController, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: children, ), ); } } class _Chip extends StatelessWidget { final String label; final VoidCallback onTap; const _Chip(this.label, this.onTap); @override Widget build(BuildContext context) { return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(10), child: Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), decoration: BoxDecoration( color: kSurface2, borderRadius: BorderRadius.circular(10), border: Border.all(color: kBorder), ), child: Text(label, style: const TextStyle( color: kAccent, fontWeight: FontWeight.w600, fontSize: 13)), ), ); } } class _PaymentMethodChip extends StatelessWidget { final String label; final IconData icon; final bool selected; final VoidCallback? onTap; const _PaymentMethodChip({ required this.label, required this.icon, required this.selected, this.onTap, }); @override Widget build(BuildContext context) { return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(12), child: AnimatedContainer( duration: const Duration(milliseconds: 180), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14), decoration: BoxDecoration( color: selected ? kAccent.withValues(alpha: 0.18) : kSurface2, borderRadius: BorderRadius.circular(12), border: Border.all( color: selected ? kAccent : kBorder, width: selected ? 1.8 : 1, ), ), child: Column( children: [ Icon(icon, size: 22, color: selected ? kAccent : kMuted), const SizedBox(height: 6), Text( label, style: TextStyle( fontSize: 12, fontWeight: selected ? FontWeight.w700 : FontWeight.w500, color: selected ? kAccent : kText, ), ), ], ), ), ); } } String _paymentMethodLabel(String method) { switch (method) { case 'transfer': return 'Bank'; case 'qris': return 'QRIS'; case 'split': return 'Split Payment'; default: return 'Tunai'; } } class _ReceiptRow extends StatelessWidget { final String label; final String value; final bool bold; const _ReceiptRow(this.label, this.value, this.bold); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(vertical: 3), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(label, style: TextStyle( color: bold ? kText : kMuted, fontWeight: bold ? FontWeight.w700 : FontWeight.normal)), Text(value, style: TextStyle( fontSize: bold ? 17 : 14, fontWeight: bold ? FontWeight.w800 : FontWeight.w600, color: bold ? kAccent : kText, )), ], ), ); } } class _ProductCard extends StatelessWidget { final Product product; final double displayPrice; final bool isReseller; final VoidCallback onTap; const _ProductCard({ required this.product, required this.displayPrice, required this.isReseller, required this.onTap, }); @override Widget build(BuildContext context) { final out = product.stock <= 0; return AbsorbPointer( absorbing: out, child: Opacity( opacity: out ? 0.5 : 1, child: InkWell( onTap: out ? null : onTap, borderRadius: BorderRadius.circular(kRadius), child: Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: kSurface2, borderRadius: BorderRadius.circular(kRadius), border: Border.all(color: kBorder), boxShadow: const [ BoxShadow( color: Colors.black26, blurRadius: 8, offset: Offset(0, 4), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ CircleAvatar( radius: 18, backgroundColor: kAccent.withValues(alpha: 0.15), child: Text( product.name.isNotEmpty ? product.name[0].toUpperCase() : '?', style: TextStyle( color: kAccent, fontWeight: FontWeight.w700, ), ), ), const Spacer(), Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 3), decoration: BoxDecoration( color: out ? kDanger.withValues(alpha: 0.15) : kSuccess.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(20), ), child: Text( out ? 'Habis' : 'Stok ${product.stock}', style: TextStyle( fontSize: 10, fontWeight: FontWeight.w600, color: out ? kDanger : kSuccess, ), ), ), ], ), const Spacer(), Text( product.name, style: const TextStyle( fontWeight: FontWeight.w600, fontSize: 14), maxLines: 2, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 8), Wrap( spacing: 6, children: [ Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 5), decoration: BoxDecoration( color: kAccent.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(8), ), child: Text( formatRupiah(displayPrice), style: TextStyle( color: kAccent, fontWeight: FontWeight.w800, fontSize: 13, ), ), ), if (isReseller) ...[ const SizedBox(width: 6), Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2), decoration: BoxDecoration( color: kAccent.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(6), ), child: const Text('Reseller', style: TextStyle( fontSize: 9, fontWeight: FontWeight.w700, color: kAccent)), ), ], ], ), ], ), ), ), ), ); } } class _QtyButton extends StatelessWidget { final IconData icon; final VoidCallback onTap; const _QtyButton({required this.icon, required this.onTap}); @override Widget build(BuildContext context) { return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(22), child: SizedBox( width: 44, height: 44, child: Icon(icon, size: 20, color: kAccent), ), ); } }