package com.example.barokahstore.ui.screens import androidx.compose.foundation.Canvas import androidx.compose.foundation.background import androidx.compose.foundation.border 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.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.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.drawText import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.example.barokahstore.data.DatabaseHelper import com.example.barokahstore.data.Product import com.example.barokahstore.data.Transaction import com.example.barokahstore.data.User import java.text.NumberFormat import java.util.Locale @OptIn(ExperimentalLayoutApi::class) @Composable fun DashboardScreen( dbHelper: DatabaseHelper, currentUser: User, onNavigateTo: (String) -> Unit ) { val isAdmin = currentUser.role == "admin" val userId = currentUser.id // Dashboard Data States var todaySales by remember { mutableStateOf(0.0) } var activePiutang by remember { mutableStateOf(0.0) } var totalProducts by remember { mutableStateOf(0) } var lowStockCount by remember { mutableStateOf(0) } var lowStockProducts by remember { mutableStateOf(listOf()) } var recentTransactions by remember { mutableStateOf(listOf()) } var chartData by remember { mutableStateOf(listOf>()) } var checkInTime by remember { mutableStateOf("--:--") } var isClockedIn by remember { mutableStateOf(false) } var attendanceStatus by remember { mutableStateOf("") } // Setting threshold val settings = remember { dbHelper.getSettings() } val threshold = settings.lowStockThreshold // Load data function val loadDashboardData = { todaySales = dbHelper.getTodaySales(isAdmin, userId) activePiutang = dbHelper.getActivePiutangAmount(isAdmin, userId) totalProducts = dbHelper.getAllProducts().size lowStockCount = dbHelper.getLowStockCount(threshold) lowStockProducts = dbHelper.getLowStockProducts(threshold) recentTransactions = dbHelper.getAllTransactions().take(5) chartData = dbHelper.getSalesChartData(isAdmin, userId) val attendance = dbHelper.getAttendanceForUserToday(userId) if (attendance != null) { isClockedIn = true attendanceStatus = attendance.status // extract time try { val fullTime = attendance.checkIn if (fullTime.length >= 19) { checkInTime = fullTime.substring(11, 16) } else { checkInTime = fullTime } } catch (e: Exception) { checkInTime = attendance.checkIn } } else { isClockedIn = false checkInTime = "--:--" } } LaunchedEffect(Unit) { loadDashboardData() } LazyColumn( modifier = Modifier .fillMaxSize() .padding(16.dp), verticalArrangement = Arrangement.spacedBy(20.dp) ) { // Stats Cards Grid item { FlowRow( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp), verticalArrangement = Arrangement.spacedBy(12.dp), maxItemsInEachRow = 2 ) { val cardWidthModifier = Modifier.weight(1f).minWidth(150.dp) // Today Sales StatCard( title = "Penjualan Hari Ini", value = formatRupiah(todaySales), icon = Icons.Default.ShoppingCart, backgroundColor = Color(0xFFE0E7FF), iconColor = Color(0xFF4F46E5), modifier = cardWidthModifier ) // Active Piutang StatCard( title = "Total Piutang Aktif", value = formatRupiah(activePiutang), icon = Icons.Default.Star, backgroundColor = Color(0xFFFEF3C7), iconColor = Color(0xFFD97706), modifier = cardWidthModifier ) // Total Products StatCard( title = "Total Produk", value = "$totalProducts Item", icon = Icons.Default.List, backgroundColor = Color(0xFFD1FAE5), iconColor = Color(0xFF059669), modifier = cardWidthModifier ) // Low Stock StatCard( title = "Stok Menipis", value = "$lowStockCount Item", icon = Icons.Default.Warning, backgroundColor = Color(0xFFFEE2E2), iconColor = Color(0xFFDC2626), modifier = cardWidthModifier ) } } // Sales Chart item { Card( modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(16.dp), colors = CardDefaults.cardColors(containerColor = Color.White), elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) ) { Column(modifier = Modifier.padding(16.dp)) { Text( text = "Grafik Omset 7 Hari Terakhir", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = Color(0xFF1F2937) ) Spacer(modifier = Modifier.height(16.dp)) if (chartData.isNotEmpty()) { CustomSalesChart(chartData = chartData) } else { Box( modifier = Modifier.height(200.dp).fillMaxWidth(), contentAlignment = Alignment.Center ) { Text("Data grafik kosong") } } } } } // Sidebar content split into list items (Low Stock & Attendance) item { Column( modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(16.dp) ) { // Low Stock Card Card( modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(16.dp), colors = CardDefaults.cardColors(containerColor = Color.White), elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) ) { Column(modifier = Modifier.padding(16.dp)) { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { Text( text = "Stok Hampir Habis", fontSize = 14.sp, fontWeight = FontWeight.Bold, color = Color(0xFFDC2626) ) TextButton(onClick = { onNavigateTo("stok") }) { Text("Kelola Stok", color = Color(0xFF4F46E5)) } } if (lowStockProducts.isEmpty()) { Text( text = "Semua stok aman!", fontSize = 13.sp, color = Color.Gray, modifier = Modifier.fillMaxWidth().padding(vertical = 12.dp), textAlign = TextAlign.Center ) } else { lowStockProducts.forEach { product -> Row( modifier = Modifier .fillMaxWidth() .padding(vertical = 6.dp) .border(0.5.dp, Color(0xFFE5E7EB), RoundedCornerShape(8.dp)) .padding(10.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { Text( text = product.name, fontSize = 13.sp, fontWeight = FontWeight.Medium, color = Color(0xFF374151) ) Surface( color = Color(0xFFFEE2E2), shape = RoundedCornerShape(6.dp) ) { Text( text = "${product.stock} ${product.unit}", color = Color(0xFFDC2626), fontSize = 12.sp, fontWeight = FontWeight.Bold, modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp) ) } } } } } } // Attendance Clock Card Card( modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(16.dp), colors = CardDefaults.cardColors(containerColor = Color.White), elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) ) { Row( modifier = Modifier .fillMaxWidth() .padding(16.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { Column { Text( text = "Kehadiran Hari Ini", fontSize = 14.sp, fontWeight = FontWeight.Bold, color = Color(0xFF059669) ) Spacer(modifier = Modifier.height(4.dp)) Text( text = "Masuk: $checkInTime", fontSize = 18.sp, fontWeight = FontWeight.ExtraBold, color = Color(0xFF1F2937) ) if (isClockedIn) { Text( text = "Status: ${attendanceStatus.uppercase()}", fontSize = 11.sp, color = if (attendanceStatus == "present") Color(0xFF059669) else Color(0xFFD97706) ) } } Button( onClick = { if (!isClockedIn) { dbHelper.checkInUser(userId, "Absen via Mobile") } else { dbHelper.checkOutUser(userId) } loadDashboardData() }, colors = ButtonDefaults.buttonColors( containerColor = if (!isClockedIn) Color(0xFF059669) else Color(0xFFDC2626) ), shape = RoundedCornerShape(10.dp) ) { Text( text = if (!isClockedIn) "Clock-In" else "Clock-Out", fontWeight = FontWeight.Bold ) } } } } } // Recent Transactions item { Card( modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(16.dp), colors = CardDefaults.cardColors(containerColor = Color.White), elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) ) { Column(modifier = Modifier.padding(16.dp)) { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { Text( text = "Transaksi Terbaru", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = Color(0xFF1F2937) ) TextButton(onClick = { onNavigateTo("riwayat") }) { Text("Lihat Semua", color = Color(0xFF4F46E5)) } } if (recentTransactions.isEmpty()) { Text( text = "Belum ada transaksi.", fontSize = 13.sp, color = Color.Gray, modifier = Modifier.fillMaxWidth().padding(vertical = 20.dp), textAlign = TextAlign.Center ) } else { recentTransactions.forEach { tx -> Row( modifier = Modifier .fillMaxWidth() .padding(vertical = 8.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { Column(modifier = Modifier.weight(1f)) { Text( text = tx.invoiceNo, fontSize = 13.sp, fontWeight = FontWeight.Bold, color = Color(0xFF1F2937) ) Text( text = "Kasir: ${tx.cashierName} • ${tx.paymentType.uppercase()}", fontSize = 11.sp, color = Color.Gray ) } Column(horizontalAlignment = Alignment.End) { Text( text = formatRupiah(tx.grandTotal), fontSize = 13.sp, fontWeight = FontWeight.Bold, color = Color(0xFF1F2937) ) Surface( color = if (tx.status == "paid") Color(0xFFD1FAE5) else Color(0xFFFEE2E2), shape = RoundedCornerShape(4.dp), modifier = Modifier.padding(top = 2.dp) ) { Text( text = if (tx.status == "paid") "LUNAS" else "BELUM LUNAS", color = if (tx.status == "paid") Color(0xFF059669) else Color(0xFFDC2626), fontSize = 9.sp, fontWeight = FontWeight.Bold, modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp) ) } } } Divider(color = Color(0xFFF3F4F6)) } } } } } } } @Composable fun StatCard( title: String, value: String, icon: androidx.compose.ui.graphics.vector.ImageVector, backgroundColor: Color, iconColor: Color, modifier: Modifier = Modifier ) { Card( modifier = modifier, shape = RoundedCornerShape(16.dp), colors = CardDefaults.cardColors(containerColor = Color.White), elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) ) { Row( modifier = Modifier .padding(16.dp) .fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { Column(modifier = Modifier.weight(1f)) { Text( text = title, fontSize = 12.sp, color = Color.Gray, fontWeight = FontWeight.Medium ) Spacer(modifier = Modifier.height(4.dp)) Text( text = value, fontSize = 16.sp, fontWeight = FontWeight.Bold, color = Color(0xFF1F2937) ) } Box( modifier = Modifier .size(40.dp) .background(backgroundColor, RoundedCornerShape(12.dp)), contentAlignment = Alignment.Center ) { Icon( imageVector = icon, contentDescription = null, tint = iconColor, modifier = Modifier.size(20.dp) ) } } } } @Composable fun CustomSalesChart(chartData: List>) { val textMeasurer = rememberTextMeasurer() val maxSales = (chartData.maxOfOrNull { it.second } ?: 1000.0).coerceAtLeast(1000.0) Canvas( modifier = Modifier .fillMaxWidth() .height(200.dp) .padding(top = 10.dp, bottom = 10.dp) ) { val width = size.width val height = size.height val paddingLeft = 120f val paddingBottom = 60f val chartWidth = width - paddingLeft - 20f val chartHeight = height - paddingBottom - 10f // Draw horizontal grid lines (4 lines) val gridLines = 4 for (i in 0..gridLines) { val y = chartHeight * i / gridLines drawLine( color = Color(0xFFE5E7EB), start = Offset(paddingLeft, y), end = Offset(width, y), strokeWidth = 1f ) // Draw Y-Axis labels val valAtLine = maxSales * (gridLines - i) / gridLines val label = formatRupiahShort(valAtLine) drawText( textMeasurer = textMeasurer, text = label, topLeft = Offset(10f, y - 20f), style = TextStyle(color = Color.Gray, fontSize = 10.sp) ) } // Generate coordinates for chart path val points = mutableListOf() val colWidth = chartWidth / (chartData.size - 1).coerceAtLeast(1) for (i in chartData.indices) { val x = paddingLeft + i * colWidth val y = chartHeight - (chartData[i].second / maxSales * chartHeight).toFloat() points.add(Offset(x, y)) // Draw X-Axis labels drawText( textMeasurer = textMeasurer, text = chartData[i].first, topLeft = Offset(x - 30f, chartHeight + 10f), style = TextStyle(color = Color.Gray, fontSize = 10.sp) ) } // Draw line connecting points val path = Path().apply { if (points.isNotEmpty()) { moveTo(points[0].x, points[0].y) for (i in 1 until points.size) { val pPrev = points[i - 1] val pCurr = points[i] // Cubic bezier control points for smooth line curves val controlX1 = pPrev.x + colWidth / 2f val controlY1 = pPrev.y val controlX2 = pCurr.x - colWidth / 2f val controlY2 = pCurr.y cubicTo(controlX1, controlY1, controlX2, controlY2, pCurr.x, pCurr.y) } } } // Draw smooth path stroke drawPath( path = path, color = Color(0xFF4F46E5), style = Stroke(width = 6f) ) // Draw points (dots) and values for (offset in points) { drawCircle( color = Color(0xFF4F46E5), radius = 8f, center = offset ) drawCircle( color = Color.White, radius = 4f, center = offset ) } } } // Helpers fun formatRupiah(value: Double): String { val format = NumberFormat.getCurrencyInstance(Locale("id", "ID")) format.maximumFractionDigits = 0 return format.format(value).replace("Rp", "Rp ") } fun formatRupiahShort(value: Double): String { return when { value >= 1_000_000 -> { val num = value / 1_000_000 String.format(Locale.getDefault(), "Rp %.1fM", num) } value >= 1_000 -> { val num = value / 1_000 String.format(Locale.getDefault(), "Rp %.0fK", num) } else -> { "Rp " + value.toInt().toString() } } } // Custom layout modifier for minimum width fun Modifier.minWidth(min: androidx.compose.ui.unit.Dp) = this.then( Modifier.widthIn(min = min) )