update fitur pembayaran dan tip.

master
a2nr 2025-12-27 15:45:41 +07:00
parent 4dbf31db3c
commit 631066024c
10 changed files with 620 additions and 178 deletions

View File

@ -34,6 +34,7 @@ class MyApp extends StatelessWidget {
initialRoute: '/', initialRoute: '/',
routes: { routes: {
'/': (context) => const ReceiptScreen(), '/': (context) => const ReceiptScreen(),
'/receipt': (context) => const ReceiptScreen(),
'/transaction': (context) => const TransactionScreen(), '/transaction': (context) => const TransactionScreen(),
'/config': (context) => const ConfigScreen(), '/config': (context) => const ConfigScreen(),
'/local-receipts': (context) => const LocalReceiptsScreen(), '/local-receipts': (context) => const LocalReceiptsScreen(),

View File

@ -16,6 +16,8 @@ class LocalReceipt {
final String? submissionError; final String? submissionError;
final DateTime? submittedAt; final DateTime? submittedAt;
final DateTime createdAt; final DateTime createdAt;
final double paymentAmount;
final bool isTip;
LocalReceipt({ LocalReceipt({
required this.id, required this.id,
@ -32,6 +34,8 @@ class LocalReceipt {
this.submissionError, this.submissionError,
this.submittedAt, this.submittedAt,
required this.createdAt, required this.createdAt,
this.paymentAmount = 0.0,
this.isTip = false,
}); });
double get total => items.fold(0.0, (sum, item) => sum + item.total); double get total => items.fold(0.0, (sum, item) => sum + item.total);
@ -52,6 +56,8 @@ class LocalReceipt {
'submissionError': submissionError, 'submissionError': submissionError,
'submittedAt': submittedAt?.toIso8601String(), 'submittedAt': submittedAt?.toIso8601String(),
'createdAt': createdAt.toIso8601String(), 'createdAt': createdAt.toIso8601String(),
'paymentAmount': paymentAmount,
'isTip': isTip,
}; };
} }
@ -75,6 +81,8 @@ class LocalReceipt {
? DateTime.parse(json['submittedAt']) ? DateTime.parse(json['submittedAt'])
: null, : null,
createdAt: DateTime.parse(json['createdAt']), createdAt: DateTime.parse(json['createdAt']),
paymentAmount: json['paymentAmount']?.toDouble() ?? 0.0,
isTip: json['isTip'] ?? false,
); );
} }
@ -93,6 +101,8 @@ class LocalReceipt {
String? submissionError, String? submissionError,
DateTime? submittedAt, DateTime? submittedAt,
DateTime? createdAt, DateTime? createdAt,
double? paymentAmount,
bool? isTip,
}) { }) {
return LocalReceipt( return LocalReceipt(
id: id ?? this.id, id: id ?? this.id,
@ -112,6 +122,8 @@ class LocalReceipt {
submissionError: submissionError ?? this.submissionError, submissionError: submissionError ?? this.submissionError,
submittedAt: submittedAt ?? this.submittedAt, submittedAt: submittedAt ?? this.submittedAt,
createdAt: createdAt ?? this.createdAt, createdAt: createdAt ?? this.createdAt,
paymentAmount: paymentAmount ?? this.paymentAmount,
isTip: isTip ?? this.isTip,
); );
} }
} }

View File

@ -215,6 +215,8 @@ class ReceiptProvider with ChangeNotifier {
accounts: _state.accounts, accounts: _state.accounts,
baseUrl: _state.fireflyUrl!, baseUrl: _state.fireflyUrl!,
accessToken: _state.accessToken!, accessToken: _state.accessToken!,
paymentAmount: _state.paymentAmount,
isTip: _state.isTip,
); );
return transactionId; return transactionId;
@ -249,6 +251,8 @@ class ReceiptProvider with ChangeNotifier {
transactionDate: _state.transactionDate, transactionDate: _state.transactionDate,
transactionDescription: transactionDescription, transactionDescription: transactionDescription,
createdAt: DateTime.now(), createdAt: DateTime.now(),
paymentAmount: _state.paymentAmount,
isTip: _state.isTip,
); );
// Simpan ke penyimpanan lokal // Simpan ke penyimpanan lokal
@ -261,6 +265,8 @@ class ReceiptProvider with ChangeNotifier {
sourceAccountName: null, sourceAccountName: null,
destinationAccountId: null, destinationAccountId: null,
destinationAccountName: null, destinationAccountName: null,
paymentAmount: 0.0,
isTip: false,
); );
notifyListeners(); notifyListeners();
@ -301,5 +307,195 @@ class ReceiptProvider with ChangeNotifier {
return itemNames.join(', '); 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<void> 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 // Tambahkan metode lain sesuai kebutuhan, seperti untuk memperbarui transactionDate jika diperlukan
} }

View File

@ -12,6 +12,8 @@ class ReceiptState {
String? destinationAccountName; String? destinationAccountName;
String? fireflyUrl; String? fireflyUrl;
String? accessToken; String? accessToken;
double paymentAmount;
bool isTip;
ReceiptState({ ReceiptState({
List<ReceiptItem>? items, List<ReceiptItem>? items,
@ -23,6 +25,8 @@ class ReceiptState {
this.destinationAccountName, this.destinationAccountName,
this.fireflyUrl, this.fireflyUrl,
this.accessToken, this.accessToken,
this.paymentAmount = 0.0,
this.isTip = false,
}) : items = items ?? [], }) : items = items ?? [],
transactionDate = transactionDate ?? DateTime.now(), transactionDate = transactionDate ?? DateTime.now(),
accounts = accounts ?? []; accounts = accounts ?? [];
@ -38,6 +42,8 @@ class ReceiptState {
String? destinationAccountName, String? destinationAccountName,
String? fireflyUrl, String? fireflyUrl,
String? accessToken, String? accessToken,
double? paymentAmount,
bool? isTip,
}) { }) {
return ReceiptState( return ReceiptState(
items: items ?? this.items, items: items ?? this.items,
@ -46,17 +52,23 @@ class ReceiptState {
sourceAccountId: sourceAccountId ?? this.sourceAccountId, sourceAccountId: sourceAccountId ?? this.sourceAccountId,
sourceAccountName: sourceAccountName ?? this.sourceAccountName, sourceAccountName: sourceAccountName ?? this.sourceAccountName,
destinationAccountId: destinationAccountId ?? this.destinationAccountId, destinationAccountId: destinationAccountId ?? this.destinationAccountId,
destinationAccountName: destinationAccountName ?? this.destinationAccountName, destinationAccountName:
destinationAccountName ?? this.destinationAccountName,
fireflyUrl: fireflyUrl ?? this.fireflyUrl, fireflyUrl: fireflyUrl ?? this.fireflyUrl,
accessToken: accessToken ?? this.accessToken, accessToken: accessToken ?? this.accessToken,
paymentAmount: paymentAmount ?? this.paymentAmount,
isTip: isTip ?? this.isTip,
); );
} }
// Method untuk menghitung total // Method untuk menghitung total
double get total => items.fold(0.0, (sum, item) => sum + item.total); double get total => items.fold(0.0, (sum, item) => sum + item.total);
// Method untuk menghitung kembalian
double get changeAmount => paymentAmount - total;
@override @override
String toString() { 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)';
} }
} }

View File

@ -4,6 +4,8 @@ import 'package:cashumit/services/local_receipt_service.dart';
import 'package:cashumit/services/receipt_service.dart'; import 'package:cashumit/services/receipt_service.dart';
import 'package:cashumit/screens/webview_screen.dart'; import 'package:cashumit/screens/webview_screen.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import 'package:cashumit/providers/receipt_provider.dart';
class LocalReceiptsScreen extends StatefulWidget { class LocalReceiptsScreen extends StatefulWidget {
const LocalReceiptsScreen({super.key}); const LocalReceiptsScreen({super.key});
@ -217,23 +219,6 @@ class _LocalReceiptsScreenState extends State<LocalReceiptsScreen> {
), ),
], ],
), ),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: isSubmitting ? null : _submitAllReceipts,
icon: isSubmitting
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor:
AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: const Icon(Icons.sync),
label:
Text(isSubmitting ? 'Mengirim...' : 'Kirim Semua Nota'),
),
], ],
), ),
), ),
@ -368,22 +353,24 @@ class _LocalReceiptsScreenState extends State<LocalReceiptsScreen> {
), ),
], ],
), ),
onTap: receipt.isSubmitted && onTap: () {
receipt.fireflyTransactionUrl != null if (receipt.isSubmitted &&
? () { receipt.fireflyTransactionUrl != null) {
Navigator.push( // Jika sudah dikirim, tampilkan di webview
context, Navigator.push(
MaterialPageRoute( context,
builder: (context) => MaterialPageRoute(
WebViewScreen( builder: (context) => WebViewScreen(
url: receipt url: receipt.fireflyTransactionUrl!,
.fireflyTransactionUrl!, title: 'Detail Transaksi',
title: 'Detail Transaksi', ),
), ),
), );
); } else {
} // Jika belum dikirim, edit nota
: null, _editReceipt(receipt);
}
},
tileColor: receipt.isSubmitted && tileColor: receipt.isSubmitted &&
receipt.fireflyTransactionUrl != null receipt.fireflyTransactionUrl != null
? Colors.blue.shade50 ? Colors.blue.shade50
@ -401,4 +388,19 @@ class _LocalReceiptsScreenState extends State<LocalReceiptsScreen> {
} }
// Fungsi _showDeleteConfirmation dihapus karena sudah menggunakan swipe gesture // Fungsi _showDeleteConfirmation dihapus karena sudah menggunakan swipe gesture
Future<void> _editReceipt(LocalReceipt receipt) async {
// Navigate ke receipt screen dan muat data receipt
final receiptProvider = context.read<ReceiptProvider>();
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();
}
} }

View File

