diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
index ca16a99..bf39c02 100644
--- a/.idea/deploymentTargetSelector.xml
+++ b/.idea/deploymentTargetSelector.xml
@@ -4,6 +4,13 @@
+
+
+
+
+
+
+
diff --git a/.idea/misc.xml b/.idea/misc.xml
index a7fab1c..29a5cc0 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,7 +1,7 @@
-
+
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index c1817af..1fd91e3 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -11,7 +11,7 @@ android {
defaultConfig {
applicationId = "com.bartek.iwentaryzacja"
- minSdk = 24
+ minSdk = 26
targetSdk = 37
versionCode = 1
versionName = "1.0"
@@ -36,6 +36,17 @@ android {
}
dependencies {
+ // Jetpack Compose & ViewModel
+ implementation(platform("androidx.compose:compose-bom:2024.02.00"))
+ implementation("androidx.compose.ui:ui")
+ implementation("androidx.compose.material3:material3")
+ implementation("androidx.compose.material:material-icons-extended")
+ implementation("androidx.activity:activity-compose:1.8.2")
+ implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0")
+
+ // Obsługa plików Excel .xls
+ implementation("org.apache.poi:poi:5.2.3")
+ implementation("org.apache.poi:poi-ooxml:5.2.3")
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.compose.material3)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index b508a32..fbd0327 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -2,6 +2,9 @@
+
+
+
+
+
+
+
+
.
+ */
+
package com.bartek.iwentaryzacja
+import android.app.DownloadManager
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.net.Uri
+import android.os.Build
import android.os.Bundle
+import android.os.Environment
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.layout.fillMaxSize
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.Scaffold
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
+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.tooling.preview.Preview
-import com.bartek.iwentaryzacja.ui.theme.IwentARyzacjaTheme
+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.core.content.ContextCompat
+import androidx.core.content.FileProvider
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import androidx.lifecycle.viewmodel.compose.viewModel
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import org.apache.poi.ss.usermodel.WorkbookFactory
+import org.json.JSONObject
+import java.io.File
+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 _updateInfo = MutableStateFlow(null)
+ val updateInfo: StateFlow = _updateInfo.asStateFlow()
+
+ data class UpdateInfo(val versionCode: Int, val apkUrl: String)
+
+ fun checkForUpdates(context: Context) {
+ viewModelScope.launch(Dispatchers.IO) {
+ try {
+ val url = URL("https://bbjsolution.org/AR/InwentARyzacja/src/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 apkUrl = json.getString("url")
+
+ val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
+ @Suppress("DEPRECATION")
+ val currentVersionCode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ packageInfo.longVersionCode.toInt()
+ } else {
+ packageInfo.versionCode
+ }
+
+ if (newVersionCode > currentVersionCode) {
+ _updateInfo.value = UpdateInfo(newVersionCode, apkUrl)
+ }
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+ }
+
+ fun downloadAndInstall(context: Context, apkUrl: String) {
+ val request = DownloadManager.Request(Uri.parse(apkUrl))
+ .setTitle("Aktualizacja inwentARyzacja")
+ .setDescription("Pobieranie nowej wersji...")
+ .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
+ .setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, "update.apk")
+ .setAllowedOverMetered(true)
+ .setAllowedOverRoaming(true)
+
+ val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
+ downloadManager.enqueue(request)
+ }
+
+ fun setSearchQuery(query: String) {
+ _searchQuery.value = query
+ }
+
+ fun setNotes(newNotes: String) {
+ _notes.value = newNotes
+ }
+
+ 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(statuses: Set, isChecked: Boolean) {
+ _machines.value = _machines.value.map { machine ->
+ if (statuses.contains(machine.originalStatus)) {
+ machine.copy(isChecked = isChecked)
+ } else {
+ machine
+ }
+ }
+ }
+
+ fun toggleMachineCheck(machineId: String) {
+ _machines.value = _machines.value.map { machine ->
+ if (machine.id == machineId) {
+ machine.copy(isChecked = !machine.isChecked)
+ } else {
+ machine
+ }
+ }
+ }
+
+ // Parser zoptymalizowany pod strukturę raportu z systemu
+ fun parseXlsFile(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()
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+}
+
+// ==========================================
+// 3. GŁÓWNA AKTYWNOŚĆ
+// ==========================================
class MainActivity : ComponentActivity() {
+ private val onDownloadComplete = object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ val id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
+ if (id != -1L) {
+ installApk(context)
+ }
+ }
+ }
+
+ private fun installApk(context: Context) {
+ val file = File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "update.apk")
+ if (file.exists()) {
+ val uri = FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", file)
+ val intent = Intent(Intent.ACTION_VIEW)
+ intent.setDataAndType(uri, "application/vnd.android.package-archive")
+ intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ context.startActivity(intent)
+ }
+ }
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
+
+ ContextCompat.registerReceiver(
+ this,
+ onDownloadComplete,
+ IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE),
+ ContextCompat.RECEIVER_EXPORTED
+ )
+
setContent {
- IwentARyzacjaTheme {
- Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
- Greeting(
- name = "Android",
- modifier = Modifier.padding(innerPadding)
+ MaterialTheme(
+ colorScheme = darkColorScheme(
+ primary = Color(0xFF007ACC),
+ background = Color(0xFF121824),
+ surface = Color(0xFF1E2638)
+ )
+ ) {
+ Surface(
+ modifier = Modifier.fillMaxSize(),
+ color = MaterialTheme.colorScheme.background
+ ) {
+ InventoryScreen()
+ }
+ }
+ }
+ }
+ override fun onDestroy() {
+ super.onDestroy()
+ unregisterReceiver(onDownloadComplete)
+ }
+}
+
+// ==========================================
+// 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 updateInfo by viewModel.updateInfo.collectAsState()
+
+ var showStatusDialog by remember { mutableStateOf(false) }
+ var showNotesDialog by remember { mutableStateOf(false) }
+ var showUpdateDialog by remember { mutableStateOf(false) }
+
+ LaunchedEffect(Unit) {
+ viewModel.checkForUpdates(context)
+ }
+
+ LaunchedEffect(updateInfo) {
+ if (updateInfo != null) {
+ showUpdateDialog = true
+ }
+ }
+
+ if (showUpdateDialog && updateInfo != null) {
+ AlertDialog(
+ onDismissRequest = { showUpdateDialog = false },
+ title = { Text("Dostępna aktualizacja", color = Color.White) },
+ text = { Text("Czy chcesz pobrać i zainstalować nową wersję aplikacji?", color = Color.White) },
+ confirmButton = {
+ Button(onClick = {
+ viewModel.downloadAndInstall(context, updateInfo!!.apkUrl)
+ showUpdateDialog = false
+ }) {
+ Text("Pobierz")
+ }
+ },
+ 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(inputStream)
+ }
+ }
+ }
+
+ if (showStatusDialog) {
+ StatusSelectionDialog(
+ statuses = viewModel.getUniqueStatuses(),
+ onDismiss = { showStatusDialog = false },
+ onConfirm = { selectedStatuses ->
+ viewModel.markMachinesByStatuses(selectedStatuses, true)
+ showStatusDialog = false
+ }
+ )
+ }
+
+ if (showNotesDialog) {
+ NotesDialog(
+ notes = notes,
+ onNotesChange = { viewModel.setNotes(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(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)
)
}
}
@@ -31,17 +708,76 @@ class MainActivity : ComponentActivity() {
}
@Composable
-fun Greeting(name: String, modifier: Modifier = Modifier) {
- Text(
- text = "Hello $name!",
- modifier = modifier
- )
+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
+ )
+ }
+ }
}
-@Preview(showBackground = true)
@Composable
-fun GreetingPreview() {
- IwentARyzacjaTheme {
- Greeting("Android")
+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)
+ )
+ }
+ }
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/bartek/iwentaryzacja/ui/theme/Color.kt b/app/src/main/java/com/bartek/iwentaryzacja/ui/theme/Color.kt
index 4525372..aa0685b 100644
--- a/app/src/main/java/com/bartek/iwentaryzacja/ui/theme/Color.kt
+++ b/app/src/main/java/com/bartek/iwentaryzacja/ui/theme/Color.kt
@@ -1,3 +1,18 @@
+/*
+ * 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.ui.theme
import androidx.compose.ui.graphics.Color
diff --git a/app/src/main/java/com/bartek/iwentaryzacja/ui/theme/Theme.kt b/app/src/main/java/com/bartek/iwentaryzacja/ui/theme/Theme.kt
index a428e0d..6e07a0c 100644
--- a/app/src/main/java/com/bartek/iwentaryzacja/ui/theme/Theme.kt
+++ b/app/src/main/java/com/bartek/iwentaryzacja/ui/theme/Theme.kt
@@ -1,3 +1,18 @@
+/*
+ * 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.ui.theme
import android.app.Activity
diff --git a/app/src/main/java/com/bartek/iwentaryzacja/ui/theme/Type.kt b/app/src/main/java/com/bartek/iwentaryzacja/ui/theme/Type.kt
index 5a3ddb2..64e9b89 100644
--- a/app/src/main/java/com/bartek/iwentaryzacja/ui/theme/Type.kt
+++ b/app/src/main/java/com/bartek/iwentaryzacja/ui/theme/Type.kt
@@ -1,3 +1,18 @@
+/*
+ * 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.ui.theme
import androidx.compose.material3.Typography
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp
new file mode 100644
index 0000000..80cce83
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp
new file mode 100644
index 0000000..3a78c05
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp
new file mode 100644
index 0000000..8e5a309
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp
new file mode 100644
index 0000000..715c80d
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp
new file mode 100644
index 0000000..9279ba9
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ
diff --git a/app/src/main/res/xml/file_paths.xml b/app/src/main/res/xml/file_paths.xml
new file mode 100644
index 0000000..7f68867
--- /dev/null
+++ b/app/src/main/res/xml/file_paths.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file