update fitur pembayaran dan tip.
parent
4dbf31db3c
commit
631066024c
|
|
@ -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(),
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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) {
|
||||||
|
// Jika sudah dikirim, tampilkan di webview
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) =>
|
builder: (context) => WebViewScreen(
|
||||||
WebViewScreen(
|
url: receipt.fireflyTransactionUrl!,
|
||||||
url: receipt
|
|
||||||
.fireflyTransactionUrl!,
|
|
||||||
title: 'Detail Transaksi',
|
title: 'Detail Transaksi',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
// Jika belum dikirim, edit nota
|
||||||
|
_editReceipt(receipt);
|
||||||
}
|
}
|
||||||
: null,
|
},
|
||||||
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -364,15 +364,14 @@ class _ReceiptScreenState extends State<ReceiptScreen> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Stack(
|
return Consumer<ReceiptProvider>(
|
||||||
children: [
|
|
||||||
Scaffold(
|
|
||||||
backgroundColor:
|
|
||||||
Colors.grey[300], // Latar belakang abu-abu untuk efek struk
|
|
||||||
floatingActionButton: Consumer<ReceiptProvider>(
|
|
||||||
builder: (context, receiptProvider, child) {
|
builder: (context, receiptProvider, child) {
|
||||||
final state = receiptProvider.state;
|
final state = receiptProvider.state;
|
||||||
return ReceiptSpeedDial(
|
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor:
|
||||||
|
Colors.grey[300], // Latar belakang abu-abu untuk efek struk
|
||||||
|
floatingActionButton: ReceiptSpeedDial(
|
||||||
bluetoothService: _bluetoothService,
|
bluetoothService: _bluetoothService,
|
||||||
onCheckConnection: _checkBluetoothConnection,
|
onCheckConnection: _checkBluetoothConnection,
|
||||||
onPrint: _printToThermalPrinter,
|
onPrint: _printToThermalPrinter,
|
||||||
|
|
@ -385,34 +384,31 @@ class _ReceiptScreenState extends State<ReceiptScreen> {
|
||||||
onPrintingStart: _startPrinting,
|
onPrintingStart: _startPrinting,
|
||||||
onPrintingEnd: _endPrinting,
|
onPrintingEnd: _endPrinting,
|
||||||
onOpenLocalReceipts: _openLocalReceipts,
|
onOpenLocalReceipts: _openLocalReceipts,
|
||||||
);
|
),
|
||||||
}),
|
body: Stack(
|
||||||
body: SafeArea(
|
children: [
|
||||||
child: Center(
|
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: Container(
|
||||||
|
width: 360,
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment
|
crossAxisAlignment: CrossAxisAlignment
|
||||||
.center, // Memusatkan konten secara horizontal
|
.center, // Memusatkan konten secara horizontal
|
||||||
children: [
|
children: [
|
||||||
// Background untuk efek kertas struk tersobek di bagian atas
|
// Bagian atas - Tear effect
|
||||||
Container(
|
Container(
|
||||||
width: 360,
|
color: const Color(0xFFE0E0E0),
|
||||||
color: const Color(
|
|
||||||
0xFFE0E0E0), // Warna latar belakang sesuai dengan latar belakang layar
|
|
||||||
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,18 +438,15 @@ class _ReceiptScreenState extends State<ReceiptScreen> {
|
||||||
},
|
},
|
||||||
onAddItem: () =>
|
onAddItem: () =>
|
||||||
_addItem(), // Memanggil fungsi langsung
|
_addItem(), // Memanggil fungsi langsung
|
||||||
);
|
),
|
||||||
}),
|
|
||||||
|
|
||||||
// Background untuk efek kertas struk tersobek di bagian bawah
|
// Bagian bawah - Tear effect
|
||||||
Container(
|
Container(
|
||||||
width: 360,
|
color: const Color(0xFFE0E0E0),
|
||||||
color: const Color(
|
|
||||||
0xFFE0E0E0), // Warna latar belakang sesuai dengan latar belakang layar
|
|
||||||
child: const Column(
|
child: const Column(
|
||||||
children: [
|
children: [
|
||||||
ReceiptTearBottom(), // Efek kertas struk tersobek di bagian bawah
|
ReceiptTearBottom(),
|
||||||
SizedBox(height: 15), // Jarak bawah yang lebih besar
|
SizedBox(height: 15),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -462,8 +455,11 @@ class _ReceiptScreenState extends State<ReceiptScreen> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
if (_isPrinting)
|
||||||
PrintingStatusCard(
|
Positioned(
|
||||||
|
top: 50,
|
||||||
|
right: 16,
|
||||||
|
child: PrintingStatusCard(
|
||||||
isVisible: _isPrinting,
|
isVisible: _isPrinting,
|
||||||
onDismiss: () {
|
onDismiss: () {
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|
@ -471,7 +467,10 @@ class _ReceiptScreenState extends State<ReceiptScreen> {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,12 @@ class ReceiptBody extends StatelessWidget {
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
),
|
),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// Bagian atas - Header dan akun
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
// Informasi toko dengan widget yang dapat diedit
|
// Informasi toko dengan widget yang dapat diedit
|
||||||
|
|
@ -56,13 +62,23 @@ class ReceiptBody extends StatelessWidget {
|
||||||
// Pengaturan akun sumber dan destinasi
|
// Pengaturan akun sumber dan destinasi
|
||||||
AccountSettingsWidget(
|
AccountSettingsWidget(
|
||||||
sourceAccount: sourceAccountName ?? 'Pilih Sumber',
|
sourceAccount: sourceAccountName ?? 'Pilih Sumber',
|
||||||
destinationAccount: destinationAccountName ?? 'Pilih Tujuan',
|
destinationAccount:
|
||||||
|
destinationAccountName ?? 'Pilih Tujuan',
|
||||||
onSelectSource: onSelectSourceAccount,
|
onSelectSource: onSelectSourceAccount,
|
||||||
onSelectDestination: onSelectDestinationAccount,
|
onSelectDestination: onSelectDestinationAccount,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
// Garis pemisah
|
// Garis pemisah
|
||||||
const HorizontalDivider(),
|
const HorizontalDivider(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Bagian tengah - Item dan total
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
// Daftar item
|
// Daftar item
|
||||||
ReceiptItemList(
|
ReceiptItemList(
|
||||||
items: items,
|
items: items,
|
||||||
|
|
@ -73,8 +89,18 @@ class ReceiptBody extends StatelessWidget {
|
||||||
// Garis pembatas
|
// Garis pembatas
|
||||||
const HorizontalDivider(),
|
const HorizontalDivider(),
|
||||||
// Total harga keseluruhan
|
// Total harga keseluruhan
|
||||||
ReceiptTotal(total: total),
|
ReceiptTotal(
|
||||||
const SizedBox(height: 8),
|
total: total,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Bagian bawah - Footer
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
// Garis pembatas
|
// Garis pembatas
|
||||||
const DottedLine(),
|
const DottedLine(),
|
||||||
// Disclaimer toko
|
// Disclaimer toko
|
||||||
|
|
@ -84,6 +110,10 @@ class ReceiptBody extends StatelessWidget {
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,20 +1,43 @@
|
||||||
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: SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Row(
|
// 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,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
const Expanded(
|
const Expanded(
|
||||||
|
|
@ -24,24 +47,180 @@ class ReceiptTotal extends StatelessWidget {
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
fontFamily: 'Courier',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
flex: 4,
|
flex: 4,
|
||||||
child: Text(
|
child: Text(
|
||||||
total.toRupiah(),
|
total.toStringAsFixed(0).replaceAllMapped(
|
||||||
|
RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'),
|
||||||
|
(Match m) => '${m[1]},'),
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
fontFamily: 'Courier',
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.right,
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue