25 Commits
Author SHA1 Message Date
rapafun f1da6d389f Update update.json 2026-08-26 21:04:27 +00:00
rapafun 70e030a6f5 Update update.json 2026-08-26 21:04:14 +00:00
rapafun b12f6970b2 Update update.json 2026-08-26 20:50:26 +00:00
rapafun 7d66c56948 Update update.json 2026-08-26 20:49:58 +00:00
rapafun 921bd63f11 Update update.json 2026-08-26 20:45:44 +00:00
rapafun a72b5ad6e7 Update update.json 2026-08-26 20:42:42 +00:00
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
rapafun de01fdb359 Merge remote-tracking branch 'main/master' 2026-08-15 19:36:53 +02:00
rapafun 5a128ffaa7 ikona 2026-08-15 19:36:18 +02:00
rapafun 47578c01c7 Update update.json 2026-08-15 17:16:00 +00:00
23 changed files with 1177 additions and 53 deletions
+7
View File
@@ -4,6 +4,13 @@
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2026-08-16T12:33:41.457151500Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\rapaf\.android\avd\Pixel_9a.avd" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState>
</selectionStates>
+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>
+1 -1
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_25" project-jdk-name="jbr-25" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_25" default="true" project-jdk-name="jbr-25" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
+18 -3
View File
@@ -1,3 +1,7 @@
/*
* Właścicielem tego kodu jest Bartłomiej Janik.
*/
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.compose)
@@ -11,10 +15,10 @@ android {
defaultConfig {
applicationId = "com.bartek.iwentaryzacja"
minSdk = 24
minSdk = 26
targetSdk = 37
versionCode = 1
versionName = "1.0"
versionCode = 8
versionName = "1.5"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@@ -36,6 +40,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)
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
import androidx.test.platform.app.InstrumentationRegistry
+3
View File
@@ -2,6 +2,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
@@ -11,6 +13,7 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.IwentARyzacja">
<activity
android:name=".MainActivity"
android:exported="true"
Binary file not shown.

After

Width:  |  Height:  |  Size: 441 KiB

@@ -1,47 +1,978 @@
/*
* Właścicielem tego kodu jest Bartłomiej Janik.
*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.bartek.iwentaryzacja
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.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.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.List
import androidx.compose.material.icons.filled.*
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.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.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import org.apache.poi.ss.usermodel.WorkbookFactory
import org.json.JSONArray
import org.json.JSONObject
import java.io.InputStream
import java.net.HttpURLConnection
import java.net.URL
import java.util.Scanner
// ==========================================
// 1. MODEL DANYCH MASZYNY
// ==========================================
data class Machine(
val id: String, // Nr ewid. (np. 7065/K)
val name: String, // Nazwa sprzętu
val category: String, // Kategoria (np. minikoparki)
val originalStatus: String, // Status z kolumny H
val isChecked: Boolean = false
)
// ==========================================
// 2. VIEWMODEL (DOPASOWANY PARSER .XLSX)
// ==========================================
class InventoryViewModel : ViewModel() {
private val _machines = MutableStateFlow<List<Machine>>(emptyList())
val machines: StateFlow<List<Machine>> = _machines.asStateFlow()
private val _searchQuery = MutableStateFlow("")
val searchQuery: StateFlow<String> = _searchQuery.asStateFlow()
private val _filterMissing = MutableStateFlow(false)
val filterMissing: StateFlow<Boolean> = _filterMissing.asStateFlow()
private val _notes = MutableStateFlow("")
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)
val updateInfo: StateFlow<UpdateInfo?> = _updateInfo.asStateFlow()
data class UpdateInfo(val versionCode: Int)
/**
* 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 {
val jsonArray = JSONArray(machinesJson)
val loadedList = mutableListOf<Machine>()
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()
}
}
}
/**
* Zapisuje bieżący stan aplikacji do pamięci urządzenia.
*/
private fun saveState(context: Context) {
val prefs = context.getSharedPreferences("inventory_prefs", Context.MODE_PRIVATE)
val editor = prefs.edit()
editor.putString("notes", _notes.value)
editor.putString("branch", _branchName.value)
editor.putString("date", _reportDate.value)
val jsonArray = JSONArray()
_machines.value.forEach { machine ->
val obj = JSONObject()
obj.put("id", machine.id)
obj.put("name", machine.name)
obj.put("category", machine.category)
obj.put("originalStatus", machine.originalStatus)
obj.put("isChecked", machine.isChecked)
jsonArray.put(obj)
}
editor.putString("machines", jsonArray.toString())
editor.apply()
}
fun setSearchQuery(query: String) {
_searchQuery.value = query
}
fun setNumericOnlySearch(isNumeric: Boolean) {
_isNumericOnlySearch.value = isNumeric
}
fun setNotes(context: Context, newNotes: String) {
_notes.value = newNotes
saveState(context)
}
fun appendDigit(digit: String) {
_searchQuery.value += digit
}
fun backspace() {
if (_searchQuery.value.isNotEmpty()) {
_searchQuery.value = _searchQuery.value.dropLast(1)
}
}
fun clearSearch() {
_searchQuery.value = ""
}
fun toggleFilterMissing() {
_filterMissing.value = !_filterMissing.value
}
/**
* 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> {
return _machines.value
.map { it.originalStatus }
.filter { it.isNotEmpty() }
.distinct()
.sorted()
}
fun markMachinesByStatuses(context: Context, statuses: Set<String>, 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)
}
/**
* 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 {
// WorkbookFactory automatycznie obsługuje zarówno .xls jak i .xlsx
val workbook = WorkbookFactory.create(inputStream)
val sheet = workbook.getSheetAt(0)
// Próba pobrania nazwy oddziału i daty z nagłówka (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>()
// Dane maszyn zaczynają się zazwyczaj od wiersza indeks 2
for (rowIndex in 2..sheet.lastRowNum) {
val row = sheet.getRow(rowIndex) ?: continue
val category = row.getCell(1)?.toString()?.trim() ?: ""
val id = row.getCell(2)?.toString()?.trim() ?: ""
val name = row.getCell(3)?.toString()?.trim() ?: ""
val status = row.getCell(7)?.toString()?.trim() ?: ""
// Wczytujemy tylko, gdy pozycja ma nazwę i nr ewidencyjny
if (name.isNotEmpty() && id.isNotEmpty()) {
val isRented = status.contains("wypo", ignoreCase = true)
parsedList.add(
Machine(
id = id,
name = name,
category = category,
originalStatus = status,
isChecked = isRented // Automatycznie zaznaczamy wypożyczone
)
)
}
}
_machines.value = parsedList
workbook.close()
saveState(context)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}
// ==========================================
// 3. GŁÓWNA AKTYWNOŚĆ
// ==========================================
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
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)
)
) {
var showSplashScreen by remember { mutableStateOf(true) }
LaunchedEffect(Unit) {
delay(2000) // Ekran startowy przez 2 sekundy
showSplashScreen = false
}
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
if (showSplashScreen) {
SplashScreen()
} else {
InventoryScreen()
}
}
}
}
}
}
/**
* Ekran startowy aplikacji wyświetlany przez krótki czas po uruchomieniu.
*/
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
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 = "Hello $name!",
modifier = modifier
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
// ==========================================
/**
* Główny ekran aplikacji zarządzający listą maszyn i interfejsem użytkownika.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun InventoryScreen(viewModel: InventoryViewModel = viewModel()) {
val context = LocalContext.current
val machines by viewModel.machines.collectAsState()
val searchQuery by viewModel.searchQuery.collectAsState()
val filterMissing by viewModel.filterMissing.collectAsState()
val notes by viewModel.notes.collectAsState()
val branchName by viewModel.branchName.collectAsState()
val reportDate by viewModel.reportDate.collectAsState()
val updateInfo by viewModel.updateInfo.collectAsState()
val isNumericOnlySearch by viewModel.isNumericOnlySearch.collectAsState()
var showStatusDialog by remember { mutableStateOf(false) }
var showNotesDialog by remember { mutableStateOf(false) }
var showUpdateDialog by remember { mutableStateOf(false) }
var showEasterEggDialog by remember { mutableStateOf(false) }
LaunchedEffect(searchQuery) {
if (searchQuery == "666666") {
showEasterEggDialog = true
viewModel.clearSearch()
}
}
if (showEasterEggDialog) {
AlertDialog(
onDismissRequest = { showEasterEggDialog = false },
title = { Text("O aplikacji", color = Color.White) },
text = {
Text(
"Aplikacja zrobiona w całości przez Bartka J, w wolnym czasie i na prywatnych komputerach",
color = Color.White
)
},
confirmButton = {
TextButton(onClick = { showEasterEggDialog = false }) {
Text("OK")
}
},
containerColor = Color(0xFF1E2638)
)
}
@Preview(showBackground = true)
LaunchedEffect(Unit) {
viewModel.loadState(context)
viewModel.checkForUpdates(context)
}
LaunchedEffect(updateInfo) {
if (updateInfo != null) {
showUpdateDialog = true
}
}
if (showUpdateDialog) {
AlertDialog(
onDismissRequest = { showUpdateDialog = false },
title = { Text("Nowa wersja dostępna", color = Color.White) },
text = {
Text(
"Dostępna jest nowsza wersja aplikacji. Pobierz ją ze strony:\nar.bbjsolution.org",
color = Color.White
)
},
confirmButton = {
TextButton(onClick = {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://ar.bbjsolution.org"))
context.startActivity(intent)
showUpdateDialog = false
}) {
Text("Otwórz stronę")
}
},
dismissButton = {
TextButton(onClick = { showUpdateDialog = false }) {
Text("Później")
}
},
containerColor = Color(0xFF1E2638)
)
}
// Launcher akceptujący pliki .xlsx oraz .xls
val filePickerLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.GetContent()
) { uri: Uri? ->
uri?.let {
context.contentResolver.openInputStream(it)?.use { inputStream ->
viewModel.parseXlsFile(context, inputStream)
}
}
}
if (showStatusDialog) {
StatusSelectionDialog(
statuses = viewModel.getUniqueStatuses(),
onDismiss = { showStatusDialog = false },
onConfirm = { selectedStatuses ->
viewModel.markMachinesByStatuses(context, selectedStatuses, true)
showStatusDialog = false
}
)
}
if (showNotesDialog) {
NotesDialog(
notes = notes,
onNotesChange = { viewModel.setNotes(context, it) },
onDismiss = { showNotesDialog = false }
)
}
val filteredMachines = remember(machines, searchQuery, filterMissing, isNumericOnlySearch) {
var list = if (searchQuery.isBlank()) machines
else machines.filter {
if (isNumericOnlySearch) {
it.id.contains(searchQuery, ignoreCase = true)
} else {
it.name.contains(searchQuery, ignoreCase = true) ||
it.id.contains(searchQuery, ignoreCase = true) ||
it.category.contains(searchQuery, ignoreCase = true)
}
}
if (filterMissing) {
list = list.filter { !it.isChecked }
}
list
}
val checkedCount = machines.count {
it.isChecked && !it.originalStatus.contains("wypo", ignoreCase = true)
}
val totalCount = machines.count {
!it.originalStatus.contains("wypo", ignoreCase = true)
}
val progress = if (totalCount > 0) checkedCount.toFloat() / totalCount.toFloat() else 0f
Scaffold(
modifier = Modifier.fillMaxSize(),
containerColor = MaterialTheme.colorScheme.background,
contentWindowInsets = WindowInsets.safeDrawing
) { innerPadding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
.padding(horizontal = 16.dp, vertical = 8.dp)
) {
// Cztery małe kafelki w jednej linii
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
DashboardTile(
title = "Wczytaj",
icon = Icons.Default.FileDownload,
color = Color(0xFF007ACC),
onClick = { filePickerLauncher.launch("*/*") },
modifier = Modifier.weight(1f)
)
DashboardTile(
title = if (filterMissing) "Wszystkie" else "Brakujące",
icon = if (filterMissing) Icons.AutoMirrored.Filled.List else Icons.Default.Search,
color = if (filterMissing) Color(0xFFFF9800) else Color(0xFFE91E63),
onClick = { viewModel.toggleFilterMissing() },
modifier = Modifier.weight(1f)
)
DashboardTile(
title = "Statusy",
icon = Icons.Default.Info,
color = Color(0xFF9C27B0),
onClick = { showStatusDialog = true },
modifier = Modifier.weight(1f)
)
DashboardTile(
title = "Dopisz",
icon = Icons.Default.EditNote,
color = Color(0xFF4CAF50),
onClick = { showNotesDialog = true },
modifier = Modifier.weight(1f)
)
}
Spacer(modifier = Modifier.height(16.dp))
if (machines.isNotEmpty()) {
Card(
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
shape = RoundedCornerShape(8.dp),
modifier = Modifier.fillMaxWidth()
) {
Column(modifier = Modifier.padding(vertical = 2.dp, horizontal = 8.dp)) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.weight(1f)) {
if (branchName.isNotEmpty()) {
Text(
text = branchName,
color = Color(0xFF11CAA0),
fontSize = 10.sp,
fontWeight = FontWeight.Bold,
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
)
}
LinearProgressIndicator(
progress = { progress },
modifier = Modifier
.fillMaxWidth()
.height(3.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(2.dp),
modifier = Modifier.weight(1f) // Używamy weight, aby zajęła resztę miejsca przed klawiaturą
) {
items(filteredMachines, key = { it.id }) { machine ->
MachineItem(
machine = machine,
onToggle = { viewModel.toggleMachineCheck(context, machine.id) }
)
}
}
Spacer(modifier = Modifier.height(8.dp))
// Bardzo mały pasek szukania nad klawiaturą
OutlinedTextField(
value = searchQuery,
onValueChange = {
viewModel.setSearchQuery(it)
viewModel.setNumericOnlySearch(false)
},
placeholder = { Text("szukanie po nazwie", fontSize = 12.sp, color = Color.Gray) },
modifier = Modifier.fillMaxWidth().height(48.dp),
shape = RoundedCornerShape(8.dp),
singleLine = true,
textStyle = LocalTextStyle.current.copy(fontSize = 13.sp),
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.5f),
unfocusedBorderColor = Color(0xFF2A3447),
focusedContainerColor = MaterialTheme.colorScheme.surface,
unfocusedContainerColor = MaterialTheme.colorScheme.surface,
focusedTextColor = Color.White,
unfocusedTextColor = Color.White
)
)
Spacer(modifier = Modifier.height(8.dp))
// Klawiatura numeryczna na dole
NumericKeyboard(
onDigitClick = {
viewModel.setNumericOnlySearch(true)
viewModel.appendDigit(it)
},
onBackspace = {
viewModel.setNumericOnlySearch(true)
viewModel.backspace()
},
onClear = {
viewModel.setNumericOnlySearch(true)
viewModel.clearSearch()
}
)
} else {
Box(
modifier = Modifier
.fillMaxSize()
.padding(32.dp),
contentAlignment = Alignment.Center
) {
Text(
text = "Brak wczytanych maszyn.\nWybierz plik .xlsx wygenerowany z systemu.",
color = Color.Gray,
fontSize = 16.sp,
textAlign = TextAlign.Center
)
}
}
}
}
}
// ==========================================
// 5. POJEDYNCZY ELEMENT LISTY
// ==========================================
/**
* Komponent pojedynczego kafelka w górnym menu nawigacyjnym.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun GreetingPreview() {
IwentARyzacjaTheme {
Greeting("Android")
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
)
}
}
}
/**
* Okno dialogowe umożliwiające zbiorcze zaznaczanie maszyn na podstawie ich statusu.
*/
@Composable
fun StatusSelectionDialog(
statuses: List<String>,
onDismiss: () -> Unit,
onConfirm: (Set<String>) -> Unit
) {
var selectedStatuses by remember { mutableStateOf(setOf<String>()) }
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
)
}
/**
* Okno dialogowe notatnika do zapisywania uwag.
*/
@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)
)
}
/**
* Niestandardowa klawiatura numeryczna do szybkiego filtrowania po numerach ID.
*/
@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
)
}
}
}
/**
* Komponent pojedynczego elementu na liście maszyn.
*/
@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(vertical = 4.dp, horizontal = 8.dp)
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = machine.id,
fontWeight = FontWeight.Bold,
fontSize = 13.sp,
color = Color.White
)
Text(
text = machine.name,
fontSize = 10.sp,
color = Color.LightGray,
maxLines = 1
)
}
if (machine.originalStatus.isNotEmpty()) {
Text(
text = machine.originalStatus,
fontSize = 11.sp,
fontWeight = FontWeight.Medium,
color = Color(0xFF11CAA0),
modifier = Modifier.padding(start = 8.dp)
)
}
}
}
}
@@ -1,7 +1,27 @@
/*
* Właścicielem tego kodu jest Bartłomiej Janik.
*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.bartek.iwentaryzacja.ui.theme
import androidx.compose.ui.graphics.Color
/**
* Definicje kolorów używanych w motywie aplikacji.
*/
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)
@@ -1,3 +1,20 @@
/*
* Właścicielem tego kodu jest Bartłomiej Janik.
*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.bartek.iwentaryzacja.ui.theme
import android.app.Activity
@@ -21,18 +38,11 @@ private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
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
fun IwentARyzacjaTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
@@ -1,3 +1,20 @@
/*
* Właścicielem tego kodu jest Bartłomiej Janik.
*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.bartek.iwentaryzacja.ui.theme
import androidx.compose.material3.Typography
@@ -6,7 +23,9 @@ import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
/**
* Definicje typografii dla interfejsu Compose.
*/
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
@@ -15,20 +34,4 @@ val Typography = Typography(
lineHeight = 24.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
)
*/
)
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

@@ -1,3 +1,7 @@
/*
* Właścicielem tego kodu jest Bartłomiej Janik.
*/
package com.bartek.iwentaryzacja
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>
+1 -1
View File
@@ -1,4 +1,4 @@
{
"versionCode": 2,
"url": "https://twoja-strona.pl/sciezka/do/aplikacji.apk"
"isLocked": false
}