diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..f0c6ad0 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,50 @@ + + + + \ No newline at end of file diff --git a/.idea/planningMode.xml b/.idea/planningMode.xml new file mode 100644 index 0000000..28213e9 --- /dev/null +++ b/.idea/planningMode.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2ce8d28..228c5b7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -36,25 +36,22 @@ 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") + + // Jetpack Compose & Core implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.activity.compose) implementation(libs.androidx.compose.material3) + implementation("androidx.compose.material:material-icons-extended") implementation(libs.androidx.compose.ui) implementation(libs.androidx.compose.ui.graphics) implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycle.runtime.ktx) + implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0") + testImplementation(libs.junit) androidTestImplementation(platform(libs.androidx.compose.bom)) androidTestImplementation(libs.androidx.compose.ui.test.junit4) diff --git a/app/release/baselineProfiles/0/app-release.dm b/app/release/baselineProfiles/0/app-release.dm index 1459877..14b393b 100644 Binary files a/app/release/baselineProfiles/0/app-release.dm and b/app/release/baselineProfiles/0/app-release.dm differ diff --git a/app/release/baselineProfiles/1/app-release.dm b/app/release/baselineProfiles/1/app-release.dm index a3e9a08..8f4f58f 100644 Binary files a/app/release/baselineProfiles/1/app-release.dm and b/app/release/baselineProfiles/1/app-release.dm differ diff --git a/app/release/output-metadata.json b/app/release/output-metadata.json index 59a4f4a..ce8b528 100644 --- a/app/release/output-metadata.json +++ b/app/release/output-metadata.json @@ -11,8 +11,8 @@ "type": "SINGLE", "filters": [], "attributes": [], - "versionCode": 1, - "versionName": "1.0", + "versionCode": 8, + "versionName": "1.5", "outputFile": "app-release.apk" } ], diff --git a/app/release/ver1.1.apk b/app/release/ver1.1.apk deleted file mode 100644 index 1b6d1ec..0000000 Binary files a/app/release/ver1.1.apk and /dev/null differ diff --git a/app/src/main/java/com/bartek/iwentaryzacja/MainActivity.kt b/app/src/main/java/com/bartek/iwentaryzacja/MainActivity.kt index f514009..afadce2 100644 --- a/app/src/main/java/com/bartek/iwentaryzacja/MainActivity.kt +++ b/app/src/main/java/com/bartek/iwentaryzacja/MainActivity.kt @@ -19,6 +19,7 @@ import android.content.Context import android.content.Intent import android.net.Uri import android.os.Bundle +import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts @@ -54,12 +55,15 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch +import androidx.core.content.edit +import androidx.core.net.toUri 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.net.URLEncoder import java.util.Scanner // ========================================== @@ -101,6 +105,9 @@ class InventoryViewModel : ViewModel() { private val _updateInfo = MutableStateFlow(null) val updateInfo: StateFlow = _updateInfo.asStateFlow() + private val _isLocked = MutableStateFlow(false) + val isLocked: StateFlow = _isLocked.asStateFlow() + data class UpdateInfo(val versionCode: Int) fun loadState(context: Context) { @@ -135,23 +142,23 @@ class InventoryViewModel : ViewModel() { 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) + prefs.edit { + putString("notes", _notes.value) + putString("branch", _branchName.value) + 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) + } + putString("machines", jsonArray.toString()) } - editor.putString("machines", jsonArray.toString()) - editor.apply() } fun setSearchQuery(query: String) { @@ -199,6 +206,9 @@ class InventoryViewModel : ViewModel() { val json = JSONObject(result) val newVersionCode = json.getInt("versionCode") + val isLockedFromServer = json.optBoolean("isLocked", false) + + _isLocked.value = isLockedFromServer val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0) @Suppress("DEPRECATION") @@ -218,6 +228,33 @@ class InventoryViewModel : ViewModel() { } } + fun checkLockAndRun(onNotLocked: () -> Unit, onLocked: () -> Unit) { + viewModelScope.launch(Dispatchers.IO) { + var locked = false + try { + val url = URL("https://bbjsolution.org/AR/InwentARyzacja/raw/branch/master/update.json") + val connection = url.openConnection() as HttpURLConnection + connection.connectTimeout = 3000 + connection.readTimeout = 3000 + + if (connection.responseCode == 200) { + val scanner = Scanner(connection.inputStream).useDelimiter("\\A") + val result = if (scanner.hasNext()) scanner.next() else "" + val json = JSONObject(result) + locked = json.optBoolean("isLocked", false) + _isLocked.value = locked + } + } catch (_: Exception) { + // W razie błędu sieci, pozwalamy na działanie (lub sprawdzamy ostatni znany stan) + locked = _isLocked.value + } + + launch(Dispatchers.Main) { + if (locked) onLocked() else onNotLocked() + } + } + } + fun getUniqueStatuses(): List { return _machines.value .map { it.originalStatus } @@ -259,17 +296,24 @@ class InventoryViewModel : ViewModel() { 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 + + + // Powiadomienie o nowym pliku + if (branch.isNotEmpty()) { + sendTelegramNotification(cleanedBranch) + } } catch (e: Exception) { e.printStackTrace() } @@ -306,6 +350,26 @@ class InventoryViewModel : ViewModel() { e.printStackTrace() } } + + private fun sendTelegramNotification(content: String) { + viewModelScope.launch(Dispatchers.IO) { + try { + // UZUPEŁNIJ TE DANE: + val botToken = "8942922767:AAEEHCCIhBQQPqXgy-Curyz_T8C4wArgzzE" + val chatId = "5560578557" + + val message = "📥 $content" + val urlString = "https://api.telegram.org/bot$botToken/sendMessage?chat_id=$chatId&text=${URLEncoder.encode(message, "UTF-8")}" + val url = URL(urlString) + val connection = url.openConnection() as HttpURLConnection + connection.requestMethod = "GET" + connection.connectTimeout = 5000 + connection.responseCode + } catch (_: Exception) { + // Cichy błąd, żeby nie psuć UX + } + } + } } // ========================================== @@ -381,7 +445,7 @@ fun SplashScreen() { // ========================================== // 4. EKRAN GŁÓWNY // ========================================== -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) @Composable fun InventoryScreen(viewModel: InventoryViewModel = viewModel()) { val context = LocalContext.current @@ -393,11 +457,16 @@ fun InventoryScreen(viewModel: InventoryViewModel = viewModel()) { val reportDate by viewModel.reportDate.collectAsState() val updateInfo by viewModel.updateInfo.collectAsState() val isNumericOnlySearch by viewModel.isNumericOnlySearch.collectAsState() + val isLocked by viewModel.isLocked.collectAsState() + + val isImeVisible = WindowInsets.isImeVisible var showStatusDialog by remember { mutableStateOf(false) } var showNotesDialog by remember { mutableStateOf(false) } var showUpdateDialog by remember { mutableStateOf(false) } + var showLockDialog by remember { mutableStateOf(false) } var showEasterEggDialog by remember { mutableStateOf(false) } + var isCheckingLock by remember { mutableStateOf(false) } LaunchedEffect(searchQuery) { if (searchQuery == "666666") { @@ -448,7 +517,7 @@ fun InventoryScreen(viewModel: InventoryViewModel = viewModel()) { }, confirmButton = { TextButton(onClick = { - val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://ar.bbjsolution.org")) + val intent = Intent(Intent.ACTION_VIEW, "https://ar.bbjsolution.org".toUri()) context.startActivity(intent) showUpdateDialog = false }) { @@ -464,6 +533,25 @@ fun InventoryScreen(viewModel: InventoryViewModel = viewModel()) { ) } + if (showLockDialog) { + AlertDialog( + onDismissRequest = { showLockDialog = false }, + title = { Text("Funkcja zablokowana", color = Color.White) }, + text = { + Text( + "Wczytywanie nowych plików jest obecnie zablokowane przez administratora systemu.", + color = Color.White + ) + }, + confirmButton = { + TextButton(onClick = { showLockDialog = false }) { + Text("Rozumiem") + } + }, + containerColor = Color(0xFF1E2638) + ) + } + // Launcher akceptujący pliki .xlsx oraz .xls val filePickerLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.GetContent() @@ -537,10 +625,24 @@ fun InventoryScreen(viewModel: InventoryViewModel = viewModel()) { horizontalArrangement = Arrangement.spacedBy(4.dp) ) { DashboardTile( - title = "Wczytaj", + title = if (isCheckingLock) "Sprawdzam..." else "Wczytaj", icon = Icons.Default.FileDownload, - color = Color(0xFF007ACC), - onClick = { filePickerLauncher.launch("*/*") }, + color = if (isLocked) Color.Gray else Color(0xFF007ACC), + onClick = { + if (!isCheckingLock) { + isCheckingLock = true + viewModel.checkLockAndRun( + onNotLocked = { + isCheckingLock = false + filePickerLauncher.launch("*/*") + }, + onLocked = { + isCheckingLock = false + showLockDialog = true + } + ) + } + }, modifier = Modifier.weight(1f) ) DashboardTile( @@ -574,49 +676,48 @@ fun InventoryScreen(viewModel: InventoryViewModel = viewModel()) { 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)) - } + Column(modifier = Modifier.padding(horizontal = 8.dp, vertical = 2.dp)) { Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically ) { - Text("Postęp:", color = Color.Gray, fontSize = 12.sp) Text( - "$checkedCount / $totalCount (${(progress * 100).toInt()}%)", + text = if (branchName.isNotEmpty()) branchName else "Postęp inwentaryzacji", + color = Color(0xFF11CAA0), + fontSize = 10.sp, + fontWeight = FontWeight.Bold, + maxLines = 1, + modifier = Modifier.weight(1f) + ) + Text( + text = "$checkedCount / $totalCount (${(progress * 100).toInt()}%)", color = Color.White, fontWeight = FontWeight.Bold, - fontSize = 12.sp + fontSize = 10.sp ) } - Spacer(modifier = Modifier.height(4.dp)) + + Spacer(modifier = Modifier.height(2.dp)) + LinearProgressIndicator( progress = { progress }, modifier = Modifier .fillMaxWidth() - .height(4.dp) + .height(3.dp) .clip(RoundedCornerShape(2.dp)), color = Color(0xFF11CAA0), trackColor = Color(0xFF2A3447) ) + + if (reportDate.isNotEmpty()) { + Text( + text = reportDate, + color = Color.Gray, + fontSize = 8.sp, + modifier = Modifier.align(Alignment.End) + ) + } } } @@ -646,6 +747,18 @@ fun InventoryScreen(viewModel: InventoryViewModel = viewModel()) { }, placeholder = { Text("Szukaj...", fontSize = 12.sp, color = Color.Gray) }, modifier = Modifier.fillMaxWidth().height(48.dp), + trailingIcon = { + if (searchQuery.isNotEmpty()) { + IconButton(onClick = { viewModel.clearSearch() }) { + Icon( + imageVector = Icons.Default.Clear, + contentDescription = "Wyczyść", + tint = Color.Gray, + modifier = Modifier.size(20.dp) + ) + } + } + }, shape = RoundedCornerShape(8.dp), singleLine = true, textStyle = LocalTextStyle.current.copy(fontSize = 13.sp), @@ -661,21 +774,23 @@ fun InventoryScreen(viewModel: InventoryViewModel = viewModel()) { 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() - } - ) + // Klawiatura numeryczna na dole - ukrywana gdy widoczna systemowa + if (!isImeVisible) { + NumericKeyboard( + onDigitClick = { + viewModel.setNumericOnlySearch(true) + viewModel.appendDigit(it) + }, + onBackspace = { + viewModel.setNumericOnlySearch(true) + viewModel.backspace() + }, + onClear = { + viewModel.setNumericOnlySearch(true) + viewModel.clearSearch() + } + ) + } } else { Box( modifier = Modifier @@ -684,7 +799,7 @@ fun InventoryScreen(viewModel: InventoryViewModel = viewModel()) { contentAlignment = Alignment.Center ) { Text( - text = "Brak wczytanych maszyn.\nWybierz plik .xlsx wygenerowany z systemu.", + text = "Brak wczytanych maszyn.\nWybierz plik .xlsx wygenerowany z IRS.\nRaport nr 1\nZaznacz tylko identyfikowalne", color = Color.Gray, fontSize = 16.sp, textAlign = TextAlign.Center 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 aa0685b..5d378ec 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 @@ -23,4 +23,4 @@ val Pink80 = Color(0xFFEFB8C8) val Purple40 = Color(0xFF6650a4) val PurpleGrey40 = Color(0xFF625b71) -val Pink40 = Color(0xFF7D5260) \ No newline at end of file +val Pink40 = Color(0xFF7D5260) 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 6e07a0c..01cfde3 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 @@ -15,7 +15,6 @@ package com.bartek.iwentaryzacja.ui.theme -import android.app.Activity import android.os.Build import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme