/* * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.bartek.iwentaryzacja import android.content.Context import android.content.Intent import android.net.Uri import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.foundation.background 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.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.List import androidx.compose.material.icons.filled.* import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.style.TextAlign import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.compose.viewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import org.apache.poi.ss.usermodel.WorkbookFactory import org.json.JSONArray import org.json.JSONObject import java.io.InputStream import java.net.HttpURLConnection import java.net.URL import java.util.Scanner // ========================================== // 1. MODEL DANYCH MASZYNY // ========================================== data class Machine( val id: String, // Nr ewid. (np. 7065/K) val name: String, // Nazwa sprzętu val category: String, // Kategoria (np. minikoparki) val originalStatus: String, // Status z kolumny H val isChecked: Boolean = false ) // ========================================== // 2. VIEWMODEL (DOPASOWANY PARSER .XLSX) // ========================================== class InventoryViewModel : ViewModel() { private val _machines = MutableStateFlow>(emptyList()) val machines: StateFlow> = _machines.asStateFlow() private val _searchQuery = MutableStateFlow("") val searchQuery: StateFlow = _searchQuery.asStateFlow() private val _filterMissing = MutableStateFlow(false) val filterMissing: StateFlow = _filterMissing.asStateFlow() private val _notes = MutableStateFlow("") val notes: StateFlow = _notes.asStateFlow() private val _branchName = MutableStateFlow("") val branchName: StateFlow = _branchName.asStateFlow() private val _reportDate = MutableStateFlow("") val reportDate: StateFlow = _reportDate.asStateFlow() private val _isNumericOnlySearch = MutableStateFlow(true) val isNumericOnlySearch: StateFlow = _isNumericOnlySearch.asStateFlow() private val _updateInfo = MutableStateFlow(null) val updateInfo: StateFlow = _updateInfo.asStateFlow() data class UpdateInfo(val versionCode: Int) fun loadState(context: Context) { val prefs = context.getSharedPreferences("inventory_prefs", Context.MODE_PRIVATE) _notes.value = prefs.getString("notes", "") ?: "" _branchName.value = prefs.getString("branch", "") ?: "" _reportDate.value = prefs.getString("date", "") ?: "" val machinesJson = prefs.getString("machines", null) if (machinesJson != null) { try { val jsonArray = JSONArray(machinesJson) val loadedList = mutableListOf() for (i in 0 until jsonArray.length()) { val obj = jsonArray.getJSONObject(i) loadedList.add( Machine( id = obj.getString("id"), name = obj.getString("name"), category = obj.getString("category"), originalStatus = obj.getString("originalStatus"), isChecked = obj.getBoolean("isChecked") ) ) } _machines.value = loadedList } catch (e: Exception) { e.printStackTrace() } } } private fun saveState(context: Context) { val prefs = context.getSharedPreferences("inventory_prefs", Context.MODE_PRIVATE) val editor = prefs.edit() editor.putString("notes", _notes.value) editor.putString("branch", _branchName.value) editor.putString("date", _reportDate.value) val jsonArray = JSONArray() _machines.value.forEach { machine -> val obj = JSONObject() obj.put("id", machine.id) obj.put("name", machine.name) obj.put("category", machine.category) obj.put("originalStatus", machine.originalStatus) obj.put("isChecked", machine.isChecked) jsonArray.put(obj) } editor.putString("machines", jsonArray.toString()) editor.apply() } fun setSearchQuery(query: String) { _searchQuery.value = query } fun setNumericOnlySearch(isNumeric: Boolean) { _isNumericOnlySearch.value = isNumeric } fun setNotes(context: Context, newNotes: String) { _notes.value = newNotes saveState(context) } fun appendDigit(digit: String) { _searchQuery.value += digit } fun backspace() { if (_searchQuery.value.isNotEmpty()) { _searchQuery.value = _searchQuery.value.dropLast(1) } } fun clearSearch() { _searchQuery.value = "" } fun toggleFilterMissing() { _filterMissing.value = !_filterMissing.value } fun checkForUpdates(context: Context) { viewModelScope.launch(Dispatchers.IO) { try { val url = URL("https://bbjsolution.org/AR/InwentARyzacja/raw/branch/master/update.json") val connection = url.openConnection() as HttpURLConnection connection.connectTimeout = 5000 connection.readTimeout = 5000 if (connection.responseCode == 200) { val scanner = Scanner(connection.inputStream).useDelimiter("\\A") val result = if (scanner.hasNext()) scanner.next() else "" val json = JSONObject(result) val newVersionCode = json.getInt("versionCode") val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0) @Suppress("DEPRECATION") val currentVersionCode = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) { packageInfo.longVersionCode.toInt() } else { packageInfo.versionCode } if (newVersionCode > currentVersionCode) { _updateInfo.value = UpdateInfo(newVersionCode) } } } catch (e: Exception) { e.printStackTrace() } } } fun getUniqueStatuses(): List { return _machines.value .map { it.originalStatus } .filter { it.isNotEmpty() } .distinct() .sorted() } fun markMachinesByStatuses(context: Context, statuses: Set, isChecked: Boolean) { _machines.value = _machines.value.map { machine -> if (statuses.contains(machine.originalStatus)) { machine.copy(isChecked = isChecked) } else { machine } } saveState(context) } fun toggleMachineCheck(context: Context, machineId: String) { _machines.value = _machines.value.map { machine -> if (machine.id == machineId) { machine.copy(isChecked = !machine.isChecked) } else { machine } } saveState(context) } // Parser zoptymalizowany pod strukturę raportu z systemu fun parseXlsFile(context: Context, inputStream: InputStream) { try { // WorkbookFactory automatycznie obsługuje zarówno .xls jak i .xlsx val workbook = WorkbookFactory.create(inputStream) val sheet = workbook.getSheetAt(0) // Próba pobrania nazwy oddziału i daty z nagłówka (zazwyczaj wiersz 0 lub 1) try { val headerRow = sheet.getRow(0) val branch = headerRow?.getCell(0)?.toString()?.trim() ?: "" val date = headerRow?.getCell(4)?.toString()?.trim() ?: "" // Przykładowa kolumna dla daty // Usuwamy zbędny napis "raport:lista produktow" w różnych wariantach val cleanedBranch = branch .replace(Regex("raport\\s*:", RegexOption.IGNORE_CASE), "") .replace(Regex("lista\\s*produktów", RegexOption.IGNORE_CASE), "") .replace(Regex("lista\\s*produktow", RegexOption.IGNORE_CASE), "") .trim() _branchName.value = cleanedBranch _reportDate.value = date } catch (e: Exception) { e.printStackTrace() } val parsedList = mutableListOf() // Dane zaczynają się od wiersza indeks 2 (po 2 wierszach nagłówka) for (rowIndex in 2..sheet.lastRowNum) { val row = sheet.getRow(rowIndex) ?: continue val category = row.getCell(1)?.toString()?.trim() ?: "" val id = row.getCell(2)?.toString()?.trim() ?: "" val name = row.getCell(3)?.toString()?.trim() ?: "" val status = row.getCell(7)?.toString()?.trim() ?: "" // Wczytujemy tylko, gdy pozycja ma nazwę i nr ewidencyjny if (name.isNotEmpty() && id.isNotEmpty()) { val isRented = status.contains("wypo", ignoreCase = true) parsedList.add( Machine( id = id, name = name, category = category, originalStatus = status, isChecked = isRented // Automatycznie zaznaczamy wypożyczone ) ) } } _machines.value = parsedList workbook.close() saveState(context) } catch (e: Exception) { e.printStackTrace() } } } // ========================================== // 3. GŁÓWNA AKTYWNOŚĆ // ========================================== class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { MaterialTheme( colorScheme = darkColorScheme( primary = Color(0xFF007ACC), background = Color(0xFF121824), surface = Color(0xFF1E2638) ) ) { var showSplashScreen by remember { mutableStateOf(true) } LaunchedEffect(Unit) { delay(2000) // Ekran startowy przez 2 sekundy showSplashScreen = false } Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) { if (showSplashScreen) { SplashScreen() } else { InventoryScreen() } } } } } } @Composable fun SplashScreen() { Box( modifier = Modifier .fillMaxSize() .background(Color(0xFF121824)), contentAlignment = Alignment.Center ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { Icon( imageVector = Icons.Default.Inventory, contentDescription = null, tint = Color(0xFF007ACC), modifier = Modifier.size(100.dp) ) Spacer(modifier = Modifier.height(24.dp)) Text( text = "inwentARyzacja", fontSize = 32.sp, fontWeight = FontWeight.Bold, color = Color.White ) Spacer(modifier = Modifier.height(8.dp)) Text( text = "System Zarządzania Sprzętem", fontSize = 14.sp, color = Color.Gray ) } } } // ========================================== // 4. EKRAN GŁÓWNY // ========================================== @OptIn(ExperimentalMaterial3Api::class) @Composable fun InventoryScreen(viewModel: InventoryViewModel = viewModel()) { val context = LocalContext.current val machines by viewModel.machines.collectAsState() val searchQuery by viewModel.searchQuery.collectAsState() val filterMissing by viewModel.filterMissing.collectAsState() val notes by viewModel.notes.collectAsState() val branchName by viewModel.branchName.collectAsState() val reportDate by viewModel.reportDate.collectAsState() val updateInfo by viewModel.updateInfo.collectAsState() val isNumericOnlySearch by viewModel.isNumericOnlySearch.collectAsState() var showStatusDialog by remember { mutableStateOf(false) } var showNotesDialog by remember { mutableStateOf(false) } var showUpdateDialog by remember { mutableStateOf(false) } var showEasterEggDialog by remember { mutableStateOf(false) } LaunchedEffect(searchQuery) { if (searchQuery == "666666") { showEasterEggDialog = true viewModel.clearSearch() } } if (showEasterEggDialog) { AlertDialog( onDismissRequest = { showEasterEggDialog = false }, title = { Text("O aplikacji", color = Color.White) }, text = { Text( "Aplikacja zrobiona w całości przez Bartka J, w wolnym czasie i na prywatnych komputerach", color = Color.White ) }, confirmButton = { TextButton(onClick = { showEasterEggDialog = false }) { Text("OK") } }, containerColor = Color(0xFF1E2638) ) } LaunchedEffect(Unit) { viewModel.loadState(context) viewModel.checkForUpdates(context) } LaunchedEffect(updateInfo) { if (updateInfo != null) { showUpdateDialog = true } } if (showUpdateDialog) { AlertDialog( onDismissRequest = { showUpdateDialog = false }, title = { Text("Nowa wersja dostępna", color = Color.White) }, text = { Text( "Dostępna jest nowsza wersja aplikacji. Pobierz ją ze strony:\nar.bbjsolution.org", color = Color.White ) }, confirmButton = { TextButton(onClick = { val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://ar.bbjsolution.org")) context.startActivity(intent) showUpdateDialog = false }) { Text("Otwórz stronę") } }, dismissButton = { TextButton(onClick = { showUpdateDialog = false }) { Text("Później") } }, containerColor = Color(0xFF1E2638) ) } // Launcher akceptujący pliki .xlsx oraz .xls val filePickerLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.GetContent() ) { uri: Uri? -> uri?.let { context.contentResolver.openInputStream(it)?.use { inputStream -> viewModel.parseXlsFile(context, inputStream) } } } if (showStatusDialog) { StatusSelectionDialog( statuses = viewModel.getUniqueStatuses(), onDismiss = { showStatusDialog = false }, onConfirm = { selectedStatuses -> viewModel.markMachinesByStatuses(context, selectedStatuses, true) showStatusDialog = false } ) } if (showNotesDialog) { NotesDialog( notes = notes, onNotesChange = { viewModel.setNotes(context, it) }, onDismiss = { showNotesDialog = false } ) } val filteredMachines = remember(machines, searchQuery, filterMissing, isNumericOnlySearch) { var list = if (searchQuery.isBlank()) machines else machines.filter { if (isNumericOnlySearch) { it.id.contains(searchQuery, ignoreCase = true) } else { it.name.contains(searchQuery, ignoreCase = true) || it.id.contains(searchQuery, ignoreCase = true) || it.category.contains(searchQuery, ignoreCase = true) } } if (filterMissing) { list = list.filter { !it.isChecked } } list } val checkedCount = machines.count { it.isChecked && !it.originalStatus.contains("wypo", ignoreCase = true) } val totalCount = machines.count { !it.originalStatus.contains("wypo", ignoreCase = true) } val progress = if (totalCount > 0) checkedCount.toFloat() / totalCount.toFloat() else 0f Scaffold( modifier = Modifier.fillMaxSize(), containerColor = MaterialTheme.colorScheme.background, contentWindowInsets = WindowInsets.safeDrawing ) { innerPadding -> Column( modifier = Modifier .fillMaxSize() .padding(innerPadding) .padding(horizontal = 16.dp, vertical = 8.dp) ) { // Cztery małe kafelki w jednej linii Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(4.dp) ) { DashboardTile( title = "Wczytaj", icon = Icons.Default.FileDownload, color = Color(0xFF007ACC), onClick = { filePickerLauncher.launch("*/*") }, modifier = Modifier.weight(1f) ) DashboardTile( title = if (filterMissing) "Wszystkie" else "Brakujące", icon = if (filterMissing) Icons.AutoMirrored.Filled.List else Icons.Default.Search, color = if (filterMissing) Color(0xFFFF9800) else Color(0xFFE91E63), onClick = { viewModel.toggleFilterMissing() }, modifier = Modifier.weight(1f) ) DashboardTile( title = "Statusy", icon = Icons.Default.Info, color = Color(0xFF9C27B0), onClick = { showStatusDialog = true }, modifier = Modifier.weight(1f) ) DashboardTile( title = "Dopisz", icon = Icons.Default.EditNote, color = Color(0xFF4CAF50), onClick = { showNotesDialog = true }, modifier = Modifier.weight(1f) ) } Spacer(modifier = Modifier.height(16.dp)) if (machines.isNotEmpty()) { Card( colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), shape = RoundedCornerShape(8.dp), modifier = Modifier.fillMaxWidth() ) { Column(modifier = Modifier.padding(8.dp)) { if (branchName.isNotEmpty() || reportDate.isNotEmpty()) { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween ) { Text( text = branchName, color = Color(0xFF11CAA0), fontSize = 11.sp, fontWeight = FontWeight.Bold, maxLines = 1 ) Text( text = reportDate, color = Color.Gray, fontSize = 10.sp ) } Spacer(modifier = Modifier.height(4.dp)) } Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween ) { Text("Postęp:", color = Color.Gray, fontSize = 12.sp) Text( "$checkedCount / $totalCount (${(progress * 100).toInt()}%)", color = Color.White, fontWeight = FontWeight.Bold, fontSize = 12.sp ) } Spacer(modifier = Modifier.height(4.dp)) LinearProgressIndicator( progress = { progress }, modifier = Modifier .fillMaxWidth() .height(4.dp) .clip(RoundedCornerShape(2.dp)), color = Color(0xFF11CAA0), trackColor = Color(0xFF2A3447) ) } } Spacer(modifier = Modifier.height(8.dp)) // Lista zajmuje elastyczną przestrzeń, ale zostanie ograniczona przez klawiaturę LazyColumn( verticalArrangement = Arrangement.spacedBy(4.dp), modifier = Modifier.weight(1f) // Używamy weight, aby zajęła resztę miejsca przed klawiaturą ) { items(filteredMachines, key = { it.id }) { machine -> MachineItem( machine = machine, onToggle = { viewModel.toggleMachineCheck(context, machine.id) } ) } } Spacer(modifier = Modifier.height(8.dp)) // Bardzo mały pasek szukania nad klawiaturą OutlinedTextField( value = searchQuery, onValueChange = { viewModel.setSearchQuery(it) viewModel.setNumericOnlySearch(false) }, placeholder = { Text("Szukaj...", fontSize = 12.sp, color = Color.Gray) }, modifier = Modifier.fillMaxWidth().height(48.dp), shape = RoundedCornerShape(8.dp), singleLine = true, textStyle = LocalTextStyle.current.copy(fontSize = 13.sp), colors = OutlinedTextFieldDefaults.colors( focusedBorderColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.5f), unfocusedBorderColor = Color(0xFF2A3447), focusedContainerColor = MaterialTheme.colorScheme.surface, unfocusedContainerColor = MaterialTheme.colorScheme.surface, focusedTextColor = Color.White, unfocusedTextColor = Color.White ) ) Spacer(modifier = Modifier.height(8.dp)) // Klawiatura numeryczna na dole NumericKeyboard( onDigitClick = { viewModel.setNumericOnlySearch(true) viewModel.appendDigit(it) }, onBackspace = { viewModel.setNumericOnlySearch(true) viewModel.backspace() }, onClear = { viewModel.setNumericOnlySearch(true) viewModel.clearSearch() } ) } else { Box( modifier = Modifier .fillMaxSize() .padding(32.dp), contentAlignment = Alignment.Center ) { Text( text = "Brak wczytanych maszyn.\nWybierz plik .xlsx wygenerowany z systemu.", color = Color.Gray, fontSize = 16.sp, textAlign = TextAlign.Center ) } } } } } // ========================================== // 5. POJEDYNCZY ELEMENT LISTY // ========================================== @OptIn(ExperimentalMaterial3Api::class) @Composable fun DashboardTile( title: String, icon: ImageVector, color: Color, onClick: () -> Unit, modifier: Modifier = Modifier ) { Card( onClick = onClick, modifier = modifier.height(70.dp), // Zmniejszona wysokość shape = RoundedCornerShape(12.dp), colors = CardDefaults.cardColors(containerColor = color.copy(alpha = 0.15f)), border = androidx.compose.foundation.BorderStroke(1.dp, color.copy(alpha = 0.3f)) ) { Column( modifier = Modifier.fillMaxSize().padding(4.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { Icon(icon, contentDescription = null, tint = color, modifier = Modifier.size(24.dp)) Spacer(modifier = Modifier.height(2.dp)) Text( text = title, fontSize = 11.sp, fontWeight = FontWeight.Bold, color = Color.White, textAlign = TextAlign.Center, lineHeight = 12.sp ) } } } @Composable fun StatusSelectionDialog( statuses: List, onDismiss: () -> Unit, onConfirm: (Set) -> Unit ) { var selectedStatuses by remember { mutableStateOf(setOf()) } AlertDialog( onDismissRequest = onDismiss, title = { Text("Wybierz statusy do zaznaczenia") }, text = { if (statuses.isEmpty()) { Text("Brak wczytanych maszyn ze statusami.") } else { LazyColumn(modifier = Modifier.fillMaxWidth().heightIn(max = 300.dp)) { items(statuses) { status -> Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier .fillMaxWidth() .clickable { selectedStatuses = if (selectedStatuses.contains(status)) { selectedStatuses - status } else { selectedStatuses + status } } .padding(vertical = 8.dp) ) { Checkbox( checked = selectedStatuses.contains(status), onCheckedChange = null // Obsłużone w Row.clickable ) Spacer(modifier = Modifier.width(8.dp)) Text(status, color = Color.White) } } } } }, confirmButton = { TextButton(onClick = { onConfirm(selectedStatuses) }) { Text("Zastosuj") } }, dismissButton = { TextButton(onClick = onDismiss) { Text("Anuluj") } }, containerColor = Color(0xFF1E2638), titleContentColor = Color.White, textContentColor = Color.White ) } @Composable fun NotesDialog( notes: String, onNotesChange: (String) -> Unit, onDismiss: () -> Unit ) { AlertDialog( onDismissRequest = onDismiss, title = { Text("Notatnik", color = Color.White) }, text = { OutlinedTextField( value = notes, onValueChange = onNotesChange, modifier = Modifier.fillMaxWidth().height(200.dp), placeholder = { Text("Wpisz notatki tutaj...", color = Color.Gray) }, colors = OutlinedTextFieldDefaults.colors( focusedTextColor = Color.White, unfocusedTextColor = Color.White, focusedContainerColor = Color(0xFF2A3447), unfocusedContainerColor = Color(0xFF2A3447) ) ) }, confirmButton = { TextButton(onClick = onDismiss) { Text("Zamknij") } }, containerColor = Color(0xFF1E2638) ) } @Composable fun NumericKeyboard( onDigitClick: (String) -> Unit, onBackspace: () -> Unit, onClear: () -> Unit ) { Column( modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp) ) { val buttons = listOf( listOf("1", "2", "3"), listOf("4", "5", "6"), listOf("7", "8", "9"), listOf("C", "0", "⌫") ) buttons.forEach { row -> Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp) ) { row.forEach { label -> val isSpecial = label == "C" || label == "⌫" KeyButton( label = label, onClick = { when (label) { "C" -> onClear() "⌫" -> onBackspace() else -> onDigitClick(label) } }, color = if (isSpecial) Color(0xFF2A3447) else Color(0xFF1E2638), textColor = if (label == "⌫") Color(0xFFE91E63) else if (label == "C") Color(0xFFFF9800) else Color.White, modifier = Modifier.weight(1f) ) } } } } } @Composable fun KeyButton( label: String, onClick: () -> Unit, color: Color, textColor: Color, modifier: Modifier = Modifier ) { Surface( onClick = onClick, modifier = modifier.height(54.dp), shape = RoundedCornerShape(12.dp), color = color, border = androidx.compose.foundation.BorderStroke(1.dp, Color.White.copy(alpha = 0.1f)) ) { Box(contentAlignment = Alignment.Center) { Text( text = label, fontSize = 20.sp, fontWeight = FontWeight.Bold, color = textColor ) } } } @Composable fun MachineItem( machine: Machine, onToggle: () -> Unit ) { Card( shape = RoundedCornerShape(8.dp), colors = CardDefaults.cardColors( containerColor = if (machine.isChecked) Color(0xFF183B32) else MaterialTheme.colorScheme.surface ), modifier = Modifier .fillMaxWidth() .clickable { onToggle() } ) { Row( modifier = Modifier .padding(8.dp) .fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween ) { Column(modifier = Modifier.weight(1f)) { Text( text = machine.id, fontWeight = FontWeight.Bold, fontSize = 14.sp, color = Color.White ) Text( text = machine.name, fontSize = 11.sp, color = Color.LightGray, maxLines = 1 ) } if (machine.originalStatus.isNotEmpty()) { Text( text = machine.originalStatus, fontSize = 12.sp, fontWeight = FontWeight.Medium, color = Color(0xFF11CAA0), modifier = Modifier.padding(start = 8.dp) ) } } } }