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") } } } } } } }