16 Commits
Author SHA1 Message Date
rapafun 14992d8148 Merge remote-tracking branch 'main/master' 2026-08-19 01:28:15 +02:00
rapafun a0817af855 ver 1.6 kosmetyka 2026-08-19 01:28:01 +02:00
rapafun 27aa4e5f88 ver 1.6 sprzatanie kodu 2026-08-19 00:04:26 +02:00
rapafun 55309f8d79 ver 1.5 2026-08-18 19:51:50 +02:00
rapafun abd70c3c54 ver 1.5 2026-08-18 19:49:28 +02:00
rapafun 7e947374b0 Merge remote-tracking branch 'main/master' 2026-08-18 18:18:30 +02:00
rapafun 889b1b03eb ver 1.2 2026-08-18 18:18:12 +02:00
rapafun 68c4e857a3 Update update.json 2026-08-17 16:30:48 +00:00
rapafun 54694ddf5c Update update.json 2026-08-17 16:27:30 +00:00
rapafun 3a34979173 Add index.html 2026-08-16 12:44:21 +00:00
rapafun b79a44f2b0 Merge remote-tracking branch 'main/master' 2026-08-16 14:28:12 +02:00
rapafun cd09e21230 ver 1.1 2026-08-16 14:27:59 +02:00
rapafun 3c5b28bb4f Update update.json 2026-08-16 12:13:01 +00:00
rapafun 1d34801eca Update update.json 2026-08-15 18:46:17 +00:00
rapafun 570c9d33d8 Update update.json 2026-08-15 18:38:27 +00:00
rapafun 20c683370d ikona 2026-08-15 19:38:00 +02:00
17 changed files with 500 additions and 195 deletions
+1 -1
View File
@@ -4,7 +4,7 @@
<selectionStates> <selectionStates>
<SelectionState runConfigName="app"> <SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" /> <option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2026-08-15T15:00:18.810512300Z"> <DropdownSelection timestamp="2026-08-16T12:33:41.457151500Z">
<Target type="DEFAULT_BOOT"> <Target type="DEFAULT_BOOT">
<handle> <handle>
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\rapaf\.android\avd\Pixel_9a.avd" /> <DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\rapaf\.android\avd\Pixel_9a.avd" />
+13
View File
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DeviceTable">
<option name="columnSorters">
<list>
<ColumnSorterState>
<option name="column" value="Name" />
<option name="order" value="ASCENDING" />
</ColumnSorterState>
</list>
</option>
</component>
</project>
+6 -2
View File
@@ -1,3 +1,7 @@
/*
* Właścicielem tego kodu jest Bartłomiej Janik.
*/
plugins { plugins {
alias(libs.plugins.android.application) alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.compose) alias(libs.plugins.kotlin.compose)
@@ -13,8 +17,8 @@ android {
applicationId = "com.bartek.iwentaryzacja" applicationId = "com.bartek.iwentaryzacja"
minSdk = 26 minSdk = 26
targetSdk = 37 targetSdk = 37
versionCode = 1 versionCode = 8
versionName = "1.0" versionName = "1.5"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
Binary file not shown.
Binary file not shown.
+37
View File
@@ -0,0 +1,37 @@
{
"version": 3,
"artifactType": {
"type": "APK",
"kind": "Directory"
},
"applicationId": "com.bartek.iwentaryzacja",
"variantName": "release",
"elements": [
{
"type": "SINGLE",
"filters": [],
"attributes": [],
"versionCode": 1,
"versionName": "1.0",
"outputFile": "app-release.apk"
}
],
"elementType": "File",
"baselineProfiles": [
{
"minApi": 28,
"maxApi": 30,
"baselineProfiles": [
"baselineProfiles/1/app-release.dm"
]
},
{
"minApi": 31,
"maxApi": 2147483647,
"baselineProfiles": [
"baselineProfiles/0/app-release.dm"
]
}
],
"minSdkVersionForDexing": 26
}
Binary file not shown.
@@ -1,3 +1,7 @@
/*
* Właścicielem tego kodu jest Bartłomiej Janik.
*/
package com.bartek.iwentaryzacja package com.bartek.iwentaryzacja
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
-11
View File
@@ -3,7 +3,6 @@
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<application <application
android:allowBackup="true" android:allowBackup="true"
@@ -15,16 +14,6 @@
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.IwentARyzacja"> android:theme="@style/Theme.IwentARyzacja">
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PATHS"
android:resource="@xml/file_paths" />
</provider>
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
@@ -1,4 +1,6 @@
/* /*
* Właścicielem tego kodu jest Bartłomiej Janik.
*
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or * the Free Software Foundation, either version 3 of the License, or
@@ -15,20 +17,16 @@
package com.bartek.iwentaryzacja package com.bartek.iwentaryzacja
import android.app.DownloadManager
import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Environment
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
@@ -49,20 +47,18 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.apache.poi.ss.usermodel.WorkbookFactory import org.apache.poi.ss.usermodel.WorkbookFactory
import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
import java.io.File
import java.io.InputStream import java.io.InputStream
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.URL import java.net.URL
@@ -95,64 +91,88 @@ class InventoryViewModel : ViewModel() {
private val _notes = MutableStateFlow("") private val _notes = MutableStateFlow("")
val notes: StateFlow<String> = _notes.asStateFlow() val notes: StateFlow<String> = _notes.asStateFlow()
private val _branchName = MutableStateFlow("")
val branchName: StateFlow<String> = _branchName.asStateFlow()
private val _reportDate = MutableStateFlow("")
val reportDate: StateFlow<String> = _reportDate.asStateFlow()
private val _isNumericOnlySearch = MutableStateFlow(true)
val isNumericOnlySearch: StateFlow<Boolean> = _isNumericOnlySearch.asStateFlow()
private val _updateInfo = MutableStateFlow<UpdateInfo?>(null) private val _updateInfo = MutableStateFlow<UpdateInfo?>(null)
val updateInfo: StateFlow<UpdateInfo?> = _updateInfo.asStateFlow() val updateInfo: StateFlow<UpdateInfo?> = _updateInfo.asStateFlow()
data class UpdateInfo(val versionCode: Int, val apkUrl: String) data class UpdateInfo(val versionCode: Int)
fun checkForUpdates(context: Context) { /**
viewModelScope.launch(Dispatchers.IO) { * Wczytuje stan aplikacji (notatki i listę maszyn) z pamięci urządzenia (SharedPreferences).
*/
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 { try {
val url = URL("https://bbjsolution.org/AR/InwentARyzacja/src/branch/master/update.json") val jsonArray = JSONArray(machinesJson)
val connection = url.openConnection() as HttpURLConnection val loadedList = mutableListOf<Machine>()
connection.connectTimeout = 5000 for (i in 0 until jsonArray.length()) {
connection.readTimeout = 5000 val obj = jsonArray.getJSONObject(i)
loadedList.add(
if (connection.responseCode == 200) { Machine(
val scanner = Scanner(connection.inputStream).useDelimiter("\\A") id = obj.getString("id"),
val result = if (scanner.hasNext()) scanner.next() else "" name = obj.getString("name"),
val json = JSONObject(result) category = obj.getString("category"),
originalStatus = obj.getString("originalStatus"),
val newVersionCode = json.getInt("versionCode") isChecked = obj.getBoolean("isChecked")
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)
}
} }
_machines.value = loadedList
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
} }
} }
fun downloadAndInstall(context: Context, apkUrl: String) { /**
val request = DownloadManager.Request(Uri.parse(apkUrl)) * Zapisuje bieżący stan aplikacji do pamięci urządzenia.
.setTitle("Aktualizacja inwentARyzacja") */
.setDescription("Pobieranie nowej wersji...") private fun saveState(context: Context) {
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) val prefs = context.getSharedPreferences("inventory_prefs", Context.MODE_PRIVATE)
.setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, "update.apk") val editor = prefs.edit()
.setAllowedOverMetered(true) editor.putString("notes", _notes.value)
.setAllowedOverRoaming(true) editor.putString("branch", _branchName.value)
editor.putString("date", _reportDate.value)
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager val jsonArray = JSONArray()
downloadManager.enqueue(request) _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) { fun setSearchQuery(query: String) {
_searchQuery.value = query _searchQuery.value = query
} }
fun setNotes(newNotes: String) { fun setNumericOnlySearch(isNumeric: Boolean) {
_isNumericOnlySearch.value = isNumeric
}
fun setNotes(context: Context, newNotes: String) {
_notes.value = newNotes _notes.value = newNotes
saveState(context)
} }
fun appendDigit(digit: String) { fun appendDigit(digit: String) {
@@ -173,6 +193,42 @@ class InventoryViewModel : ViewModel() {
_filterMissing.value = !_filterMissing.value _filterMissing.value = !_filterMissing.value
} }
/**
* Sprawdza dostępność aktualizacji na serwerze zdalnym.
*/
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<String> { fun getUniqueStatuses(): List<String> {
return _machines.value return _machines.value
.map { it.originalStatus } .map { it.originalStatus }
@@ -181,7 +237,7 @@ class InventoryViewModel : ViewModel() {
.sorted() .sorted()
} }
fun markMachinesByStatuses(statuses: Set<String>, isChecked: Boolean) { fun markMachinesByStatuses(context: Context, statuses: Set<String>, isChecked: Boolean) {
_machines.value = _machines.value.map { machine -> _machines.value = _machines.value.map { machine ->
if (statuses.contains(machine.originalStatus)) { if (statuses.contains(machine.originalStatus)) {
machine.copy(isChecked = isChecked) machine.copy(isChecked = isChecked)
@@ -189,9 +245,10 @@ class InventoryViewModel : ViewModel() {
machine machine
} }
} }
saveState(context)
} }
fun toggleMachineCheck(machineId: String) { fun toggleMachineCheck(context: Context, machineId: String) {
_machines.value = _machines.value.map { machine -> _machines.value = _machines.value.map { machine ->
if (machine.id == machineId) { if (machine.id == machineId) {
machine.copy(isChecked = !machine.isChecked) machine.copy(isChecked = !machine.isChecked)
@@ -199,17 +256,41 @@ class InventoryViewModel : ViewModel() {
machine machine
} }
} }
saveState(context)
} }
// Parser zoptymalizowany pod strukturę raportu z systemu /**
fun parseXlsFile(inputStream: InputStream) { * Parsuje plik Excel (.xls lub .xlsx) i wyodrębnia listę maszyn oraz nagłówki raportu.
*/
fun parseXlsFile(context: Context, inputStream: InputStream) {
viewModelScope.launch(Dispatchers.IO) {
try { try {
// WorkbookFactory automatycznie obsługuje zarówno .xls jak i .xlsx // WorkbookFactory automatycznie obsługuje zarówno .xls jak i .xlsx
val workbook = WorkbookFactory.create(inputStream) val workbook = WorkbookFactory.create(inputStream)
val sheet = workbook.getSheetAt(0) val sheet = workbook.getSheetAt(0)
// Próba pobrania nazwy oddziału i daty z nagłówka (wiersz 0)
try {
val headerRow = sheet.getRow(0)
val branch = headerRow?.getCell(0)?.toString()?.trim() ?: ""
val date = headerRow?.getCell(4)?.toString()?.trim() ?: ""
// 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<Machine>() val parsedList = mutableListOf<Machine>()
// Dane zaczynają się od wiersza indeks 2 (po 2 wierszach nagłówka) // Dane maszyn zaczynają się zazwyczaj od wiersza indeks 2
for (rowIndex in 2..sheet.lastRowNum) { for (rowIndex in 2..sheet.lastRowNum) {
val row = sheet.getRow(rowIndex) ?: continue val row = sheet.getRow(rowIndex) ?: continue
@@ -220,60 +301,36 @@ class InventoryViewModel : ViewModel() {
// Wczytujemy tylko, gdy pozycja ma nazwę i nr ewidencyjny // Wczytujemy tylko, gdy pozycja ma nazwę i nr ewidencyjny
if (name.isNotEmpty() && id.isNotEmpty()) { if (name.isNotEmpty() && id.isNotEmpty()) {
val isRented = status.contains("wypo", ignoreCase = true)
parsedList.add( parsedList.add(
Machine( Machine(
id = id, id = id,
name = name, name = name,
category = category, category = category,
originalStatus = status originalStatus = status,
isChecked = isRented // Automatycznie zaznaczamy wypożyczone
) )
) )
} }
} }
_machines.value = parsedList _machines.value = parsedList
workbook.close() workbook.close()
saveState(context)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
} }
} }
}
// ========================================== // ==========================================
// 3. GŁÓWNA AKTYWNOŚĆ // 3. GŁÓWNA AKTYWNOŚĆ
// ========================================== // ==========================================
class MainActivity : ComponentActivity() { 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
enableEdgeToEdge() enableEdgeToEdge()
ContextCompat.registerReceiver(
this,
onDownloadComplete,
IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE),
ContextCompat.RECEIVER_EXPORTED
)
setContent { setContent {
MaterialTheme( MaterialTheme(
colorScheme = darkColorScheme( colorScheme = darkColorScheme(
@@ -282,24 +339,69 @@ class MainActivity : ComponentActivity() {
surface = Color(0xFF1E2638) surface = Color(0xFF1E2638)
) )
) { ) {
var showSplashScreen by remember { mutableStateOf(true) }
LaunchedEffect(Unit) {
delay(2000) // Ekran startowy przez 2 sekundy
showSplashScreen = false
}
Surface( Surface(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background color = MaterialTheme.colorScheme.background
) { ) {
if (showSplashScreen) {
SplashScreen()
} else {
InventoryScreen() InventoryScreen()
} }
} }
} }
} }
override fun onDestroy() { }
super.onDestroy() }
unregisterReceiver(onDownloadComplete)
/**
* Ekran startowy aplikacji wyświetlany przez krótki czas po uruchomieniu.
*/
@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 // 4. EKRAN GŁÓWNY
// ========================================== // ==========================================
/**
* Główny ekran aplikacji zarządzający listą maszyn i interfejsem użytkownika.
*/
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun InventoryScreen(viewModel: InventoryViewModel = viewModel()) { fun InventoryScreen(viewModel: InventoryViewModel = viewModel()) {
@@ -308,13 +410,44 @@ fun InventoryScreen(viewModel: InventoryViewModel = viewModel()) {
val searchQuery by viewModel.searchQuery.collectAsState() val searchQuery by viewModel.searchQuery.collectAsState()
val filterMissing by viewModel.filterMissing.collectAsState() val filterMissing by viewModel.filterMissing.collectAsState()
val notes by viewModel.notes.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 updateInfo by viewModel.updateInfo.collectAsState()
val isNumericOnlySearch by viewModel.isNumericOnlySearch.collectAsState()
var showStatusDialog by remember { mutableStateOf(false) } var showStatusDialog by remember { mutableStateOf(false) }
var showNotesDialog by remember { mutableStateOf(false) } var showNotesDialog by remember { mutableStateOf(false) }
var showUpdateDialog 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) { LaunchedEffect(Unit) {
viewModel.loadState(context)
viewModel.checkForUpdates(context) viewModel.checkForUpdates(context)
} }
@@ -324,17 +457,23 @@ fun InventoryScreen(viewModel: InventoryViewModel = viewModel()) {
} }
} }
if (showUpdateDialog && updateInfo != null) { if (showUpdateDialog) {
AlertDialog( AlertDialog(
onDismissRequest = { showUpdateDialog = false }, onDismissRequest = { showUpdateDialog = false },
title = { Text("Dostępna aktualizacja", color = Color.White) }, title = { Text("Nowa wersja dostępna", color = Color.White) },
text = { Text("Czy chcesz pobrać i zainstalować nową wersję aplikacji?", color = Color.White) }, text = {
Text(
"Dostępna jest nowsza wersja aplikacji. Pobierz ją ze strony:\nar.bbjsolution.org",
color = Color.White
)
},
confirmButton = { confirmButton = {
Button(onClick = { TextButton(onClick = {
viewModel.downloadAndInstall(context, updateInfo!!.apkUrl) val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://ar.bbjsolution.org"))
context.startActivity(intent)
showUpdateDialog = false showUpdateDialog = false
}) { }) {
Text("Pobierz") Text("Otwórz stronę")
} }
}, },
dismissButton = { dismissButton = {
@@ -352,7 +491,7 @@ fun InventoryScreen(viewModel: InventoryViewModel = viewModel()) {
) { uri: Uri? -> ) { uri: Uri? ->
uri?.let { uri?.let {
context.contentResolver.openInputStream(it)?.use { inputStream -> context.contentResolver.openInputStream(it)?.use { inputStream ->
viewModel.parseXlsFile(inputStream) viewModel.parseXlsFile(context, inputStream)
} }
} }
} }
@@ -362,7 +501,7 @@ fun InventoryScreen(viewModel: InventoryViewModel = viewModel()) {
statuses = viewModel.getUniqueStatuses(), statuses = viewModel.getUniqueStatuses(),
onDismiss = { showStatusDialog = false }, onDismiss = { showStatusDialog = false },
onConfirm = { selectedStatuses -> onConfirm = { selectedStatuses ->
viewModel.markMachinesByStatuses(selectedStatuses, true) viewModel.markMachinesByStatuses(context, selectedStatuses, true)
showStatusDialog = false showStatusDialog = false
} }
) )
@@ -371,18 +510,22 @@ fun InventoryScreen(viewModel: InventoryViewModel = viewModel()) {
if (showNotesDialog) { if (showNotesDialog) {
NotesDialog( NotesDialog(
notes = notes, notes = notes,
onNotesChange = { viewModel.setNotes(it) }, onNotesChange = { viewModel.setNotes(context, it) },
onDismiss = { showNotesDialog = false } onDismiss = { showNotesDialog = false }
) )
} }
val filteredMachines = remember(machines, searchQuery, filterMissing) { val filteredMachines = remember(machines, searchQuery, filterMissing, isNumericOnlySearch) {
var list = if (searchQuery.isBlank()) machines var list = if (searchQuery.isBlank()) machines
else machines.filter { else machines.filter {
if (isNumericOnlySearch) {
it.id.contains(searchQuery, ignoreCase = true)
} else {
it.name.contains(searchQuery, ignoreCase = true) || it.name.contains(searchQuery, ignoreCase = true) ||
it.id.contains(searchQuery, ignoreCase = true) || it.id.contains(searchQuery, ignoreCase = true) ||
it.category.contains(searchQuery, ignoreCase = true) it.category.contains(searchQuery, ignoreCase = true)
} }
}
if (filterMissing) { if (filterMissing) {
list = list.filter { !it.isChecked } list = list.filter { !it.isChecked }
@@ -390,8 +533,12 @@ fun InventoryScreen(viewModel: InventoryViewModel = viewModel()) {
list list
} }
val checkedCount = machines.count { it.isChecked } val checkedCount = machines.count {
val totalCount = machines.size 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 val progress = if (totalCount > 0) checkedCount.toFloat() / totalCount.toFloat() else 0f
Scaffold( Scaffold(
@@ -448,25 +595,46 @@ fun InventoryScreen(viewModel: InventoryViewModel = viewModel()) {
shape = RoundedCornerShape(8.dp), shape = RoundedCornerShape(8.dp),
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) { ) {
Column(modifier = Modifier.padding(8.dp)) { Column(modifier = Modifier.padding(vertical = 2.dp, horizontal = 8.dp)) {
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) { ) {
Text("Postęp:", color = Color.Gray, fontSize = 12.sp) Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.weight(1f)) {
if (branchName.isNotEmpty()) {
Text( Text(
"$checkedCount / $totalCount (${(progress * 100).toInt()}%)", text = branchName,
color = Color.White, color = Color(0xFF11CAA0),
fontSize = 10.sp,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
fontSize = 12.sp maxLines = 1
)
Spacer(modifier = Modifier.width(6.dp))
}
if (reportDate.isNotEmpty()) {
Text(
text = reportDate,
color = Color.Gray,
fontSize = 9.sp
)
}
if (branchName.isEmpty() && reportDate.isEmpty()) {
Text("Postęp:", color = Color.Gray, fontSize = 10.sp)
}
}
Text(
text = "$checkedCount/$totalCount (${(progress * 100).toInt()}%)",
color = Color.White,
fontWeight = FontWeight.Bold,
fontSize = 11.sp
) )
} }
Spacer(modifier = Modifier.height(4.dp))
LinearProgressIndicator( LinearProgressIndicator(
progress = { progress }, progress = { progress },
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(4.dp) .height(3.dp)
.clip(RoundedCornerShape(2.dp)), .clip(RoundedCornerShape(2.dp)),
color = Color(0xFF11CAA0), color = Color(0xFF11CAA0),
trackColor = Color(0xFF2A3447) trackColor = Color(0xFF2A3447)
@@ -478,13 +646,13 @@ fun InventoryScreen(viewModel: InventoryViewModel = viewModel()) {
// Lista zajmuje elastyczną przestrzeń, ale zostanie ograniczona przez klawiaturę // Lista zajmuje elastyczną przestrzeń, ale zostanie ograniczona przez klawiaturę
LazyColumn( LazyColumn(
verticalArrangement = Arrangement.spacedBy(4.dp), verticalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier.weight(1f) // Używamy weight, aby zajęła resztę miejsca przed klawiaturą modifier = Modifier.weight(1f) // Używamy weight, aby zajęła resztę miejsca przed klawiaturą
) { ) {
items(filteredMachines, key = { it.id }) { machine -> items(filteredMachines, key = { it.id }) { machine ->
MachineItem( MachineItem(
machine = machine, machine = machine,
onToggle = { viewModel.toggleMachineCheck(machine.id) } onToggle = { viewModel.toggleMachineCheck(context, machine.id) }
) )
} }
} }
@@ -494,8 +662,11 @@ fun InventoryScreen(viewModel: InventoryViewModel = viewModel()) {
// Bardzo mały pasek szukania nad klawiaturą // Bardzo mały pasek szukania nad klawiaturą
OutlinedTextField( OutlinedTextField(
value = searchQuery, value = searchQuery,
onValueChange = { viewModel.setSearchQuery(it) }, onValueChange = {
placeholder = { Text("Szukaj...", fontSize = 12.sp, color = Color.Gray) }, viewModel.setSearchQuery(it)
viewModel.setNumericOnlySearch(false)
},
placeholder = { Text("szukanie po nazwie", fontSize = 12.sp, color = Color.Gray) },
modifier = Modifier.fillMaxWidth().height(48.dp), modifier = Modifier.fillMaxWidth().height(48.dp),
shape = RoundedCornerShape(8.dp), shape = RoundedCornerShape(8.dp),
singleLine = true, singleLine = true,
@@ -514,9 +685,18 @@ fun InventoryScreen(viewModel: InventoryViewModel = viewModel()) {
// Klawiatura numeryczna na dole // Klawiatura numeryczna na dole
NumericKeyboard( NumericKeyboard(
onDigitClick = { viewModel.appendDigit(it) }, onDigitClick = {
onBackspace = { viewModel.backspace() }, viewModel.setNumericOnlySearch(true)
onClear = { viewModel.clearSearch() } viewModel.appendDigit(it)
},
onBackspace = {
viewModel.setNumericOnlySearch(true)
viewModel.backspace()
},
onClear = {
viewModel.setNumericOnlySearch(true)
viewModel.clearSearch()
}
) )
} else { } else {
Box( Box(
@@ -540,6 +720,9 @@ fun InventoryScreen(viewModel: InventoryViewModel = viewModel()) {
// ========================================== // ==========================================
// 5. POJEDYNCZY ELEMENT LISTY // 5. POJEDYNCZY ELEMENT LISTY
// ========================================== // ==========================================
/**
* Komponent pojedynczego kafelka w górnym menu nawigacyjnym.
*/
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun DashboardTile( fun DashboardTile(
@@ -575,6 +758,9 @@ fun DashboardTile(
} }
} }
/**
* Okno dialogowe umożliwiające zbiorcze zaznaczanie maszyn na podstawie ich statusu.
*/
@Composable @Composable
fun StatusSelectionDialog( fun StatusSelectionDialog(
statuses: List<String>, statuses: List<String>,
@@ -632,6 +818,9 @@ fun StatusSelectionDialog(
) )
} }
/**
* Okno dialogowe notatnika do zapisywania uwag.
*/
@Composable @Composable
fun NotesDialog( fun NotesDialog(
notes: String, notes: String,
@@ -664,6 +853,9 @@ fun NotesDialog(
) )
} }
/**
* Niestandardowa klawiatura numeryczna do szybkiego filtrowania po numerach ID.
*/
@Composable @Composable
fun NumericKeyboard( fun NumericKeyboard(
onDigitClick: (String) -> Unit, onDigitClick: (String) -> Unit,
@@ -733,6 +925,9 @@ fun KeyButton(
} }
} }
/**
* Komponent pojedynczego elementu na liście maszyn.
*/
@Composable @Composable
fun MachineItem( fun MachineItem(
machine: Machine, machine: Machine,
@@ -749,7 +944,7 @@ fun MachineItem(
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
.padding(8.dp) .padding(vertical = 4.dp, horizontal = 8.dp)
.fillMaxWidth(), .fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween
@@ -758,12 +953,12 @@ fun MachineItem(
Text( Text(
text = machine.id, text = machine.id,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
fontSize = 14.sp, fontSize = 13.sp,
color = Color.White color = Color.White
) )
Text( Text(
text = machine.name, text = machine.name,
fontSize = 11.sp, fontSize = 10.sp,
color = Color.LightGray, color = Color.LightGray,
maxLines = 1 maxLines = 1
) )
@@ -772,7 +967,7 @@ fun MachineItem(
if (machine.originalStatus.isNotEmpty()) { if (machine.originalStatus.isNotEmpty()) {
Text( Text(
text = machine.originalStatus, text = machine.originalStatus,
fontSize = 10.sp, fontSize = 11.sp,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
color = Color(0xFF11CAA0), color = Color(0xFF11CAA0),
modifier = Modifier.padding(start = 8.dp) modifier = Modifier.padding(start = 8.dp)
@@ -1,4 +1,6 @@
/* /*
* Właścicielem tego kodu jest Bartłomiej Janik.
*
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or * the Free Software Foundation, either version 3 of the License, or
@@ -17,6 +19,9 @@ package com.bartek.iwentaryzacja.ui.theme
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
/**
* Definicje kolorów używanych w motywie aplikacji.
*/
val Purple80 = Color(0xFFD0BCFF) val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC) val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8) val Pink80 = Color(0xFFEFB8C8)
@@ -1,4 +1,6 @@
/* /*
* Właścicielem tego kodu jest Bartłomiej Janik.
*
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or * the Free Software Foundation, either version 3 of the License, or
@@ -36,18 +38,11 @@ private val LightColorScheme = lightColorScheme(
primary = Purple40, primary = Purple40,
secondary = PurpleGrey40, secondary = PurpleGrey40,
tertiary = Pink40 tertiary = Pink40
/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
) )
/**
* Główny motyw aplikacji inwentARyzacja.
*/
@Composable @Composable
fun IwentARyzacjaTheme( fun IwentARyzacjaTheme(
darkTheme: Boolean = isSystemInDarkTheme(), darkTheme: Boolean = isSystemInDarkTheme(),
@@ -1,4 +1,6 @@
/* /*
* Właścicielem tego kodu jest Bartłomiej Janik.
*
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or * the Free Software Foundation, either version 3 of the License, or
@@ -21,7 +23,9 @@ import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with /**
* Definicje typografii dla interfejsu Compose.
*/
val Typography = Typography( val Typography = Typography(
bodyLarge = TextStyle( bodyLarge = TextStyle(
fontFamily = FontFamily.Default, fontFamily = FontFamily.Default,
@@ -30,20 +34,4 @@ val Typography = Typography(
lineHeight = 24.sp, lineHeight = 24.sp,
letterSpacing = 0.5.sp letterSpacing = 0.5.sp
) )
/* Other default text styles to override
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
) )
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path name="external_files" path="." />
<cache-path name="cache_files" path="." />
<files-path name="internal_files" path="." />
</paths>
@@ -1,3 +1,7 @@
/*
* Właścicielem tego kodu jest Bartłomiej Janik.
*/
package com.bartek.iwentaryzacja package com.bartek.iwentaryzacja
import org.junit.Test import org.junit.Test
+77
View File
@@ -0,0 +1,77 @@
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BBJ Solution - Pobierz Aplikację</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
}
body {
background-color: #0f172a;
color: #f8fafc;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
padding: 20px;
}
.card {
background-color: #1e293b;
border-radius: 16px;
padding: 32px 24px;
max-width: 420px;
width: 100%;
text-align: center;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.4);
border: 1px solid #334155;
}
h1 {
font-size: 24px;
margin-bottom: 8px;
color: #38bdf8;
}
p {
color: #94a3b8;
font-size: 14px;
margin-bottom: 24px;
line-height: 1.5;
}
.download-btn {
display: inline-block;
width: 100%;
padding: 14px 20px;
background-color: #0284c7;
color: #ffffff;
font-weight: bold;
font-size: 16px;
text-decoration: none;
border-radius: 8px;
transition: background-color 0.2s ease;
}
.download-btn:active {
background-color: #0369a1;
}
.info {
margin-top: 16px;
font-size: 12px;
color: #64748b;
}
</style>
</head>
<body>
<div class="card">
<h1>BBJ Solution</h1>
<p>Aplikacja mobilna do obsługi magazynu i urządzeń.</p>
<!-- Podmień app-release.apk na dokładną nazwę swojego pliku APK -->
<a href="app-release.apk" class="download-btn" download>Pobierz aplikację (APK)</a>
<p class="info">Po pobraniu zezwól w telefonie na instalację z nieznanych źródeł.</p>
</div>
</body>
</html>
+2 -2
View File
@@ -1,4 +1,4 @@
{ {
"versionCode": 2, "versionCode": 4,
"url": "bbjsolution.org/AR/InwentARyzacja/releases/download/ver1/ver1.apk" "url": "bbjsolution.org/AR/InwentARyzacja/releases/download/ver1.1/ver1.1.apk"
} }