From 631066024cd5e9c9be4cabbe7391430377305398 Mon Sep 17 00:00:00 2001 From: a2nr Date: Sat, 27 Dec 2025 15:45:41 +0700 Subject: [PATCH] update fitur pembayaran dan tip. --- lib/main.dart | 1 + lib/models/local_receipt.dart | 12 ++ lib/providers/receipt_provider.dart | 196 ++++++++++++++++++++ lib/providers/receipt_state.dart | 18 +- lib/screens/local_receipts_screen.dart | 68 +++---- lib/screens/receipt_screen.dart | 141 +++++++------- lib/services/local_receipt_service.dart | 7 +- lib/services/receipt_service.dart | 8 +- lib/widgets/receipt_body.dart | 110 +++++++---- lib/widgets/receipt_total.dart | 237 +++++++++++++++++++++--- 10 files changed, 620 insertions(+), 178 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 90f4fd5..c64350a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -34,6 +34,7 @@ class MyApp extends StatelessWidget { initialRoute: '/', routes: { '/': (context) => const ReceiptScreen(), + '/receipt': (context) => const ReceiptScreen(), '/transaction': (context) => const TransactionScreen(), '/config': (context) => const ConfigScreen(), '/local-receipts': (context) => const LocalReceiptsScreen(), diff --git a/lib/models/local_receipt.dart b/lib/models/local_receipt.dart index 6de12b8..3895d78 100644 --- a/lib/models/local_receipt.dart +++ b/lib/models/local_receipt.dart @@ -16,6 +16,8 @@ class LocalReceipt { final String? submissionError; final DateTime? submittedAt; final DateTime createdAt; + final double paymentAmount; + final bool isTip; LocalReceipt({ required this.id, @@ -32,6 +34,8 @@ class LocalReceipt { this.submissionError, this.submittedAt, required this.createdAt, + this.paymentAmount = 0.0, + this.isTip = false, }); double get total => items.fold(0.0, (sum, item) => sum + item.total); @@ -52,6 +56,8 @@ class LocalReceipt { 'submissionError': submissionError, 'submittedAt': submittedAt?.toIso8601String(), 'createdAt': createdAt.toIso8601String(), + 'paymentAmount': paymentAmount, + 'isTip': isTip, }; } @@ -75,6 +81,8 @@ class LocalReceipt { ? DateTime.parse(json['submittedAt']) : null, createdAt: DateTime.parse(json['createdAt']), + paymentAmount: json['paymentAmount']?.toDouble() ?? 0.0, + isTip: json['isTip'] ?? false, ); } @@ -93,6 +101,8 @@ class LocalReceipt { String? submissionError, DateTime? submittedAt, DateTime? createdAt, + double? paymentAmount, + bool? isTip, }) { return LocalReceipt( id: id ?? this.id, @@ -112,6 +122,8 @@ class LocalReceipt { submissionError: submissionError ?? this.submissionError, submittedAt: submittedAt ?? this.submittedAt, createdAt: createdAt ?? this.createdAt, + paymentAmount: paymentAmount ?? this.paymentAmount, + isTip: isTip ?? this.isTip, ); } } diff --git a/lib/providers/receipt_provider.dart b/lib/providers/receipt_provider.dart index b72f9d1..67c5a82 100644 --- a/lib/providers/receipt_provider.dart +++ b/lib/providers/receipt_provider.dart @@ -215,6 +215,8 @@ class ReceiptProvider with ChangeNotifier { accounts: _state.accounts, baseUrl: _state.fireflyUrl!, accessToken: _state.accessToken!, + paymentAmount: _state.paymentAmount, + isTip: _state.isTip, ); return transactionId; @@ -249,6 +251,8 @@ class ReceiptProvider with ChangeNotifier { transactionDate: _state.transactionDate, transactionDescription: transactionDescription, createdAt: DateTime.now(), + paymentAmount: _state.paymentAmount, + isTip: _state.isTip, ); // Simpan ke penyimpanan lokal @@ -261,6 +265,8 @@ class ReceiptProvider with ChangeNotifier { sourceAccountName: null, destinationAccountId: null, destinationAccountName: null, + paymentAmount: 0.0, + isTip: false, ); notifyListeners(); @@ -301,5 +307,195 @@ class ReceiptProvider with ChangeNotifier { return itemNames.join(', '); } + /// Set payment amount + void setPaymentAmount(double amount) { + _state = _state.copyWith( + paymentAmount: amount, + ); + notifyListeners(); + } + + /// Toggle tip status + void toggleTipStatus(bool isTip) { + _state = _state.copyWith( + isTip: isTip, + ); + notifyListeners(); + } + + /// Get change amount + double get changeAmount => _state.changeAmount; + + /// Get payment amount + double get paymentAmount => _state.paymentAmount; + + /// Get tip status + bool get isTip => _state.isTip; + + /// Load receipt data to current state for editing + void loadReceiptForEdit(LocalReceipt receipt) { + _state = _state.copyWith( + items: receipt.items, + sourceAccountId: receipt.sourceAccountId, + sourceAccountName: receipt.sourceAccountName, + destinationAccountId: receipt.destinationAccountId, + destinationAccountName: receipt.destinationAccountName, + transactionDate: receipt.transactionDate, + paymentAmount: receipt.paymentAmount, + isTip: receipt.isTip, + ); + notifyListeners(); + } + + /// Show payment and tip dialog + Future showPaymentTipDialog(BuildContext context) async { + await showDialog( + context: context, + builder: (BuildContext context) { + double localPaymentAmount = _state.paymentAmount; + bool localIsTip = _state.isTip; + + return AlertDialog( + title: const Text('Pembayaran dan Tip'), + content: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Total info + Container( + padding: const EdgeInsets.all(8.0), + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(4), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'TOTAL:', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + Text( + _state.total.toStringAsFixed(0).replaceAllMapped( + RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'), + (Match m) => '${m[1]},'), + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + const SizedBox(height: 16), + TextField( + decoration: const InputDecoration( + labelText: 'Jumlah Bayar', + hintText: 'Masukkan jumlah uang yang dibayar', + labelStyle: TextStyle(fontSize: 14), + hintStyle: TextStyle(fontSize: 14, color: Colors.grey), + ), + keyboardType: TextInputType.number, + style: const TextStyle(fontSize: 14), + controller: TextEditingController( + text: localPaymentAmount > 0 + ? localPaymentAmount.toString() + : '', + ), + onChanged: (value) { + localPaymentAmount = double.tryParse(value) ?? 0.0; + }, + ), + const SizedBox(height: 16), + Row( + children: [ + const Text( + 'Sebagai Tip: ', + style: TextStyle(fontSize: 14), + ), + Switch( + value: localIsTip, + onChanged: (value) { + setState(() { + localIsTip = value; + }); + }, + ), + ], + ), + if (localPaymentAmount > 0 && + localPaymentAmount >= _state.total) + const SizedBox(height: 8), + if (localPaymentAmount > 0 && + localPaymentAmount >= _state.total) + Container( + padding: const EdgeInsets.all(8.0), + decoration: BoxDecoration( + color: (localPaymentAmount - _state.total) >= 0 + ? Colors.green[50] + : Colors.red[50], + borderRadius: BorderRadius.circular(4), + border: Border.all( + color: (localPaymentAmount - _state.total) >= 0 + ? Colors.green + : Colors.red, + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'KEMBALI:', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + Text( + (localPaymentAmount - _state.total) + .toStringAsFixed(0) + .replaceAllMapped( + RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'), + (Match m) => '${m[1]},'), + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: (localPaymentAmount - _state.total) >= 0 + ? Colors.green + : Colors.red, + ), + ), + ], + ), + ), + ], + ); + }, + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('Batal'), + ), + TextButton( + onPressed: () { + setPaymentAmount(localPaymentAmount); + toggleTipStatus(localIsTip); + Navigator.of(context).pop(); + }, + child: const Text('Simpan'), + ), + ], + ); + }, + ); + } + // Tambahkan metode lain sesuai kebutuhan, seperti untuk memperbarui transactionDate jika diperlukan } diff --git a/lib/providers/receipt_state.dart b/lib/providers/receipt_state.dart index c68867c..c042349 100644 --- a/lib/providers/receipt_state.dart +++ b/lib/providers/receipt_state.dart @@ -12,6 +12,8 @@ class ReceiptState { String? destinationAccountName; String? fireflyUrl; String? accessToken; + double paymentAmount; + bool isTip; ReceiptState({ List? items, @@ -23,6 +25,8 @@ class ReceiptState { this.destinationAccountName, this.fireflyUrl, this.accessToken, + this.paymentAmount = 0.0, + this.isTip = false, }) : items = items ?? [], transactionDate = transactionDate ?? DateTime.now(), accounts = accounts ?? []; @@ -38,6 +42,8 @@ class ReceiptState { String? destinationAccountName, String? fireflyUrl, String? accessToken, + double? paymentAmount, + bool? isTip, }) { return ReceiptState( items: items ?? this.items, @@ -46,17 +52,23 @@ class ReceiptState { sourceAccountId: sourceAccountId ?? this.sourceAccountId, sourceAccountName: sourceAccountName ?? this.sourceAccountName, destinationAccountId: destinationAccountId ?? this.destinationAccountId, - destinationAccountName: destinationAccountName ?? this.destinationAccountName, + destinationAccountName: + destinationAccountName ?? this.destinationAccountName, fireflyUrl: fireflyUrl ?? this.fireflyUrl, accessToken: accessToken ?? this.accessToken, + paymentAmount: paymentAmount ?? this.paymentAmount, + isTip: isTip ?? this.isTip, ); } // Method untuk menghitung total double get total => items.fold(0.0, (sum, item) => sum + item.total); + // Method untuk menghitung kembalian + double get changeAmount => paymentAmount - total; + @override String toString() { - return 'ReceiptState(items: $items, transactionDate: $transactionDate, accounts: $accounts, sourceAccountId: $sourceAccountId, sourceAccountName: $sourceAccountName, destinationAccountId: $destinationAccountId, destinationAccountName: $destinationAccountName, fireflyUrl: $fireflyUrl, accessToken: $accessToken)'; + return 'ReceiptState(items: $items, transactionDate: $transactionDate, accounts: $accounts, sourceAccountId: $sourceAccountId, sourceAccountName: $sourceAccountName, destinationAccountId: $destinationAccountId, destinationAccountName: $destinationAccountName, fireflyUrl: $fireflyUrl, accessToken: $accessToken, paymentAmount: $paymentAmount, isTip: $isTip)'; } -} \ No newline at end of file +} diff --git a/lib/screens/local_receipts_screen.dart b/lib/screens/local_receipts_screen.dart index 550586f..57daa53 100644 --- a/lib/screens/local_receipts_screen.dart +++ b/lib/screens/local_receipts_screen.dart @@ -4,6 +4,8 @@ import 'package:cashumit/services/local_receipt_service.dart'; import 'package:cashumit/services/receipt_service.dart'; import 'package:cashumit/screens/webview_screen.dart'; import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; +import 'package:cashumit/providers/receipt_provider.dart'; class LocalReceiptsScreen extends StatefulWidget { const LocalReceiptsScreen({super.key}); @@ -217,23 +219,6 @@ class _LocalReceiptsScreenState extends State { ), ], ), - const SizedBox(height: 16), - ElevatedButton.icon( - onPressed: isSubmitting ? null : _submitAllReceipts, - icon: isSubmitting - ? const SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: - AlwaysStoppedAnimation(Colors.white), - ), - ) - : const Icon(Icons.sync), - label: - Text(isSubmitting ? 'Mengirim...' : 'Kirim Semua Nota'), - ), ], ), ), @@ -368,22 +353,24 @@ class _LocalReceiptsScreenState extends State { ), ], ), - onTap: receipt.isSubmitted && - receipt.fireflyTransactionUrl != null - ? () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - WebViewScreen( - url: receipt - .fireflyTransactionUrl!, - title: 'Detail Transaksi', - ), - ), - ); - } - : null, + onTap: () { + if (receipt.isSubmitted && + receipt.fireflyTransactionUrl != null) { + // Jika sudah dikirim, tampilkan di webview + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => WebViewScreen( + url: receipt.fireflyTransactionUrl!, + title: 'Detail Transaksi', + ), + ), + ); + } else { + // Jika belum dikirim, edit nota + _editReceipt(receipt); + } + }, tileColor: receipt.isSubmitted && receipt.fireflyTransactionUrl != null ? Colors.blue.shade50 @@ -401,4 +388,19 @@ class _LocalReceiptsScreenState extends State { } // Fungsi _showDeleteConfirmation dihapus karena sudah menggunakan swipe gesture + + Future _editReceipt(LocalReceipt receipt) async { + // Navigate ke receipt screen dan muat data receipt + final receiptProvider = context.read(); + receiptProvider.loadReceiptForEdit(receipt); + + // Hapus receipt dari daftar sebelum navigasi + await LocalReceiptService.removeReceipt(receipt.id); + + // Navigasi ke receipt screen + await Navigator.pushNamed(context, '/receipt'); + + // Refresh daftar setelah kembali dari edit + await _loadReceipts(); + } } diff --git a/lib/screens/receipt_screen.dart b/lib/screens/receipt_screen.dart index 07700bf..63c5974 100644 --- a/lib/screens/receipt_screen.dart +++ b/lib/screens/receipt_screen.dart @@ -364,55 +364,51 @@ class _ReceiptScreenState extends State { @override Widget build(BuildContext context) { - return Stack( - children: [ - Scaffold( - backgroundColor: - Colors.grey[300], // Latar belakang abu-abu untuk efek struk - floatingActionButton: Consumer( - builder: (context, receiptProvider, child) { - final state = receiptProvider.state; - return ReceiptSpeedDial( - bluetoothService: _bluetoothService, - onCheckConnection: _checkBluetoothConnection, - onPrint: _printToThermalPrinter, - onSettings: _openSettings, - onReloadAccounts: receiptProvider.loadAccounts, - hasItems: state.items.isNotEmpty, - hasSourceAccount: state.sourceAccountId != null, - hasDestinationAccount: state.destinationAccountId != null, - onSendToFirefly: _sendToFirefly, - onPrintingStart: _startPrinting, - onPrintingEnd: _endPrinting, - onOpenLocalReceipts: _openLocalReceipts, - ); - }), - body: SafeArea( - child: Center( + return Consumer( + builder: (context, receiptProvider, child) { + final state = receiptProvider.state; + + return Scaffold( + backgroundColor: + Colors.grey[300], // Latar belakang abu-abu untuk efek struk + floatingActionButton: ReceiptSpeedDial( + bluetoothService: _bluetoothService, + onCheckConnection: _checkBluetoothConnection, + onPrint: _printToThermalPrinter, + onSettings: _openSettings, + onReloadAccounts: receiptProvider.loadAccounts, + hasItems: state.items.isNotEmpty, + hasSourceAccount: state.sourceAccountId != null, + hasDestinationAccount: state.destinationAccountId != null, + onSendToFirefly: _sendToFirefly, + onPrintingStart: _startPrinting, + onPrintingEnd: _endPrinting, + onOpenLocalReceipts: _openLocalReceipts, + ), + body: Stack( + children: [ + Center( // Membungkus dengan widget Center untuk memastikan struk berada di tengah child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment - .center, // Memusatkan konten secara horizontal - children: [ - // Background untuk efek kertas struk tersobek di bagian atas - Container( - width: 360, - color: const Color( - 0xFFE0E0E0), // Warna latar belakang sesuai dengan latar belakang layar - child: const Column( - children: [ - SizedBox(height: 15), // Jarak atas yang lebih besar - ReceiptTearTop(), // Efek kertas struk tersobek di bagian atas - ], + child: Container( + width: 360, + child: Column( + crossAxisAlignment: CrossAxisAlignment + .center, // Memusatkan konten secara horizontal + children: [ + // Bagian atas - Tear effect + Container( + color: const Color(0xFFE0E0E0), + child: const Column( + children: [ + SizedBox(height: 15), + ReceiptTearTop(), + ], + ), ), - ), - // Konten struk - Consumer( - builder: (context, receiptProvider, child) { - final state = receiptProvider.state; - return ReceiptBody( + // Bagian tengah - Receipt body + ReceiptBody( items: state.items, sourceAccountName: state.sourceAccountName, destinationAccountName: state.destinationAccountName, @@ -442,36 +438,39 @@ class _ReceiptScreenState extends State { }, onAddItem: () => _addItem(), // Memanggil fungsi langsung - ); - }), - - // Background untuk efek kertas struk tersobek di bagian bawah - Container( - width: 360, - color: const Color( - 0xFFE0E0E0), // Warna latar belakang sesuai dengan latar belakang layar - child: const Column( - children: [ - ReceiptTearBottom(), // Efek kertas struk tersobek di bagian bawah - SizedBox(height: 15), // Jarak bawah yang lebih besar - ], ), - ), - ], + + // Bagian bawah - Tear effect + Container( + color: const Color(0xFFE0E0E0), + child: const Column( + children: [ + ReceiptTearBottom(), + SizedBox(height: 15), + ], + ), + ), + ], + ), ), ), ), - ), + if (_isPrinting) + Positioned( + top: 50, + right: 16, + child: PrintingStatusCard( + isVisible: _isPrinting, + onDismiss: () { + setState(() { + _isPrinting = false; + }); + }, + ), + ), + ], ), - PrintingStatusCard( - isVisible: _isPrinting, - onDismiss: () { - setState(() { - _isPrinting = false; - }); - }, - ), - ], - ); + ); + }); } } diff --git a/lib/services/local_receipt_service.dart b/lib/services/local_receipt_service.dart index 8c30d93..534d945 100644 --- a/lib/services/local_receipt_service.dart +++ b/lib/services/local_receipt_service.dart @@ -212,6 +212,11 @@ class LocalReceiptService { required String accessToken, }) async { try { + // Gunakan payment amount jika dijadikan tip, otherwise gunakan total item + final transactionAmount = receipt.isTip && receipt.paymentAmount > 0 + ? receipt.paymentAmount + : receipt.total; + final transactionId = await FireflyApiService.submitDummyTransaction( baseUrl: baseUrl, accessToken: accessToken, @@ -222,7 +227,7 @@ class LocalReceiptService { _generateTransactionDescription(receipt.items), date: '${receipt.transactionDate.year}-${receipt.transactionDate.month.toString().padLeft(2, '0')}-${receipt.transactionDate.day.toString().padLeft(2, '0')}', - amount: receipt.total.toStringAsFixed(2), + amount: transactionAmount.toStringAsFixed(2), ); // Jika transaksi berhasil dikirim, simpan transactionId dan URL diff --git a/lib/services/receipt_service.dart b/lib/services/receipt_service.dart index 7263947..62da99b 100644 --- a/lib/services/receipt_service.dart +++ b/lib/services/receipt_service.dart @@ -136,6 +136,8 @@ class ReceiptService { required List> accounts, // Daftar akun yang dimuat required String baseUrl, required String accessToken, + double paymentAmount = 0.0, + bool isTip = false, }) async { if (items.isEmpty) { throw Exception('Tidak ada item untuk dikirim'); @@ -191,6 +193,10 @@ class ReceiptService { final total = _calculateTotal(items); + // Gunakan payment amount jika dijadikan tip, otherwise gunakan total item + final transactionAmount = + isTip && paymentAmount > 0 ? paymentAmount : total; + // Generate transaction description final transactionDescription = generateTransactionDescription(items); @@ -203,7 +209,7 @@ class ReceiptService { description: transactionDescription, date: '${transactionDate.year}-${transactionDate.month.toString().padLeft(2, '0')}-${transactionDate.day.toString().padLeft(2, '0')}', - amount: total.toStringAsFixed(2), + amount: transactionAmount.toStringAsFixed(2), ); return transactionId; diff --git a/lib/widgets/receipt_body.dart b/lib/widgets/receipt_body.dart index d4134d6..7bd0cef 100644 --- a/lib/widgets/receipt_body.dart +++ b/lib/widgets/receipt_body.dart @@ -44,46 +44,76 @@ class ReceiptBody extends StatelessWidget { decoration: const BoxDecoration( color: Colors.white, ), - child: Column( - children: [ - // Informasi toko dengan widget yang dapat diedit - StoreInfoWidget( - onTap: onOpenStoreInfoConfig, - ), - // Garis pembatas - const DottedLine(), - const SizedBox(height: 8), - // Pengaturan akun sumber dan destinasi - AccountSettingsWidget( - sourceAccount: sourceAccountName ?? 'Pilih Sumber', - destinationAccount: destinationAccountName ?? 'Pilih Tujuan', - onSelectSource: onSelectSourceAccount, - onSelectDestination: onSelectDestinationAccount, - ), - const SizedBox(height: 8), - // Garis pemisah - const HorizontalDivider(), - // Daftar item - ReceiptItemList( - items: items, - onEditItem: onEditItem, - onRemoveItem: onRemoveItem, - onAddItem: onAddItem, - ), - // Garis pembatas - const HorizontalDivider(), - // Total harga keseluruhan - ReceiptTotal(total: total), - const SizedBox(height: 8), - // Garis pembatas - const DottedLine(), - // Disclaimer toko - StoreDisclaimer(onTap: onOpenCustomTextConfig), - // Ucapan terima kasih dan pantun - ThankYouPantun(onTap: onOpenCustomTextConfig), - const SizedBox(height: 16), - ], + child: SingleChildScrollView( + child: Column( + children: [ + // Bagian atas - Header dan akun + Container( + padding: const EdgeInsets.all(8), + child: Column( + children: [ + // Informasi toko dengan widget yang dapat diedit + StoreInfoWidget( + onTap: onOpenStoreInfoConfig, + ), + // Garis pembatas + const DottedLine(), + const SizedBox(height: 8), + // Pengaturan akun sumber dan destinasi + AccountSettingsWidget( + sourceAccount: sourceAccountName ?? 'Pilih Sumber', + destinationAccount: + destinationAccountName ?? 'Pilih Tujuan', + onSelectSource: onSelectSourceAccount, + onSelectDestination: onSelectDestinationAccount, + ), + const SizedBox(height: 8), + // Garis pemisah + const HorizontalDivider(), + ], + ), + ), + + // Bagian tengah - Item dan total + Container( + padding: const EdgeInsets.all(8), + child: Column( + children: [ + // Daftar item + ReceiptItemList( + items: items, + onEditItem: onEditItem, + onRemoveItem: onRemoveItem, + onAddItem: onAddItem, + ), + // Garis pembatas + const HorizontalDivider(), + // Total harga keseluruhan + ReceiptTotal( + total: total, + ), + ], + ), + ), + + // Bagian bawah - Footer + Container( + padding: const EdgeInsets.all(8), + child: Column( + children: [ + // Garis pembatas + const DottedLine(), + // Disclaimer toko + StoreDisclaimer(onTap: onOpenCustomTextConfig), + // Ucapan terima kasih dan pantun + ThankYouPantun(onTap: onOpenCustomTextConfig), + const SizedBox(height: 16), + ], + ), + ), + ], + ), ), ); } -} \ No newline at end of file +} diff --git a/lib/widgets/receipt_total.dart b/lib/widgets/receipt_total.dart index 17cf3f8..3696502 100644 --- a/lib/widgets/receipt_total.dart +++ b/lib/widgets/receipt_total.dart @@ -1,47 +1,226 @@ import 'package:flutter/material.dart'; -import 'package:cashumit/extensions/double_extensions.dart'; +import 'package:provider/provider.dart'; +import 'package:cashumit/providers/receipt_provider.dart'; class ReceiptTotal extends StatelessWidget { final double total; - const ReceiptTotal({Key? key, required this.total}) : super(key: key); + const ReceiptTotal({ + Key? key, + required this.total, + }) : super(key: key); @override Widget build(BuildContext context) { + final receiptProvider = Provider.of(context); + final state = receiptProvider.state; + double changeAmount = state.paymentAmount - total; + bool hasSufficientPayment = state.paymentAmount >= total; + return Container( width: double.infinity, padding: const EdgeInsets.all(8.0), color: Colors.white, - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Expanded( - flex: 4, - child: Text( - 'TOTAL:', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, + child: SingleChildScrollView( + child: Column( + children: [ + // Total - Tap to open payment dialog + GestureDetector( + onTap: () { + // Call the dialog from provider + receiptProvider.showPaymentTipDialog(context); + }, + child: Container( + padding: const EdgeInsets.all(8.0), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey[300]!), + borderRadius: BorderRadius.circular(4), + color: Colors.grey[50], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Expanded( + flex: 4, + child: Text( + 'TOTAL:', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + fontFamily: 'Courier', + ), + ), + ), + Expanded( + flex: 4, + child: Text( + total.toStringAsFixed(0).replaceAllMapped( + RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'), + (Match m) => '${m[1]},'), + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + fontFamily: 'Courier', + ), + textAlign: TextAlign.right, + ), + ), + ], + ), + ), + ), + const SizedBox(height: 8), + + // Payment Amount Display + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + flex: 4, + child: Text( + 'BAYAR:', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.grey[700], + fontFamily: 'Courier', + ), + ), + ), + Expanded( + flex: 4, + child: Text( + state.paymentAmount > 0 + ? state.paymentAmount + .toStringAsFixed(0) + .replaceAllMapped( + RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'), + (Match m) => '${m[1]},') + : '0', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.grey[700], + fontFamily: 'Courier', + ), + textAlign: TextAlign.right, + ), + ), + ], + ), + const SizedBox(height: 8), + + // Change Amount + if (state.paymentAmount > 0) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + flex: 4, + child: Text( + 'KEMBALI:', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: hasSufficientPayment + ? Colors.grey[700] + : Colors.red, + fontFamily: 'Courier', + ), + ), + ), + Expanded( + flex: 4, + child: Text( + changeAmount >= 0 + ? changeAmount.toStringAsFixed(0).replaceAllMapped( + RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'), + (Match m) => '${m[1]},') + : (changeAmount * -1) + .toStringAsFixed(0) + .replaceAllMapped( + RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'), + (Match m) => '${m[1]},'), + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: hasSufficientPayment && changeAmount >= 0 + ? Colors.green + : Colors.red, + fontFamily: 'Courier', + ), + textAlign: TextAlign.right, + ), + ), + ], + ), + + // Tip Status + if (state.paymentAmount > 0 && + state.paymentAmount >= total && + changeAmount > 0) + Padding( + padding: const EdgeInsets.only(top: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + flex: 4, + child: Text( + 'SEBAGAI TIP:', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.grey[700], + fontFamily: 'Courier', + ), + ), + ), + Expanded( + flex: 4, + child: Align( + alignment: Alignment.centerRight, + child: Text( + state.isTip ? 'Ya' : 'Tidak', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: state.isTip ? Colors.orange : Colors.grey, + fontFamily: 'Courier', + ), + ), + ), + ), + ], + ), + ), + + // Tip Indicator + if (state.isTip && changeAmount > 0) + Padding( + padding: const EdgeInsets.only(top: 4), + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: Colors.orange[100], + borderRadius: BorderRadius.circular(4), + border: Border.all(color: Colors.orange), + ), + child: Text( + 'Uang kembalian sebesar ${changeAmount.abs().toStringAsFixed(0).replaceAllMapped(RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'), (Match m) => '${m[1]},')} sebagai tip untuk toko', + style: TextStyle( + fontSize: 12, + color: Colors.orange[800], + fontStyle: FontStyle.italic, + fontFamily: 'Courier', + ), + textAlign: TextAlign.center, ), ), ), - Expanded( - flex: 4, - child: Text( - total.toRupiah(), - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - textAlign: TextAlign.right, - ), - ), - ], - ), - ], + ], + ), ), ); } -} \ No newline at end of file +}