/* * 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.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.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.viewmodel.compose.viewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import org.apache.poi.ss.usermodel.WorkbookFactory import org.json.JSONArray import org.json.JSONObject import java.io.InputStream // ========================================== // 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() fun loadState(context: Context) { val prefs = context.getSharedPreferences("inventory_prefs", Context.MODE_PRIVATE) _notes.value = prefs.getString("notes", "") ?: "" 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) 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 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 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) 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()) { parsedList.add( Machine( id = id, name = name, category = category, originalStatus = status ) ) } } _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) ) ) { Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) { InventoryScreen() } } } } } // ========================================== // 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() var showStatusDialog by remember { mutableStateOf(false) } var showNotesDialog by remember { mutableStateOf(false) } LaunchedEffect(Unit) { viewModel.loadState(context) } // 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) { var list = if (searchQuery.isBlank()) machines else machines.filter { 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 } val totalCount = machines.size 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)) { 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) }, 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.appendDigit(it) }, onBackspace = { viewModel.backspace() }, onClear = { 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 = 10.sp, fontWeight = FontWeight.Medium, color = Color(0xFF11CAA0), modifier = Modifier.padding(start = 8.dp) ) } } } }