@ -364,55 +364,51 @@ class _ReceiptScreenState extends State<ReceiptScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Stack( return Consumer<ReceiptProvider>(
children: [ builder: (context, receiptProvider, child) {
Scaffold( final state = receiptProvider.state;
backgroundColor:
Colors.grey[300], // Latar belakang abu-abu untuk efek struk return Scaffold(
floatingActionButton: Consumer<ReceiptProvider>( backgroundColor:
builder: (context, receiptProvider, child) { Colors.grey[300], // Latar belakang abu-abu untuk efek struk
final state = receiptProvider.state; floatingActionButton: ReceiptSpeedDial(
return ReceiptSpeedDial( bluetoothService: _bluetoothService,
bluetoothService: _bluetoothService, onCheckConnection: _checkBluetoothConnection,
onCheckConnection: _checkBluetoothConnection, onPrint: _printToThermalPrinter,
onPrint: _printToThermalPrinter, onSettings: _openSettings,
onSettings: _openSettings, onReloadAccounts: receiptProvider.loadAccounts,
onReloadAccounts: receiptProvider.loadAccounts, hasItems: state.items.isNotEmpty,
hasItems: state.items.isNotEmpty, hasSourceAccount: state.sourceAccountId != null,
hasSourceAccount: state.sourceAccountId != null, hasDestinationAccount: state.destinationAccountId != null,
hasDestinationAccount: state.destinationAccountId != null, onSendToFirefly: _sendToFirefly,
onSendToFirefly: _sendToFirefly, onPrintingStart: _startPrinting,
onPrintingStart: _startPrinting, onPrintingEnd: _endPrinting,
onPrintingEnd: _endPrinting, onOpenLocalReceipts: _openLocalReceipts,
onOpenLocalReceipts: _openLocalReceipts, ),
); body: Stack(
}), children: [
body: SafeArea( Center(
child: Center(
// Membungkus dengan widget Center untuk memastikan struk berada di tengah // Membungkus dengan widget Center untuk memastikan struk berada di tengah
child: SingleChildScrollView( child: SingleChildScrollView(
child: Column( child: Container(
crossAxisAlignment: CrossAxisAlignment width: 360,
.center, // Memusatkan konten secara horizontal child: Column(
children: [ crossAxisAlignment: CrossAxisAlignment
// Background untuk efek kertas struk tersobek di bagian atas .center, // Memusatkan konten secara horizontal
Container( children: [
width: 360, // Bagian atas - Tear effect
color: const Color( Container(
0xFFE0E0E0), // Warna latar belakang sesuai dengan latar belakang layar color: const Color(0xFFE0E0E0),
child: const Column( child: const Column(
children: [ children: [
SizedBox(height: 15), // Jarak atas yang lebih besar SizedBox(height: 15),
ReceiptTearTop(), // Efek kertas struk tersobek di bagian atas ReceiptTearTop(),
], ],
),
), ),
),
// Konten struk // Bagian tengah - Receipt body
Consumer<ReceiptProvider>( ReceiptBody(
builder: (context, receiptProvider, child) {
final state = receiptProvider.state;
return ReceiptBody(
items: state.items, items: state.items,
sourceAccountName: state.sourceAccountName, sourceAccountName: state.sourceAccountName,
destinationAccountName: state.destinationAccountName, destinationAccountName: state.destinationAccountName,
@ -442,36 +438,39 @@ class _ReceiptScreenState extends State<ReceiptScreen> {
}, },
onAddItem: () => onAddItem: () =>
_addItem(), // Memanggil fungsi langsung _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;
});
},
),
],
);
} }
} }

View File

@ -212,6 +212,11 @@ class LocalReceiptService {
required String accessToken, required String accessToken,
}) async { }) async {
try { 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( final transactionId = await FireflyApiService.submitDummyTransaction(
baseUrl: baseUrl, baseUrl: baseUrl,
accessToken: accessToken, accessToken: accessToken,
@ -222,7 +227,7 @@ class LocalReceiptService {
_generateTransactionDescription(receipt.items), _generateTransactionDescription(receipt.items),
date: date:
'${receipt.transactionDate.year}-${receipt.transactionDate.month.toString().padLeft(2, '0')}-${receipt.transactionDate.day.toString().padLeft(2, '0')}', '${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 // Jika transaksi berhasil dikirim, simpan transactionId dan URL

View File

@ -136,6 +136,8 @@ class ReceiptService {
required List<Map<String, dynamic>> accounts, // Daftar akun yang dimuat required List<Map<String, dynamic>> accounts, // Daftar akun yang dimuat
required String baseUrl, required String baseUrl,
required String accessToken, required String accessToken,
double paymentAmount = 0.0,
bool isTip = false,
}) async { }) async {
if (items.isEmpty) { if (items.isEmpty) {
throw Exception('Tidak ada item untuk dikirim'); throw Exception('Tidak ada item untuk dikirim');
@ -191,6 +193,10 @@ class ReceiptService {
final total = _calculateTotal(items); final total = _calculateTotal(items);
// Gunakan payment amount jika dijadikan tip, otherwise gunakan total item
final transactionAmount =
isTip && paymentAmount > 0 ? paymentAmount : total;
// Generate transaction description // Generate transaction description
final transactionDescription = generateTransactionDescription(items); final transactionDescription = generateTransactionDescription(items);
@ -203,7 +209,7 @@ class ReceiptService {
description: transactionDescription, description: transactionDescription,
date: date:
'${transactionDate.year}-${transactionDate.month.toString().padLeft(2, '0')}-${transactionDate.day.toString().padLeft(2, '0')}', '${transactionDate.year}-${transactionDate.month.toString().padLeft(2, '0')}-${transactionDate.day.toString().padLeft(2, '0')}',
amount: total.toStringAsFixed(2), amount: transactionAmount.toStringAsFixed(2),
); );
return transactionId; return transactionId;

View File

@ -44,46 +44,76 @@ class ReceiptBody extends StatelessWidget {
decoration: const BoxDecoration( decoration: const BoxDecoration(
color: Colors.white, color: Colors.white,
), ),
child: Column( child: SingleChildScrollView(
children: [ child: Column(
// Informasi toko dengan widget yang dapat diedit children: [
StoreInfoWidget( // Bagian atas - Header dan akun
onTap: onOpenStoreInfoConfig, Container(
), padding: const EdgeInsets.all(8),
// Garis pembatas child: Column(
const DottedLine(), children: [
const SizedBox(height: 8), // Informasi toko dengan widget yang dapat diedit
// Pengaturan akun sumber dan destinasi StoreInfoWidget(
AccountSettingsWidget( onTap: onOpenStoreInfoConfig,
sourceAccount: sourceAccountName ?? 'Pilih Sumber', ),
destinationAccount: destinationAccountName ?? 'Pilih Tujuan', // Garis pembatas
onSelectSource: onSelectSourceAccount, const DottedLine(),
onSelectDestination: onSelectDestinationAccount, const SizedBox(height: 8),
), // Pengaturan akun sumber dan destinasi
const SizedBox(height: 8), AccountSettingsWidget(
// Garis pemisah sourceAccount: sourceAccountName ?? 'Pilih Sumber',
const HorizontalDivider(), destinationAccount:
// Daftar item destinationAccountName ?? 'Pilih Tujuan',
ReceiptItemList( onSelectSource: onSelectSourceAccount,
items: items, onSelectDestination: onSelectDestinationAccount,
onEditItem: onEditItem, ),
onRemoveItem: onRemoveItem, const SizedBox(height: 8),
onAddItem: onAddItem, // Garis pemisah
), const HorizontalDivider(),
// Garis pembatas ],
const HorizontalDivider(), ),
// Total harga keseluruhan ),
ReceiptTotal(total: total),
const SizedBox(height: 8), // Bagian tengah - Item dan total
// Garis pembatas Container(
const DottedLine(), padding: const EdgeInsets.all(8),
// Disclaimer toko child: Column(
StoreDisclaimer(onTap: onOpenCustomTextConfig), children: [
// Ucapan terima kasih dan pantun // Daftar item
ThankYouPantun(onTap: onOpenCustomTextConfig), ReceiptItemList(
const SizedBox(height: 16), 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),
],
),
),
],
),
), ),
); );
} }
} }

View File

@ -1,47 +1,226 @@
import 'package:flutter/material.dart'; 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 { class ReceiptTotal extends StatelessWidget {
final double total; final double total;
const ReceiptTotal({Key? key, required this.total}) : super(key: key); const ReceiptTotal({
Key? key,
required this.total,
}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final receiptProvider = Provider.of<ReceiptProvider>(context);
final state = receiptProvider.state;
double changeAmount = state.paymentAmount - total;
bool hasSufficientPayment = state.paymentAmount >= total;
return Container( return Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
color: Colors.white, color: Colors.white,
child: Column( child: SingleChildScrollView(
children: [ child: Column(
Row( children: [
mainAxisAlignment: MainAxisAlignment.spaceBetween, // Total - Tap to open payment dialog
children: [ GestureDetector(
const Expanded( onTap: () {
flex: 4, // Call the dialog from provider
child: Text( receiptProvider.showPaymentTipDialog(context);
'TOTAL:', },
style: TextStyle( child: Container(
fontSize: 16, padding: const EdgeInsets.all(8.0),
fontWeight: FontWeight.bold, 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,
),
),
],
),
],
), ),
); );
} }
} }