package com.example.barokahstore.ui.screens
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import com.example.barokahstore.data.DatabaseHelper
import com.example.barokahstore.data.Product
import com.example.barokahstore.data.User
import com.example.barokahstore.ui.components.BarcodeScannerView
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import android.widget.Toast
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.platform.LocalContext
import android.content.Context
import android.print.PrintAttributes
import android.print.PrintManager
import android.webkit.WebView
import android.webkit.WebViewClient
fun printReceipt(context: Context, receiptText: String, invoiceNo: String) {
val webView = WebView(context)
webView.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
val printManager = context.getSystemService(Context.PRINT_SERVICE) as PrintManager
val printAdapter = webView.createPrintDocumentAdapter("Receipt-$invoiceNo")
val jobName = "Toko Barokah Receipt $invoiceNo"
printManager.print(
jobName,
printAdapter,
PrintAttributes.Builder().build()
)
}
}
val htmlContent = """
${receiptText.replace("\n", "
")}
""".trimIndent()
webView.loadDataWithBaseURL(null, htmlContent, "text/html", "UTF-8", null)
}
fun barcodeMatches(dbBarcode: String, scannedBarcode: String): Boolean {
val cleanDb = dbBarcode.trim()
val cleanScanned = scannedBarcode.trim()
if (cleanDb == cleanScanned) return true
val dbNoZero = cleanDb.removePrefix("0")
val scannedNoZero = cleanScanned.removePrefix("0")
return dbNoZero.isNotEmpty() && dbNoZero == scannedNoZero
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun KasirScreen(
dbHelper: DatabaseHelper,
currentUser: User
) {
// Session State
val userId = currentUser.id
// Search and Autocomplete
var searchQuery by remember { mutableStateOf("") }
var barcodeQuery by remember { mutableStateOf("") }
var autocompleteResults by remember { mutableStateOf(listOf()) }
var allProducts by remember { mutableStateOf(listOf()) }
// Cart State: Product mapped to Quantity
var cartItems by remember { mutableStateOf(mutableStateMapOf()) }
// Discount State
var discountText by remember { mutableStateOf("") }
val discount = discountText.toDoubleOrNull() ?: 0.0
// Checkout States
var showCheckoutDialog by remember { mutableStateOf(false) }
var paymentType by remember { mutableStateOf("cash") } // 'cash' or 'piutang'
var customerName by remember { mutableStateOf("") }
var dueDate by remember { mutableStateOf("") }
var payAmountText by remember { mutableStateOf("") }
var alertMessage by remember { mutableStateOf("") }
// Camera scan toggle state
var showCameraScanner by remember { mutableStateOf(false) }
// Receipt State
var showReceiptDialog by remember { mutableStateOf(false) }
var receiptInvoiceNo by remember { mutableStateOf("") }
var receiptContent by remember { mutableStateOf("") }
val context = LocalContext.current
val focusRequester = remember { FocusRequester() }
// Load initial products list
fun loadProducts() {
allProducts = dbHelper.getAllProducts()
}
// Handlers
val addToCart = { product: Product, qty: Int ->
// Find if product already in cart
val existing = cartItems.keys.find { it.id == product.id }
if (existing != null) {
val currentQty = cartItems[existing] ?: 0
if (currentQty + qty <= product.stock) {
cartItems[existing] = currentQty + qty
}
} else {
if (qty <= product.stock) {
cartItems[product] = qty
}
}
}
val performScanSearch = {
val trimmed = barcodeQuery.trim()
if (trimmed.isNotEmpty()) {
val prod = allProducts.find { p -> barcodeMatches(p.barcode, trimmed) }
if (prod != null) {
addToCart(prod, 1)
barcodeQuery = ""
} else {
Toast.makeText(context, "Produk tidak ditemukan!", Toast.LENGTH_SHORT).show()
barcodeQuery = ""
}
focusRequester.requestFocus()
}
}
LaunchedEffect(Unit) {
loadProducts()
focusRequester.requestFocus()
}
// Auto-update suggestions as query changes
LaunchedEffect(searchQuery) {
if (searchQuery.length >= 2) {
autocompleteResults = allProducts.filter {
it.name.contains(searchQuery, ignoreCase = true) || it.barcode.contains(searchQuery)
}
} else {
autocompleteResults = emptyList()
}
}
val subtotal = cartItems.entries.sumOf { it.key.sellPrice * it.value }
val grandTotal = (subtotal - discount).coerceAtLeast(0.0)
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
// Barcode Scanner & Search Autocomplete Row
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Barcode Input
OutlinedTextField(
value = barcodeQuery,
onValueChange = {
barcodeQuery = it
val trimmed = it.trim()
if (trimmed.isNotEmpty()) {
val prod = allProducts.find { p -> barcodeMatches(p.barcode, trimmed) }
if (prod != null) {
addToCart(prod, 1)
barcodeQuery = "" // Clear barcode
focusRequester.requestFocus()
}
}
},
label = { Text("Scan / Ketik Barcode") },
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
trailingIcon = {
// Quick scan test button
IconButton(onClick = {
// Select a random product barcode to simulate a camera scan
if (allProducts.isNotEmpty()) {
val randProd = allProducts.random()
addToCart(randProd, 1)
focusRequester.requestFocus()
}
}) {
Icon(Icons.Default.Refresh, contentDescription = "Simulate Scan", tint = Color(0xFF4F46E5))
}
},
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number, imeAction = ImeAction.Search),
keyboardActions = KeyboardActions(onSearch = { performScanSearch() }),
modifier = Modifier
.weight(1f)
.focusRequester(focusRequester),
shape = RoundedCornerShape(12.dp)
)
// Start/Stop Camera Scan Button
Button(
onClick = { showCameraScanner = !showCameraScanner },
colors = ButtonDefaults.buttonColors(
containerColor = if (showCameraScanner) Color.Red else Color(0xFF4F46E5)
),
shape = RoundedCornerShape(12.dp),
modifier = Modifier.height(50.dp)
) {
Icon(
imageVector = if (showCameraScanner) Icons.Default.Close else Icons.Default.PlayArrow,
contentDescription = null
)
Spacer(modifier = Modifier.width(4.dp))
Text(if (showCameraScanner) "Stop" else "Kamera")
}
}
// Live Realtime Camera Scanner Preview
if (showCameraScanner) {
BarcodeScannerView(
onBarcodeScanned = { scannedCode ->
val foundProduct = allProducts.find { p -> barcodeMatches(p.barcode, scannedCode) }
if (foundProduct != null) {
addToCart(foundProduct, 1)
} else {
Toast.makeText(context, "Produk tidak ditemukan!", Toast.LENGTH_SHORT).show()
}
focusRequester.requestFocus()
},
modifier = Modifier.fillMaxWidth()
)
}
// Product Search Autocomplete Field
if (!showCameraScanner) {
Box(modifier = Modifier.fillMaxWidth()) {
OutlinedTextField(
value = searchQuery,
onValueChange = { searchQuery = it },
label = { Text("Cari Nama Produk...") },
singleLine = true,
shape = RoundedCornerShape(12.dp),
modifier = Modifier.fillMaxWidth()
)
// Autocomplete Popup
if (autocompleteResults.isNotEmpty()) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(top = 60.dp)
.heightIn(max = 200.dp),
shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
) {
LazyColumn(modifier = Modifier.padding(8.dp)) {
items(autocompleteResults) { product ->
Row(
modifier = Modifier
.fillMaxWidth()
.clickable {
addToCart(product, 1)
searchQuery = ""
autocompleteResults = emptyList()
}
.padding(12.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
Column {
Text(product.name, fontWeight = FontWeight.Bold, fontSize = 13.sp)
Text("Stok: ${product.stock} ${product.unit} • Barcode: ${product.barcode}", fontSize = 11.sp, color = Color.Gray)
}
Text(formatRupiah(product.sellPrice), fontWeight = FontWeight.Bold, fontSize = 13.sp, color = Color(0xFF4F46E5))
}
Divider(color = Color(0xFFF3F4F6))
}
}
}
}
}
}
// Cart List Section
Card(
modifier = Modifier
.fillMaxWidth()
.weight(1f),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = Color.White),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Keranjang Belanja",
fontSize = 15.sp,
fontWeight = FontWeight.Bold,
color = Color(0xFF1F2937)
)
Spacer(modifier = Modifier.height(12.dp))
if (cartItems.isEmpty()) {
Box(
modifier = Modifier
.fillMaxSize()
.weight(1f),
contentAlignment = Alignment.Center
) {
Text("Keranjang kosong. Cari produk di atas untuk menambahkan.", color = Color.Gray, fontSize = 13.sp, textAlign = TextAlign.Center)
}
} else {
LazyColumn(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(cartItems.keys.toList()) { product ->
val qty = cartItems[product] ?: 0
Row(
modifier = Modifier
.fillMaxWidth()
.border(0.5.dp, Color(0xFFE5E7EB), RoundedCornerShape(10.dp))
.padding(8.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(0.4f)) {
Text(product.name, fontSize = 13.sp, fontWeight = FontWeight.Bold)
Text(formatRupiah(product.sellPrice), fontSize = 11.sp, color = Color.Gray)
}
// Qty Controls
Row(
modifier = Modifier.weight(0.35f),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
IconButton(
onClick = {
if (qty > 1) {
cartItems[product] = qty - 1
} else {
cartItems.remove(product)
}
},
modifier = Modifier.size(28.dp)
) {
Icon(Icons.Default.KeyboardArrowDown, contentDescription = null, tint = Color.Red)
}
Text(
text = qty.toString(),
modifier = Modifier.padding(horizontal = 8.dp),
fontWeight = FontWeight.Bold,
fontSize = 14.sp
)
IconButton(
onClick = {
if (qty < product.stock) {
cartItems[product] = qty + 1
}
},
modifier = Modifier.size(28.dp)
) {
Icon(Icons.Default.Add, contentDescription = null, tint = Color(0xFF059669))
}
}
Text(
text = formatRupiah(product.sellPrice * qty),
fontWeight = FontWeight.Bold,
fontSize = 13.sp,
modifier = Modifier.weight(0.25f),
textAlign = TextAlign.End,
color = Color(0xFF1F2937)
)
}
}
}
}
}
}
// Summary & Checkout Drawer
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = Color(0xFFF9FAFB)),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
// Grand Total
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text("Total Bayar", fontWeight = FontWeight.Bold, fontSize = 16.sp, color = Color(0xFF1F2937))
Text(formatRupiah(grandTotal), fontWeight = FontWeight.ExtraBold, fontSize = 20.sp, color = Color(0xFF4F46E5))
}
Spacer(modifier = Modifier.height(4.dp))
// Checkout Button
Button(
onClick = {
if (cartItems.isEmpty()) return@Button
payAmountText = ""
customerName = ""
dueDate = ""
alertMessage = ""
showCheckoutDialog = true
},
enabled = cartItems.isNotEmpty(),
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF4F46E5)),
shape = RoundedCornerShape(12.dp),
modifier = Modifier
.fillMaxWidth()
.height(50.dp)
) {
Icon(Icons.Default.Check, contentDescription = null)
Spacer(modifier = Modifier.width(8.dp))
Text("Proses Pembayaran", fontWeight = FontWeight.Bold, fontSize = 16.sp)
}
}
}
}
// --- CHECKOUT DIALOG ---
if (showCheckoutDialog) {
val totalToPay = grandTotal
Dialog(onDismissRequest = { showCheckoutDialog = false }) {
Card(
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = Color.White),
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
LazyColumn(
modifier = Modifier.padding(24.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
item {
Text(
text = "Pilih Metode Pembayaran",
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
color = Color(0xFF1F2937),
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center
)
}
if (alertMessage.isNotEmpty()) {
item {
Text(alertMessage, color = Color.Red, fontSize = 13.sp, fontWeight = FontWeight.Medium)
}
}
item {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Button(
onClick = { paymentType = "cash" },
colors = ButtonDefaults.buttonColors(
containerColor = if (paymentType == "cash") Color(0xFF4F46E5) else Color(0xFFE5E7EB)
),
shape = RoundedCornerShape(8.dp),
modifier = Modifier.weight(1f)
) {
Text("CASH / TUNAI", color = if (paymentType == "cash") Color.White else Color.Black)
}
Button(
onClick = { paymentType = "piutang" },
colors = ButtonDefaults.buttonColors(
containerColor = if (paymentType == "piutang") Color(0xFF4F46E5) else Color(0xFFE5E7EB)
),
shape = RoundedCornerShape(8.dp),
modifier = Modifier.weight(1f)
) {
Text("PIUTANG", color = if (paymentType == "piutang") Color.White else Color.Black)
}
}
}
if (paymentType == "cash") {
item {
OutlinedTextField(
value = payAmountText,
onValueChange = { payAmountText = it },
label = { Text("Jumlah Bayar Tunai (Rp)") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
singleLine = true,
shape = RoundedCornerShape(10.dp),
modifier = Modifier.fillMaxWidth()
)
}
item {
Text("Pilih Uang Cepat:", fontSize = 11.sp, color = Color.Gray)
FlowRow(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
val exactAmount = totalToPay.toInt()
SuggestionChip(
onClick = { payAmountText = exactAmount.toString() },
label = { Text("Uang Pas (${formatRupiah(totalToPay)})", fontSize = 11.sp) }
)
val nominalList = listOf(20000, 50000, 100000, 200000)
nominalList.forEach { valNominal ->
SuggestionChip(
onClick = { payAmountText = valNominal.toString() },
label = { Text(formatRupiah(valNominal.toDouble()), fontSize = 11.sp) }
)
}
}
}
item {
val payVal = payAmountText.toDoubleOrNull() ?: 0.0
val changeAmount = (payVal - totalToPay).coerceAtLeast(0.0)
Card(
colors = CardDefaults.cardColors(containerColor = Color(0xFFECFDF5)),
shape = RoundedCornerShape(8.dp),
modifier = Modifier.fillMaxWidth()
) {
Row(
modifier = Modifier.padding(12.dp).fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text("Kembalian", fontWeight = FontWeight.Medium, color = Color(0xFF065F46), fontSize = 13.sp)
Text(formatRupiah(changeAmount), fontWeight = FontWeight.Bold, color = Color(0xFF047857), fontSize = 15.sp)
}
}
}
} else {
item {
OutlinedTextField(
value = customerName,
onValueChange = { customerName = it },
label = { Text("Nama Pelanggan") },
singleLine = true,
shape = RoundedCornerShape(10.dp),
modifier = Modifier.fillMaxWidth()
)
}
item {
OutlinedTextField(
value = dueDate,
onValueChange = { dueDate = it },
label = { Text("Tanggal Jatuh Tempo (YYYY-MM-DD)") },
singleLine = true,
shape = RoundedCornerShape(10.dp),
modifier = Modifier.fillMaxWidth()
)
}
item {
OutlinedTextField(
value = payAmountText,
onValueChange = { payAmountText = it },
label = { Text("Uang Muka / DP (Rp - Opsional)") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
singleLine = true,
shape = RoundedCornerShape(10.dp),
modifier = Modifier.fillMaxWidth()
)
}
}
item {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
OutlinedButton(
onClick = { showCheckoutDialog = false },
modifier = Modifier.weight(1f)
) {
Text("Batal")
}
Button(
onClick = {
val payVal = payAmountText.toDoubleOrNull() ?: 0.0
if (paymentType == "cash" && payVal < totalToPay) {
alertMessage = "Pembayaran kurang!"
return@Button
}
if (paymentType == "piutang" && customerName.isBlank()) {
alertMessage = "Nama pelanggan piutang harus diisi!"
return@Button
}
if (paymentType == "piutang" && dueDate.isBlank()) {
alertMessage = "Tanggal jatuh tempo harus diisi!"
return@Button
}
// Save transaction
val (success, invoiceNo) = dbHelper.saveTransaction(
userId = userId,
items = cartItems.entries.map { Pair(it.key, it.value) },
discount = discount,
paymentType = paymentType,
customerName = if (paymentType == "piutang") customerName else "Pelanggan Umum",
dueDate = if (paymentType == "piutang") dueDate else null,
payAmount = payVal
)
if (success) {
// Build Receipt
receiptInvoiceNo = invoiceNo
val settings = dbHelper.getSettings()
val changeVal = if (paymentType == "cash") (payVal - totalToPay) else 0.0
val builder = StringBuilder()
builder.append("===============================\n")
builder.append(" ${settings.storeName}\n")
builder.append(" ${settings.storeAddress}\n")
builder.append(" Telp: ${settings.storePhone}\n")
builder.append("===============================\n")
builder.append("No: $invoiceNo\n")
builder.append("Tgl: ${SimpleDateFormat("dd/MM/yyyy HH:mm", Locale.getDefault()).format(Date())}\n")
builder.append("Kasir: ${currentUser.name}\n")
builder.append("-------------------------------\n")
for (item in cartItems.entries) {
builder.append("${item.key.name}\n")
builder.append(" ${item.value} x ${formatRupiah(item.key.sellPrice)} = ${formatRupiah(item.key.sellPrice * item.value)}\n")
}
builder.append("-------------------------------\n")
builder.append("Subtotal: ${formatRupiah(subtotal)}\n")
if (discount > 0) {
builder.append("Diskon: -${formatRupiah(discount)}\n")
}
builder.append("Total: ${formatRupiah(totalToPay)}\n")
builder.append("Bayar: ${formatRupiah(payVal)}\n")
if (paymentType == "cash") {
builder.append("Kembali: ${formatRupiah(changeVal)}\n")
} else {
builder.append("Sisa Piutang: ${formatRupiah((totalToPay - payVal).coerceAtLeast(0.0))}\n")
builder.append("Tempo: $dueDate\n")
}
builder.append("===============================\n")
builder.append(" ${settings.receiptHeader ?: ""}\n")
builder.append(" ${settings.receiptFooter ?: ""}\n")
builder.append("===============================\n")
receiptContent = builder.toString()
// Clear Cart
cartItems.clear()
discountText = ""
loadProducts() // Reload stocks
showCheckoutDialog = false
showReceiptDialog = true
} else {
alertMessage = "Gagal menyimpan transaksi!"
}
},
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF059669)),
modifier = Modifier.weight(1f)
) {
Text("Bayar")
}
}
}
}
}
}
}
// --- RECEIPT DIALOG ---
if (showReceiptDialog) {
Dialog(onDismissRequest = { showReceiptDialog = false }) {
Card(
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = Color.White),
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Column(
modifier = Modifier.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
text = "Transaksi Berhasil!",
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
color = Color(0xFF059669)
)
Surface(
color = Color(0xFFF3F4F6),
shape = RoundedCornerShape(8.dp),
modifier = Modifier
.fillMaxWidth()
.heightIn(max = 300.dp)
) {
Box(modifier = Modifier.padding(16.dp)) {
Text(
text = receiptContent,
fontFamily = FontFamily.Monospace,
fontSize = 11.sp,
modifier = Modifier.fillMaxWidth()
)
}
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Button(
onClick = {
printReceipt(context, receiptContent, receiptInvoiceNo)
},
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF059669)),
modifier = Modifier.weight(1f)
) {
Text("Cetak Struk")
}
OutlinedButton(
onClick = { showReceiptDialog = false },
modifier = Modifier.weight(1f)
) {
Text("Tutup")
}
}
}
}
}
}
}