376 lines
11 KiB
Dart
376 lines
11 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:mobile_scanner/mobile_scanner.dart';
|
|
import 'package:firebase_auth/firebase_auth.dart';
|
|
import 'package:firebase_database/firebase_database.dart';
|
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
|
import 'package:intl/intl.dart';
|
|
|
|
class ScanQrScreen extends StatefulWidget {
|
|
const ScanQrScreen({super.key});
|
|
|
|
@override
|
|
State<ScanQrScreen> createState() => _ScanQrScreenState();
|
|
}
|
|
|
|
class _ScanQrScreenState extends State<ScanQrScreen> {
|
|
static const Color primaryColor = Color(0xFFA5CC36);
|
|
|
|
final TextEditingController nominalController = TextEditingController();
|
|
|
|
bool loading = false;
|
|
bool isScanned = false;
|
|
String? qrData;
|
|
|
|
String rupiah(int value) {
|
|
return NumberFormat.currency(
|
|
locale: 'id_ID',
|
|
symbol: 'Rp ',
|
|
decimalDigits: 0,
|
|
).format(value);
|
|
}
|
|
|
|
void addNumber(String number) {
|
|
setState(() {
|
|
nominalController.text += number;
|
|
});
|
|
}
|
|
|
|
void backspace() {
|
|
setState(() {
|
|
if (nominalController.text.isNotEmpty) {
|
|
nominalController.text = nominalController.text.substring(
|
|
0,
|
|
nominalController.text.length - 1,
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
void clearNominal() {
|
|
setState(() {
|
|
nominalController.clear();
|
|
});
|
|
}
|
|
|
|
void showError(String pesan) {
|
|
showDialog(
|
|
context: context,
|
|
builder: (_) => Dialog(
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(25)),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(25),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Container(
|
|
width: 90,
|
|
height: 90,
|
|
decoration: const BoxDecoration(
|
|
color: Colors.red,
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: const Icon(Icons.close, size: 60, color: Colors.white),
|
|
),
|
|
const SizedBox(height: 20),
|
|
const Text(
|
|
"Pembayaran Gagal",
|
|
style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
|
|
),
|
|
const SizedBox(height: 10),
|
|
Text(pesan, textAlign: TextAlign.center),
|
|
const SizedBox(height: 20),
|
|
SizedBox(
|
|
width: double.infinity,
|
|
child: ElevatedButton(
|
|
onPressed: () => Navigator.pop(context),
|
|
child: const Text("OK"),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
void showSuccess(int nominal, int saldoBaru) {
|
|
showDialog(
|
|
context: context,
|
|
barrierDismissible: false,
|
|
builder: (_) => Dialog(
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(30)),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(25),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
TweenAnimationBuilder(
|
|
duration: const Duration(milliseconds: 700),
|
|
tween: Tween(begin: 0.0, end: 1.0),
|
|
builder: (context, value, child) {
|
|
return Transform.scale(
|
|
scale: value,
|
|
child: Container(
|
|
width: 100,
|
|
height: 100,
|
|
decoration: const BoxDecoration(
|
|
color: Color(0xFFA5CC36),
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: const Icon(
|
|
Icons.check,
|
|
size: 65,
|
|
color: Colors.black,
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
const SizedBox(height: 20),
|
|
const Text(
|
|
"Pembayaran Berhasil",
|
|
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
|
|
),
|
|
const SizedBox(height: 10),
|
|
|
|
Text("Nominal pembayaran", textAlign: TextAlign.center),
|
|
|
|
const SizedBox(height: 5),
|
|
|
|
Text(
|
|
rupiah(nominal),
|
|
style: const TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 20,
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 10),
|
|
|
|
Text(
|
|
"Sisa saldo : ${rupiah(saldoBaru)}",
|
|
textAlign: TextAlign.center,
|
|
),
|
|
|
|
const SizedBox(height: 25),
|
|
|
|
SizedBox(
|
|
width: double.infinity,
|
|
child: ElevatedButton(
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: primaryColor,
|
|
foregroundColor: Colors.black,
|
|
),
|
|
onPressed: () {
|
|
Navigator.pop(context);
|
|
Navigator.pop(context);
|
|
},
|
|
child: const Text("SELESAI"),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> prosesPembayaran() async {
|
|
final nominal = int.tryParse(nominalController.text) ?? 0;
|
|
|
|
if (qrData == null) {
|
|
showError("QR belum dipindai");
|
|
return;
|
|
}
|
|
|
|
if (nominal < 1000) {
|
|
showError("Minimal pembayaran Rp1.000");
|
|
return;
|
|
}
|
|
|
|
setState(() => loading = true);
|
|
|
|
try {
|
|
final uid = FirebaseAuth.instance.currentUser!.uid;
|
|
|
|
final firestoreUser = await FirebaseFirestore.instance
|
|
.collection("users")
|
|
.doc(uid)
|
|
.get();
|
|
|
|
final rfid = firestoreUser.data()?["rfid_uid"]?.toString();
|
|
|
|
if (rfid == null || rfid.isEmpty) {
|
|
throw Exception("RFID user tidak ditemukan");
|
|
}
|
|
|
|
final userRef = FirebaseDatabase.instance.ref("users/$rfid");
|
|
|
|
final snapshot = await userRef.get();
|
|
|
|
final data = Map<String, dynamic>.from(snapshot.value as Map);
|
|
|
|
final saldo = (data["saldo"] ?? 0) as int;
|
|
|
|
if (saldo < nominal) {
|
|
showError("Saldo tidak mencukupi");
|
|
return;
|
|
}
|
|
|
|
final saldoBaru = saldo - nominal;
|
|
|
|
await userRef.update({"saldo": saldoBaru});
|
|
|
|
await userRef.child("history").push().set({
|
|
"type": "Pembayaran QRIS",
|
|
"nilai": nominal,
|
|
"waktu": DateTime.now().toIso8601String(),
|
|
"saldo_sebelum": saldo,
|
|
"saldo_sesudah": saldoBaru,
|
|
"qr_data": qrData,
|
|
});
|
|
|
|
if (mounted) {
|
|
showSuccess(nominal, saldoBaru);
|
|
}
|
|
} catch (e) {
|
|
showError(e.toString());
|
|
} finally {
|
|
if (mounted) {
|
|
setState(() => loading = false);
|
|
}
|
|
}
|
|
}
|
|
|
|
Widget buildKeypad() {
|
|
return Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
for (var row in [
|
|
["1", "2", "3"],
|
|
["4", "5", "6"],
|
|
["7", "8", "9"],
|
|
["C", "0", "⌫"],
|
|
])
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
|
child: Row(
|
|
children: row.map((e) {
|
|
return Expanded(
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 5),
|
|
child: SizedBox(
|
|
height: 60,
|
|
child: ElevatedButton(
|
|
onPressed: () {
|
|
if (e == "C") {
|
|
clearNominal();
|
|
} else if (e == "⌫") {
|
|
backspace();
|
|
} else {
|
|
addNumber(e);
|
|
}
|
|
},
|
|
child: Text(e),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}).toList(),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
backgroundColor: const Color(0xFF0B1600),
|
|
appBar: AppBar(
|
|
title: const Text("Scan QRIS"),
|
|
backgroundColor: primaryColor,
|
|
),
|
|
body: qrData == null
|
|
? Stack(
|
|
children: [
|
|
MobileScanner(
|
|
onDetect: (capture) {
|
|
if (isScanned) return;
|
|
|
|
final code = capture.barcodes.first.rawValue;
|
|
|
|
if (code != null) {
|
|
setState(() {
|
|
isScanned = true;
|
|
qrData = code;
|
|
});
|
|
}
|
|
},
|
|
),
|
|
Center(
|
|
child: Container(
|
|
width: 260,
|
|
height: 260,
|
|
decoration: BoxDecoration(
|
|
border: Border.all(color: primaryColor, width: 4),
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
)
|
|
: Padding(
|
|
padding: const EdgeInsets.all(20),
|
|
child: Column(
|
|
children: [
|
|
const Text(
|
|
"QRIS Berhasil Dipindai",
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 22,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const SizedBox(height: 20),
|
|
Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(15),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF1A1A1A),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Text(
|
|
nominalController.text.isEmpty
|
|
? "Rp 0"
|
|
: "Rp ${nominalController.text}",
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 28,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 20),
|
|
Expanded(child: buildKeypad()),
|
|
SizedBox(
|
|
width: double.infinity,
|
|
height: 55,
|
|
child: ElevatedButton(
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: primaryColor,
|
|
foregroundColor: Colors.black,
|
|
),
|
|
onPressed: loading ? null : prosesPembayaran,
|
|
child: loading
|
|
? const CircularProgressIndicator()
|
|
: const Text("Bayar"),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|