update fitur pembayaran dan tip.
parent
4dbf31db3c
commit
631066024c
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ class ReceiptState {
|
|||
String? destinationAccountName;
|
||||
String? fireflyUrl;
|
||||
String? accessToken;
|
||||
double paymentAmount;
|
||||
bool isTip;
|
||||
|
||||
ReceiptState({
|
||||
List<ReceiptItem>? 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)';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<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 &&
|
||||
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<LocalReceiptsScreen> {
|
|||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -364,55 +364,51 @@ class _ReceiptScreenState extends State<ReceiptScreen> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
Scaffold(
|
||||
backgroundColor:
|
||||
Colors.grey[300], // Latar belakang abu-abu untuk efek struk
|
||||
floatingActionButton: Consumer<ReceiptProvider>(
|
||||
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<ReceiptProvider>(
|
||||
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<ReceiptProvider>(
|
||||
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<ReceiptScreen> {
|
|||
},
|
||||
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;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -136,6 +136,8 @@ class ReceiptService {
|
|||
required List<Map<String, dynamic>> 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;
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ReceiptProvider>(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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue