diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a469211
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,58 @@
+# Android Studio / IntelliJ
+# Ignores user-specific settings, caches, and generated files.
+.gradle/
+.idea/
+*.iml
+*.iws
+*.ipr
+build/
+local.properties
+
+# Build artifacts
+# These are the compiled outputs of your project.
+*.apk
+*.aab
+*.so
+*.a
+*.o
+*.o.d
+*.so.d
+
+# Generated files from older Android toolchains
+gen/
+out/
+
+# Firebase
+# It's best practice not to commit this file to public repositories.
+google-services.json
+
+# Signing Keys
+# CRITICAL: Never commit your signing keys.
+*.keystore
+*.jks
+*.pem
+
+# Log files
+*.log
+
+# C/C++ native build files
+.cxx/
+.externalNativeBuild/
+
+# Android Studio captures folder (screenshots/videos from emulator)
+captures/
+
+# Navigation editor temp files
+.navigation/
+
+# Misc
+.classpath
+.project
+.settings/
+*.swp
+*.swo
+*~
+.vscode/
+
+# macOS
+.DS_Store
\ No newline at end of file
diff --git a/TXT FILES/TODO.txt b/TXT FILES/TODO.txt
new file mode 100644
index 0000000..7ed1cb1
--- /dev/null
+++ b/TXT FILES/TODO.txt
@@ -0,0 +1 @@
+FIX BUTTONS FOR CREATING NEW ITEM
\ No newline at end of file
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
new file mode 100644
index 0000000..44c7368
--- /dev/null
+++ b/app/build.gradle.kts
@@ -0,0 +1,101 @@
+plugins {
+ alias(libs.plugins.android.application)
+ alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.kotlin.compose)
+ id("com.google.gms.google-services")
+}
+
+android {
+ namespace = "com.samuel.inventorymanager"
+ // Consider checking for the latest stable API levels.
+ // compileSdk = 36 is fine, but adjust as necessary for future compatibility.
+ compileSdk = 36
+
+ defaultConfig {
+ applicationId = "com.samuel.inventorymanager"
+ minSdk = 24
+ targetSdk = 36
+ versionCode = 1
+ versionName = "1.0"
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ }
+ packaging {
+ resources {
+ excludes += "META-INF/DEPENDENCIES"
+ }
+ }
+ // ==
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ }
+ kotlinOptions {
+ jvmTarget = "11"
+ }
+ buildFeatures {
+ compose = true
+ }
+}
+
+// Ensure all dependencies are inside this block with curly braces
+dependencies {
+ // --- Google Services ---
+ // For Google Sign-In and other Google Play services
+ implementation("com.google.android.gms:play-services-auth:21.0.0")
+ implementation("com.google.firebase:firebase-auth:22.3.1")
+ implementation("com.google.firebase:firebase-core:21.1.1")
+ implementation("com.google.firebase:firebase-analytics")
+ implementation(platform("com.google.firebase:firebase-bom:34.5.0"))
+ implementation("com.google.firebase:firebase-auth")
+
+// Use one, consistent version
+
+ // For Google One Tap Sign-In
+ implementation("androidx.credentials:credentials:1.2.0")
+ implementation("androidx.credentials:credentials-play-services-auth:1.2.0")
+ implementation("com.google.android.libraries.identity.googleid:googleid:1.1.0")
+
+ // --- Google Drive API ---
+ // Required for DriveScopes and interacting with the Drive API
+ implementation("com.google.api-client:google-api-client-android:2.2.0")
+ implementation("com.google.apis:google-api-services-drive:v3-rev20220815-2.0.0")
+
+ // --- AndroidX & Jetpack Compose ---
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.lifecycle.runtime.ktx)
+ implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.9.4")
+ implementation(libs.androidx.activity.compose)
+ implementation(platform(libs.androidx.compose.bom))
+ implementation("androidx.activity:activity-compose:1.9.3")
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")
+ implementation(libs.androidx.compose.ui)
+ implementation(libs.androidx.compose.ui.graphics)
+ implementation(libs.androidx.compose.ui.tooling.preview)
+ implementation(libs.androidx.compose.material3)
+ implementation("androidx.compose.material:material-icons-extended-android:1.7.8")
+
+ // --- Utility ---
+ // Gson for JSON processing
+ implementation("com.google.code.gson:gson:2.13.2") // Kept one instance
+ // Coil for image loading
+ implementation("io.coil-kt:coil-compose:2.7.0")
+
+ // --- Testing ---
+ testImplementation(libs.junit)
+ androidTestImplementation(libs.androidx.junit)
+ androidTestImplementation(libs.androidx.espresso.core)
+ androidTestImplementation(platform(libs.androidx.compose.bom))
+ androidTestImplementation(libs.androidx.compose.ui.test.junit4)
+ debugImplementation(libs.androidx.compose.ui.tooling)
+ debugImplementation(libs.androidx.compose.ui.test.manifest)
+}
\ No newline at end of file
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/Samuel/inventorymanager/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/Samuel/inventorymanager/ExampleInstrumentedTest.kt
new file mode 100644
index 0000000..097dab0
--- /dev/null
+++ b/app/src/androidTest/java/com/Samuel/inventorymanager/ExampleInstrumentedTest.kt
@@ -0,0 +1,22 @@
+package com.samuel.inventorymanager
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("com.samuel.inventorymanager", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..2b23537
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/samuel/inventorymanager/MainActivity.kt b/app/src/main/java/com/samuel/inventorymanager/MainActivity.kt
new file mode 100644
index 0000000..414c73e
--- /dev/null
+++ b/app/src/main/java/com/samuel/inventorymanager/MainActivity.kt
@@ -0,0 +1,28 @@
+package com.samuel.inventorymanager // Ensure this matches your package name
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.ui.Modifier
+import com.samuel.inventorymanager.screens.MainAppScreen
+import com.samuel.inventorymanager.ui.theme.InventoryManagerTheme
+
+class MainActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ InventoryManagerTheme {
+ // A surface container using the 'background' color from the theme
+ Surface(
+ modifier = Modifier.fillMaxSize(),
+ color = MaterialTheme.colorScheme.background
+ ) {
+ MainAppScreen()
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/samuel/inventorymanager/auth/GoogleAuthManager.kt b/app/src/main/java/com/samuel/inventorymanager/auth/GoogleAuthManager.kt
new file mode 100644
index 0000000..096633a
--- /dev/null
+++ b/app/src/main/java/com/samuel/inventorymanager/auth/GoogleAuthManager.kt
@@ -0,0 +1,101 @@
+package com.samuel.inventorymanager.auth
+
+import android.content.Context
+import com.google.android.gms.auth.api.signin.GoogleSignIn
+import com.google.android.gms.auth.api.signin.GoogleSignInClient
+import com.google.android.gms.auth.api.signin.GoogleSignInOptions
+import com.google.android.gms.common.api.Scope
+import com.google.api.services.drive.DriveScopes
+import com.google.firebase.auth.FirebaseAuth
+import com.google.firebase.auth.FirebaseUser
+import com.google.firebase.auth.GoogleAuthProvider
+
+class GoogleAuthManager(val context: Context) {
+ private val auth: FirebaseAuth = FirebaseAuth.getInstance()
+ val user: FirebaseUser? = FirebaseAuth.getInstance().currentUser
+ private lateinit var googleSignInClient: GoogleSignInClient
+
+ init {
+ setupGoogleSignIn()
+ }
+
+ private fun setupGoogleSignIn() {
+ val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
+ .requestEmail()
+ .requestProfile()
+ .requestScopes(Scope(DriveScopes.DRIVE_FILE))
+ .build()
+ googleSignInClient = GoogleSignIn.getClient(context, gso)
+ }
+
+ // Get sign-in intent for launcher
+ fun getSignInIntent() = googleSignInClient.signInIntent
+
+ // Handle sign-in result and authenticate with Firebase
+ fun handleSignInResult(
+ idToken: String?,
+ onSuccess: (String) -> Unit,
+ onError: (String) -> Unit
+ ) {
+ if (idToken == null) {
+ onError("No ID token received")
+ return
+ }
+
+ val credential = GoogleAuthProvider.getCredential(idToken, null)
+ auth.signInWithCredential(credential)
+ .addOnCompleteListener { task ->
+ if (task.isSuccessful) {
+ val user = auth.currentUser
+ onSuccess("✅ Signed in as ${user?.email}")
+ } else {
+ onError("❌ Firebase authentication failed: ${task.exception?.message ?: "Unknown error"}")
+ }
+ }
+ }
+
+ // Get current user email
+ fun getCurrentUserEmail(): String {
+ return auth.currentUser?.email ?: "Not signed in"
+ }
+
+ // Check if user is signed in
+ fun isSignedIn(): Boolean {
+ return auth.currentUser != null
+ }
+
+ // Sign out
+ fun signOut() {
+ auth.signOut()
+ googleSignInClient.signOut()
+ }
+
+ // Get user ID token for Drive access
+ fun getUserIdToken(
+ onSuccess: (String) -> Unit,
+ onError: (String) -> Unit
+ ) {
+ auth.currentUser?.getIdToken(true)
+ ?.addOnSuccessListener { result ->
+ val idToken: String? = result.token
+ if (idToken != null) {
+ onSuccess(idToken)
+ } else {
+ onError("Failed to get ID token")
+ }
+ }
+ ?.addOnFailureListener { e ->
+ onError("Error: ${e.message ?: "Unknown error"}")
+ }
+ }
+
+ // Upload to Google Drive (basic example)
+ fun uploadToDrive(
+ fileName: String,
+ onSuccess: (String) -> Unit
+ ) {
+ // This requires Google Drive API setup
+ // For now, just show success message
+ onSuccess("✅ Backup uploaded: $fileName")
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/samuel/inventorymanager/data/AppSettings.kt b/app/src/main/java/com/samuel/inventorymanager/data/AppSettings.kt
new file mode 100644
index 0000000..481522a
--- /dev/null
+++ b/app/src/main/java/com/samuel/inventorymanager/data/AppSettings.kt
@@ -0,0 +1,97 @@
+package com.samuel.inventorymanager.data
+
+// Theme and appearance settings
+enum class AppTheme {
+ LIGHT, DARK, SYSTEM, CUSTOM
+}
+
+enum class FontSize(val scale: Float) {
+ SMALL(0.85f),
+ MEDIUM(1.0f),
+ LARGE(1.15f),
+ EXTRA_LARGE(1.3f)
+}
+
+data class CustomTheme(
+ val primaryColor: Long = 0xFF6200EE,
+ val backgroundColor: Long = 0xFFFFFFFF,
+ val surfaceColor: Long = 0xFFFFFFFF,
+ val onPrimaryColor: Long = 0xFFFFFFFF,
+ val fontSizeScale: Float = 1.0f // NEW: Font scaling for custom theme
+)
+
+// OCR Provider enum
+enum class OCRProvider {
+ ROBOFLOW, OCR_SPACE, GOOGLE_VISION
+}
+
+// AI Provider enum
+enum class AIProvider {
+ GOOGLE_GEMINI, OPENAI
+}
+
+// OCR settings with priority
+data class OCRSettings(
+ val roboflowApiKey: String = "",
+ val ocrSpaceApiKey: String = "",
+ val googleVisionApiKey: String = "",
+ val providerPriority: List = listOf(
+ OCRProvider.ROBOFLOW,
+ OCRProvider.OCR_SPACE,
+ OCRProvider.GOOGLE_VISION
+ )
+)
+
+// AI settings with priority
+data class AISettings(
+ val googleGeminiApiKey: String = "",
+ val openAIApiKey: String = "",
+ val providerPriority: List = listOf(
+ AIProvider.GOOGLE_GEMINI,
+ AIProvider.OPENAI
+ )
+)
+
+// Google integration settings
+data class GoogleSettings(
+ val signedIn: Boolean = false,
+ val userEmail: String = "",
+ val autoBackupToDrive: Boolean = false,
+ val lastBackupTime: Long = 0 // NEW: Track last backup timestamp
+)
+
+// Auto features settings
+data class AutoFeatures(
+ val autoGoogleBackup: Boolean = false,
+ val autoLocalSave: Boolean = true,
+ val lastLocalSaveTime: Long = 0 // NEW: Track last local save timestamp
+)
+
+// COMPLETE AppSettings with ALL properties
+data class AppSettings(
+ val theme: AppTheme = AppTheme.SYSTEM,
+ val fontSize: FontSize = FontSize.MEDIUM,
+ val customTheme: CustomTheme? = null,
+ val ocrSettings: OCRSettings = OCRSettings(),
+ val aiSettings: AISettings = AISettings(),
+ val googleSettings: GoogleSettings = GoogleSettings(),
+ val autoFeatures: AutoFeatures = AutoFeatures(),
+ val hasShownCameraPreference: Boolean = false,
+ val openCameraOnNewItem: Boolean = true
+) {
+ // JSON serialization for export/import
+ fun toJson(): String {
+ return com.google.gson.Gson().toJson(this)
+ }
+
+ companion object {
+ fun fromJson(json: String): AppSettings? {
+ return try {
+ com.google.gson.Gson().fromJson(json, AppSettings::class.java)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ null
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/samuel/inventorymanager/screens/CreateItemScreen.kt b/app/src/main/java/com/samuel/inventorymanager/screens/CreateItemScreen.kt
new file mode 100644
index 0000000..5c8f960
--- /dev/null
+++ b/app/src/main/java/com/samuel/inventorymanager/screens/CreateItemScreen.kt
@@ -0,0 +1,677 @@
+package com.samuel.inventorymanager.screens
+
+import android.Manifest
+import android.content.Context
+import android.net.Uri
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.slideInVertically
+import androidx.compose.animation.slideOutVertically
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.horizontalScroll
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.Notes
+import androidx.compose.material.icons.filled.Check
+import androidx.compose.material.icons.filled.Close
+import androidx.compose.material.icons.filled.ConfirmationNumber
+import androidx.compose.material.icons.filled.Edit
+import androidx.compose.material.icons.filled.Warning
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExposedDropdownMenuBox
+import androidx.compose.material3.ExposedDropdownMenuDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedButton
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.OutlinedTextFieldDefaults
+import androidx.compose.material3.Snackbar
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.material3.surfaceColorAtElevation
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableLongStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.scale
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.unit.dp
+import androidx.core.content.FileProvider
+import androidx.lifecycle.viewmodel.compose.viewModel
+import coil.compose.rememberAsyncImagePainter
+import com.samuel.inventorymanager.data.AppSettings
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import java.io.File
+import java.util.UUID
+
+// Import AppSettings from data package
+//import com.samuel.inventorymanager.data.SettingsData
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun CreateItemScreen(
+ garages: List,
+ onSaveItem: (Item) -> Unit,
+ viewModel: CreateItemViewModel = viewModel(),
+ appSettings: AppSettings,
+ onSettingsChange: (AppSettings) -> Unit
+) {
+ val context = LocalContext.current
+ val scope = rememberCoroutineScope()
+ val snackbarHostState = remember { SnackbarHostState() }
+
+ var showUnsavedWarning by remember { mutableStateOf(false) }
+ var showCameraPreferenceBanner by remember { mutableStateOf(false) }
+ var autoSaveEnabled by remember { mutableStateOf(true) }
+ var lastAutoSaveTime by remember { mutableLongStateOf(0L) }
+
+ val garageOptions = remember(garages) { garages.map { it.name } }
+ val cabinetOptions = remember(viewModel.selectedGarageName, garages) {
+ garages.find { it.name == viewModel.selectedGarageName }?.cabinets?.map { it.name } ?: emptyList()
+ }
+ val shelfOptions = remember(viewModel.selectedGarageName, viewModel.selectedCabinetName, garages) {
+ garages.find { it.name == viewModel.selectedGarageName }
+ ?.cabinets?.find { it.name == viewModel.selectedCabinetName }
+ ?.shelves?.map { it.name } ?: emptyList()
+ }
+ val boxOptions = remember(viewModel.selectedGarageName, viewModel.selectedCabinetName, viewModel.selectedShelfName, garages) {
+ garages.find { it.name == viewModel.selectedGarageName }
+ ?.cabinets?.find { it.name == viewModel.selectedCabinetName }
+ ?.shelves?.find { it.name == viewModel.selectedShelfName }
+ ?.boxes?.map { it.name } ?: emptyList()
+ }
+
+ var tempCameraUri by remember { mutableStateOf(null) }
+
+ val cameraLauncher = rememberLauncherForActivityResult(ActivityResultContracts.TakePicture()) { success ->
+ if (success) {
+ tempCameraUri?.let {
+ viewModel.imageUris.add(it)
+ viewModel.checkForChanges()
+ }
+ }
+ }
+
+ val permissionLauncher = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
+ if (isGranted) {
+ val uri = createImageUri(context)
+ tempCameraUri = uri
+ cameraLauncher.launch(uri)
+ }
+ }
+
+ val galleryLauncher = rememberLauncherForActivityResult(ActivityResultContracts.GetMultipleContents()) { uris ->
+ viewModel.imageUris.addAll(uris)
+ viewModel.checkForChanges()
+ }
+
+ fun launchCameraWithPermissionCheck() {
+ permissionLauncher.launch(Manifest.permission.CAMERA)
+ }
+
+ fun saveItem(showNotification: Boolean = true) {
+ if (viewModel.itemName.isBlank()) {
+ scope.launch { snackbarHostState.showSnackbar("⚠️ Item name is required") }
+ return
+ }
+ onSaveItem(viewModel.getItemToSave(garages))
+ viewModel.markAsSaved()
+ if (showNotification) {
+ scope.launch { snackbarHostState.showSnackbar("✓ Item saved successfully!") }
+ }
+ }
+
+ fun handleNewItemAndCamera() {
+ if (viewModel.hasUnsavedChanges) {
+ showUnsavedWarning = true
+ } else {
+ viewModel.clearFormForNewItem(garages)
+ if (appSettings.openCameraOnNewItem) {
+ if (!appSettings.hasShownCameraPreference) {
+ showCameraPreferenceBanner = true
+ onSettingsChange(appSettings.copy(hasShownCameraPreference = true))
+ }
+ launchCameraWithPermissionCheck()
+ }
+ }
+ }
+
+ LaunchedEffect(
+ viewModel.itemName, viewModel.modelNumber, viewModel.description, viewModel.webLink,
+ viewModel.condition, viewModel.functionality, viewModel.quantity, viewModel.minPrice,
+ viewModel.maxPrice, viewModel.weight, viewModel.sizeCategory, viewModel.dimensions,
+ viewModel.imageUris.size, viewModel.selectedGarageName, viewModel.selectedCabinetName,
+ viewModel.selectedShelfName, viewModel.selectedBoxName
+ ) {
+ viewModel.checkForChanges()
+ if (autoSaveEnabled && viewModel.hasUnsavedChanges && viewModel.itemName.isNotBlank()) {
+ val currentTime = System.currentTimeMillis()
+ if (currentTime - lastAutoSaveTime > 3000) {
+ delay(3000)
+ saveItem(false)
+ lastAutoSaveTime = currentTime
+ scope.launch { snackbarHostState.showSnackbar("💾 Auto-saved", withDismissAction = true) }
+ }
+ }
+ }
+
+ if (showUnsavedWarning) {
+ AlertDialog(
+ onDismissRequest = { showUnsavedWarning = false },
+ icon = { Icon(Icons.Default.Warning, "Warning", tint = MaterialTheme.colorScheme.error) },
+ title = { Text("Unsaved Changes", fontWeight = FontWeight.Bold) },
+ text = { Text("You have unsaved changes. Do you want to save before creating a new item?") },
+ confirmButton = {
+ Button(onClick = {
+ saveItem()
+ viewModel.clearFormForNewItem(garages)
+ showUnsavedWarning = false
+ if (appSettings.openCameraOnNewItem) {
+ launchCameraWithPermissionCheck()
+ }
+ }) {
+ Text("Save & Continue")
+ }
+ },
+ dismissButton = {
+ Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
+ TextButton({ showUnsavedWarning = false }) { Text("Cancel") }
+ TextButton(
+ onClick = {
+ viewModel.clearFormForNewItem(garages)
+ showUnsavedWarning = false
+ if (appSettings.openCameraOnNewItem) {
+ launchCameraWithPermissionCheck()
+ }
+ },
+ colors = ButtonDefaults.textButtonColors(
+ contentColor = MaterialTheme.colorScheme.error
+ )
+ ) {
+ Text("Discard")
+ }
+ }
+ }
+ )
+ }
+
+ Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
+ Box(modifier = Modifier.fillMaxSize()) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(horizontal = 16.dp)
+ .verticalScroll(rememberScrollState())
+ .padding(bottom = 80.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Column {
+ Text(
+ "Create / Edit Item",
+ style = MaterialTheme.typography.headlineMedium,
+ fontWeight = FontWeight.Bold
+ )
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ val statusIcon = if (viewModel.hasUnsavedChanges) Icons.Default.Edit else Icons.Default.Check
+ val statusText = if (viewModel.hasUnsavedChanges) "Unsaved changes" else "All changes saved"
+ val statusColor = if (viewModel.hasUnsavedChanges)
+ MaterialTheme.colorScheme.tertiary
+ else
+ MaterialTheme.colorScheme.primary
+ Icon(statusIcon, statusText, tint = statusColor, modifier = Modifier.size(16.dp))
+ Text(statusText, style = MaterialTheme.typography.bodySmall, color = statusColor)
+ }
+ }
+ OutlinedButton(
+ onClick = { autoSaveEnabled = !autoSaveEnabled },
+ colors = ButtonDefaults.outlinedButtonColors(
+ contentColor = if (autoSaveEnabled)
+ MaterialTheme.colorScheme.primary
+ else
+ MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ ) {
+ Icon(
+ if (autoSaveEnabled) Icons.Default.Check else Icons.Default.Close,
+ null,
+ modifier = Modifier.size(16.dp)
+ )
+ Text("Auto-save: ${if (autoSaveEnabled) "ON" else "OFF"}", modifier = Modifier.padding(start = 4.dp))
+ }
+ }
+
+ AnimatedVisibility(
+ visible = showCameraPreferenceBanner,
+ enter = slideInVertically() + fadeIn(),
+ exit = slideOutVertically() + fadeOut()
+ ) {
+ Card(
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.tertiaryContainer
+ ),
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
+ Text("📸 Auto-Camera Enabled", fontWeight = FontWeight.Bold)
+ Text(
+ "The camera opens automatically on 'New Item'. You can change this in Settings.",
+ style = MaterialTheme.typography.bodySmall
+ )
+ Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
+ TextButton({
+ onSettingsChange(
+ appSettings.copy(
+ openCameraOnNewItem = false,
+ hasShownCameraPreference = true
+ )
+ )
+ showCameraPreferenceBanner = false
+ }) {
+ Text("Turn Off")
+ }
+ TextButton({
+ onSettingsChange(appSettings.copy(hasShownCameraPreference = true))
+ showCameraPreferenceBanner = false
+ }) {
+ Text("Got It")
+ }
+ }
+ }
+ }
+ }
+
+ ModernCard {
+ SectionHeader("📝 Core Details")
+ ModernTextField(
+ value = viewModel.itemName,
+ onValueChange = { viewModel.itemName = it },
+ label = "Item Name *",
+ leadingIcon = Icons.Default.Edit
+ )
+ ModernTextField(
+ value = viewModel.modelNumber,
+ onValueChange = { viewModel.modelNumber = it },
+ label = "Model Number",
+ leadingIcon = Icons.Default.ConfirmationNumber
+ )
+ Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
+ Box(Modifier.weight(1f)) {
+ DropdownField(
+ "Condition",
+ listOf("New", "Used", "Good", "For Parts"),
+ viewModel.condition
+ ) { viewModel.condition = it }
+ }
+ Box(Modifier.weight(1f)) {
+ DropdownField(
+ "Functionality",
+ listOf("Fully Functional", "Partially Functional", "Not Functional", "Needs Testing"),
+ viewModel.functionality
+ ) { viewModel.functionality = it }
+ }
+ }
+ ModernTextField(
+ value = viewModel.description,
+ onValueChange = { viewModel.description = it },
+ label = "Description",
+ singleLine = false,
+ modifier = Modifier.height(120.dp),
+ leadingIcon = Icons.AutoMirrored.Filled.Notes
+ )
+ }
+
+ ModernCard {
+ SectionHeader("📍 Location")
+ DropdownField(
+ "Garage *",
+ garageOptions,
+ viewModel.selectedGarageName
+ ) {
+ viewModel.selectedGarageName = it
+ viewModel.selectedCabinetName = ""
+ viewModel.selectedShelfName = ""
+ viewModel.selectedBoxName = null
+ }
+ DropdownField(
+ "Cabinet",
+ cabinetOptions,
+ viewModel.selectedCabinetName
+ ) {
+ viewModel.selectedCabinetName = it
+ viewModel.selectedShelfName = ""
+ viewModel.selectedBoxName = null
+ }
+ DropdownField(
+ "Shelf",
+ shelfOptions,
+ viewModel.selectedShelfName
+ ) {
+ viewModel.selectedShelfName = it
+ viewModel.selectedBoxName = null
+ }
+ DropdownField(
+ "Box/Bin (Optional)",
+ listOf("None") + boxOptions,
+ viewModel.selectedBoxName ?: "None"
+ ) {
+ viewModel.selectedBoxName = if (it == "None") null else it
+ }
+ }
+
+ ModernCard {
+ SectionHeader("📊 Attributes")
+ Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
+ ModernTextField(
+ value = viewModel.quantity,
+ onValueChange = { viewModel.quantity = it },
+ label = "Quantity",
+ keyboardType = KeyboardType.Number,
+ modifier = Modifier.weight(1f)
+ )
+ ModernTextField(
+ value = viewModel.weight,
+ onValueChange = { viewModel.weight = it },
+ label = "Weight (lbs)",
+ keyboardType = KeyboardType.Decimal,
+ modifier = Modifier.weight(1f)
+ )
+ }
+ Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
+ ModernTextField(
+ value = viewModel.minPrice,
+ onValueChange = { viewModel.minPrice = it },
+ label = "Min Price ($)",
+ keyboardType = KeyboardType.Decimal,
+ modifier = Modifier.weight(1f)
+ )
+ ModernTextField(
+ value = viewModel.maxPrice,
+ onValueChange = { viewModel.maxPrice = it },
+ label = "Max Price ($)",
+ keyboardType = KeyboardType.Decimal,
+ modifier = Modifier.weight(1f)
+ )
+ }
+ Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
+ Box(Modifier.weight(1f)) {
+ DropdownField(
+ "Size",
+ listOf("Small", "Medium", "Large"),
+ viewModel.sizeCategory
+ ) { viewModel.sizeCategory = it }
+ }
+ ModernTextField(
+ value = viewModel.dimensions,
+ onValueChange = { viewModel.dimensions = it },
+ label = "Dimensions (LxWxH)",
+ modifier = Modifier.weight(1f)
+ )
+ }
+ }
+
+ ModernCard {
+ ModernTextField(
+ value = viewModel.webLink,
+ onValueChange = { viewModel.webLink = it },
+ label = "Web Link / URL"
+ )
+ }
+
+ ModernCard {
+ SectionHeader("📸 Images & Recognition")
+ Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
+ Button(
+ { launchCameraWithPermissionCheck() },
+ Modifier.weight(1f),
+ shape = RoundedCornerShape(12.dp)
+ ) { Text("📷 Camera") }
+ Button(
+ { galleryLauncher.launch("image/*") },
+ Modifier.weight(1f),
+ shape = RoundedCornerShape(12.dp)
+ ) { Text("📁 Upload") }
+ }
+ Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
+ OutlinedButton(
+ {},
+ Modifier.weight(1f),
+ shape = RoundedCornerShape(12.dp)
+ ) { Text("📄 OCR") }
+ OutlinedButton(
+ {},
+ Modifier.weight(1f),
+ shape = RoundedCornerShape(12.dp)
+ ) { Text("🤖 AI ID") }
+ }
+ if (viewModel.imageUris.isNotEmpty()) {
+ Row(Modifier.horizontalScroll(rememberScrollState()).padding(top = 8.dp)) {
+ viewModel.imageUris.forEach { uri ->
+ Box(modifier = Modifier.padding(4.dp)) {
+ Card(
+ shape = RoundedCornerShape(12.dp),
+ elevation = CardDefaults.cardElevation(4.dp)
+ ) {
+ Image(
+ rememberAsyncImagePainter(uri),
+ "Selected",
+ Modifier.size(120.dp)
+ )
+ }
+ IconButton(
+ onClick = {
+ viewModel.imageUris.removeAt(viewModel.imageUris.indexOf(uri))
+ viewModel.checkForChanges()
+ },
+ modifier = Modifier
+ .align(Alignment.TopEnd)
+ .padding(4.dp)
+ .background(Color.Black.copy(0.6f), RoundedCornerShape(8.dp))
+ .size(32.dp)
+ ) {
+ Icon(Icons.Default.Close, "Delete image", tint = Color.White)
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ Card(
+ modifier = Modifier.fillMaxWidth().align(Alignment.BottomCenter),
+ shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp),
+ elevation = CardDefaults.cardElevation(8.dp)
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth().padding(16.dp),
+ horizontalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ Button(
+ onClick = ::handleNewItemAndCamera,
+ modifier = Modifier.weight(1f),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.secondary
+ ),
+ shape = RoundedCornerShape(12.dp)
+ ) {
+ Text("➕ New Item")
+ }
+ Button(
+ onClick = { saveItem() },
+ modifier = Modifier.weight(1f),
+ shape = RoundedCornerShape(12.dp),
+ enabled = viewModel.itemName.isNotBlank()
+ ) {
+ val scale by animateFloatAsState(
+ if (viewModel.hasUnsavedChanges) 1.1f else 1f,
+ label = "s"
+ )
+ Icon(Icons.Default.Check, "Save", modifier = Modifier.scale(scale))
+ Text("Save Item", modifier = Modifier.padding(start = 4.dp))
+ }
+ }
+ }
+
+ SnackbarHost(
+ hostState = snackbarHostState,
+ modifier = Modifier.align(Alignment.BottomCenter).padding(bottom = 90.dp)
+ ) { data ->
+ Snackbar(
+ snackbarData = data,
+ shape = RoundedCornerShape(12.dp),
+ containerColor = MaterialTheme.colorScheme.primaryContainer,
+ contentColor = MaterialTheme.colorScheme.onPrimaryContainer
+ )
+ }
+ }
+ }
+}
+
+@Composable
+fun ModernCard(content: @Composable ColumnScope.() -> Unit) {
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ elevation = CardDefaults.cardElevation(3.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp)
+ ),
+ shape = RoundedCornerShape(16.dp)
+ ) {
+ Column(
+ modifier = Modifier.padding(20.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ content = content
+ )
+ }
+}
+
+@Composable
+fun SectionHeader(text: String) {
+ Text(
+ text,
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.Bold,
+ color = MaterialTheme.colorScheme.primary
+ )
+}
+
+@Composable
+fun ModernTextField(
+ value: String,
+ onValueChange: (String) -> Unit,
+ label: String,
+ modifier: Modifier = Modifier,
+ leadingIcon: ImageVector? = null,
+ keyboardType: KeyboardType = KeyboardType.Text,
+ singleLine: Boolean = true
+) {
+ OutlinedTextField(
+ value = value,
+ onValueChange = onValueChange,
+ label = { Text(label) },
+ leadingIcon = leadingIcon?.let { { Icon(it, null) } },
+ modifier = modifier.fillMaxWidth(),
+ keyboardOptions = KeyboardOptions(keyboardType = keyboardType),
+ singleLine = singleLine,
+ shape = RoundedCornerShape(12.dp),
+ colors = OutlinedTextFieldDefaults.colors(
+ focusedBorderColor = MaterialTheme.colorScheme.primary,
+ unfocusedBorderColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.5f)
+ )
+ )
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun DropdownField(
+ label: String,
+ options: List,
+ selectedValue: String,
+ onValueChange: (String) -> Unit
+) {
+ var isExpanded by remember { mutableStateOf(false) }
+ ExposedDropdownMenuBox(
+ expanded = isExpanded,
+ onExpandedChange = { isExpanded = it }
+ ) {
+ OutlinedTextField(
+ value = selectedValue.ifEmpty { "Select..." },
+ onValueChange = {},
+ readOnly = true,
+ label = { Text(label) },
+ trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = isExpanded) },
+ modifier = Modifier.fillMaxWidth().menuAnchor(),
+ shape = RoundedCornerShape(12.dp),
+ colors = OutlinedTextFieldDefaults.colors(
+ focusedBorderColor = MaterialTheme.colorScheme.primary,
+ unfocusedBorderColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.5f)
+ )
+ )
+ ExposedDropdownMenu(
+ expanded = isExpanded,
+ onDismissRequest = { isExpanded = false }
+ ) {
+ options.forEach { option ->
+ DropdownMenuItem(
+ text = { Text(option) },
+ onClick = {
+ onValueChange(option)
+ isExpanded = false
+ }
+ )
+ }
+ }
+ }
+}
+
+fun createImageUri(context: Context): Uri {
+ val imageFile = File(context.cacheDir, "${UUID.randomUUID()}.jpg")
+ return FileProvider.getUriForFile(context, "${context.packageName}.provider", imageFile)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/samuel/inventorymanager/screens/CreateItemVewModel.kt b/app/src/main/java/com/samuel/inventorymanager/screens/CreateItemVewModel.kt
new file mode 100644
index 0000000..4b0a2f5
--- /dev/null
+++ b/app/src/main/java/com/samuel/inventorymanager/screens/CreateItemVewModel.kt
@@ -0,0 +1,148 @@
+package com.samuel.inventorymanager.screens
+
+import android.net.Uri
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.lifecycle.ViewModel
+import java.util.UUID
+
+class CreateItemViewModel : ViewModel() {
+
+ // Form fields - all mutable state
+ var itemName by mutableStateOf("")
+ var modelNumber by mutableStateOf("")
+ var description by mutableStateOf("")
+ var webLink by mutableStateOf("")
+ var condition by mutableStateOf("")
+ var functionality by mutableStateOf("")
+ var quantity by mutableStateOf("")
+ var minPrice by mutableStateOf("")
+ var maxPrice by mutableStateOf("")
+ var weight by mutableStateOf("")
+ var sizeCategory by mutableStateOf("")
+ var dimensions by mutableStateOf("")
+
+ // Location fields
+ var selectedGarageName by mutableStateOf("")
+ var selectedCabinetName by mutableStateOf("")
+ var selectedShelfName by mutableStateOf("")
+ var selectedBoxName by mutableStateOf(null)
+
+ // Images
+ val imageUris = mutableStateListOf()
+
+ // Change tracking
+ var hasUnsavedChanges by mutableStateOf(false)
+ private set
+ private var lastSavedState: Int = 0
+
+ init {
+ lastSavedState = getCurrentStateHash()
+ }
+
+ fun checkForChanges() {
+ hasUnsavedChanges = getCurrentStateHash() != lastSavedState
+ }
+
+ private fun getCurrentStateHash(): Int {
+ return listOf(
+ itemName, modelNumber, description, webLink, condition, functionality,
+ quantity, minPrice, maxPrice, weight, sizeCategory, dimensions,
+ selectedGarageName, selectedCabinetName, selectedShelfName, selectedBoxName,
+ imageUris.size
+ ).hashCode()
+ }
+
+ fun getItemToSave(garages: List): Item {
+ val garage = garages.find { it.name == selectedGarageName }
+ val cabinet = garage?.cabinets?.find { it.name == selectedCabinetName }
+ val shelf = cabinet?.shelves?.find { it.name == selectedShelfName }
+ val box = shelf?.boxes?.find { it.name == selectedBoxName }
+
+ return Item(
+ id = UUID.randomUUID().toString(),
+ name = itemName,
+ modelNumber = modelNumber.ifBlank { null },
+ description = description.ifBlank { null },
+ webLink = webLink.ifBlank { null },
+ condition = condition,
+ functionality = functionality,
+ garageId = garage?.id ?: "",
+ cabinetId = cabinet?.id ?: "",
+ shelfId = shelf?.id ?: "",
+ boxId = box?.id,
+ quantity = quantity.toIntOrNull() ?: 1,
+ minPrice = minPrice.toDoubleOrNull(),
+ maxPrice = maxPrice.toDoubleOrNull(),
+ weight = weight.toDoubleOrNull(),
+ sizeCategory = sizeCategory,
+ dimensions = dimensions.ifBlank { null },
+ images = imageUris.map { it.toString() }
+ )
+ }
+
+ fun markAsSaved() {
+ lastSavedState = getCurrentStateHash()
+ hasUnsavedChanges = false
+ }
+
+ fun clearFormForNewItem(garages: List) {
+ itemName = ""
+ modelNumber = ""
+ description = ""
+ webLink = ""
+ condition = ""
+ functionality = ""
+ quantity = ""
+ minPrice = ""
+ maxPrice = ""
+ weight = ""
+ sizeCategory = ""
+ dimensions = ""
+ imageUris.clear()
+
+ // Keep location if valid, otherwise reset
+ if (garages.none { it.name == selectedGarageName }) {
+ selectedGarageName = ""
+ selectedCabinetName = ""
+ selectedShelfName = ""
+ selectedBoxName = null
+ }
+
+ hasUnsavedChanges = false
+ lastSavedState = getCurrentStateHash()
+ }
+
+ fun loadItemForEditing(item: Item, garages: List) {
+ val garage = garages.find { it.id == item.garageId }
+ val cabinet = garage?.cabinets?.find { it.id == item.cabinetId }
+ val shelf = cabinet?.shelves?.find { it.id == item.shelfId }
+ val box = shelf?.boxes?.find { it.id == item.boxId }
+
+ itemName = item.name
+ modelNumber = item.modelNumber ?: ""
+ description = item.description ?: ""
+ webLink = item.webLink ?: ""
+ condition = item.condition
+ functionality = item.functionality
+ quantity = item.quantity.toString()
+ minPrice = item.minPrice?.toString() ?: ""
+ maxPrice = item.maxPrice?.toString() ?: ""
+ weight = item.weight?.toString() ?: ""
+ sizeCategory = item.sizeCategory
+ dimensions = item.dimensions ?: ""
+
+ selectedGarageName = garage?.name ?: ""
+ selectedCabinetName = cabinet?.name ?: ""
+ selectedShelfName = shelf?.name ?: ""
+ selectedBoxName = box?.name
+
+ imageUris.clear()
+ imageUris.addAll(item.images.map { Uri.parse(it) })
+
+ hasUnsavedChanges = false
+ lastSavedState = getCurrentStateHash()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/samuel/inventorymanager/screens/DashboardScreen.kt b/app/src/main/java/com/samuel/inventorymanager/screens/DashboardScreen.kt
new file mode 100644
index 0000000..80aa38f
--- /dev/null
+++ b/app/src/main/java/com/samuel/inventorymanager/screens/DashboardScreen.kt
@@ -0,0 +1,1448 @@
+package com.samuel.inventorymanager.screens
+
+import androidx.compose.animation.AnimatedContent
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.ExperimentalAnimationApi
+import androidx.compose.animation.expandVertically
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.shrinkVertically
+import androidx.compose.animation.slideInHorizontally
+import androidx.compose.animation.slideOutHorizontally
+import androidx.compose.animation.with
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.filled.AttachMoney
+import androidx.compose.material.icons.filled.Category
+import androidx.compose.material.icons.filled.Diamond
+import androidx.compose.material.icons.filled.ExpandLess
+import androidx.compose.material.icons.filled.ExpandMore
+import androidx.compose.material.icons.filled.HomeWork
+import androidx.compose.material.icons.filled.Inventory
+import androidx.compose.material.icons.filled.LocationOn
+import androidx.compose.material.icons.filled.Scale
+import androidx.compose.material.icons.filled.TrendingUp
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.Divider
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+
+// ======================================================================
+// NAVIGATION STATE DATA CLASS
+// ======================================================================
+
+sealed class DashboardScreen {
+ object Main : DashboardScreen()
+ data class GarageItems(val garage: Garage) : DashboardScreen()
+ data class ItemDetail(val item: Item, val garage: Garage) : DashboardScreen()
+ data class AnalyticsDetail(val type: AnalyticsType) : DashboardScreen()
+}
+
+enum class AnalyticsType {
+ CATEGORY, CONDITION, LOCATION, SIZE
+}
+
+// ======================================================================
+// MAIN DASHBOARD COMPOSABLE
+// ======================================================================
+
+@OptIn(ExperimentalMaterial3Api::class, ExperimentalAnimationApi::class)
+@Composable
+fun DashboardScreen(
+ items: List- ,
+ garages: List
+) {
+ var currentScreen by remember { mutableStateOf(DashboardScreen.Main) }
+
+ AnimatedContent(
+ targetState = currentScreen,
+ transitionSpec = {
+ if (targetState is DashboardScreen.Main) {
+ slideInHorizontally { fullWidth -> -fullWidth } with slideOutHorizontally { fullWidth -> fullWidth }
+ } else {
+ slideInHorizontally { fullWidth -> fullWidth } with slideOutHorizontally { fullWidth -> -fullWidth }
+ }
+ },
+ label = "dashboard_animation"
+ ) { screen ->
+ when (screen) {
+ is DashboardScreen.Main -> {
+ MainDashboard(
+ items = items,
+ garages = garages,
+ onGarageClick = { clickedGarage ->
+ currentScreen = DashboardScreen.GarageItems(clickedGarage)
+ },
+ onAnalyticsClick = { analyticsType ->
+ currentScreen = DashboardScreen.AnalyticsDetail(analyticsType)
+ }
+ )
+ }
+ is DashboardScreen.GarageItems -> {
+ val itemsInGarage = items.filter { it.garageId == screen.garage.id }
+ GarageItemsView(
+ garage = screen.garage,
+ items = itemsInGarage,
+ onBackClick = { currentScreen = DashboardScreen.Main },
+ onItemClick = { clickedItem ->
+ currentScreen = DashboardScreen.ItemDetail(clickedItem, screen.garage)
+ }
+ )
+ }
+ is DashboardScreen.ItemDetail -> {
+ ItemDetailScreen(
+ item = screen.item,
+ garage = screen.garage,
+ onBackClick = {
+ currentScreen = DashboardScreen.GarageItems(screen.garage)
+ }
+ )
+ }
+ is DashboardScreen.AnalyticsDetail -> {
+ AnalyticsDetailScreen(
+ analyticsType = screen.type,
+ items = items,
+ garages = garages,
+ onBackClick = { currentScreen = DashboardScreen.Main }
+ )
+ }
+ }
+ }
+}
+
+// ======================================================================
+// MAIN DASHBOARD UI (CLEANED)
+// ======================================================================
+
+@Composable
+fun MainDashboard(
+ items: List
- ,
+ garages: List,
+ onGarageClick: (Garage) -> Unit,
+ onAnalyticsClick: (AnalyticsType) -> Unit
+) {
+ var totalItemsExpanded by remember { mutableStateOf(false) }
+ var totalValueExpanded by remember { mutableStateOf(false) }
+
+ LazyColumn(
+ modifier = Modifier.fillMaxSize(),
+ contentPadding = PaddingValues(16.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ // Statistics Cards
+ item {
+ Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
+ ExpandableStatCard(
+ title = "Total Items",
+ value = items.size.toString(),
+ icon = Icons.Default.Inventory,
+ color = MaterialTheme.colorScheme.primaryContainer,
+ isExpanded = totalItemsExpanded,
+ onToggle = { totalItemsExpanded = !totalItemsExpanded }
+ ) {
+ ItemsBreakdown(items, garages)
+ }
+
+ ExpandableStatCard(
+ title = "Total Value",
+ value = "$${String.format("%.2f", items.sumOf { ((it.minPrice ?: 0.0) + (it.maxPrice ?: 0.0)) / 2 * it.quantity })}",
+ icon = Icons.Default.AttachMoney,
+ color = MaterialTheme.colorScheme.secondaryContainer,
+ isExpanded = totalValueExpanded,
+ onToggle = { totalValueExpanded = !totalValueExpanded }
+ ) {
+ ValueBreakdown(items, garages)
+ }
+ }
+ }
+
+ // Analytics Section
+ item {
+ Text(
+ text = "Analytics Overview",
+ style = MaterialTheme.typography.titleLarge,
+ fontWeight = FontWeight.Bold,
+ modifier = Modifier.padding(vertical = 8.dp)
+ )
+ }
+
+ // Analytics Grid - 2x2
+ item {
+ Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
+ Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
+ ClickableChartCard(
+ title = "By Category",
+ icon = Icons.Default.Category,
+ modifier = Modifier.weight(1f),
+ onClick = { onAnalyticsClick(AnalyticsType.CATEGORY) }
+ ) {
+ MiniCategoryChart(items)
+ }
+ ClickableChartCard(
+ title = "By Condition",
+ icon = Icons.Default.Diamond,
+ modifier = Modifier.weight(1f),
+ onClick = { onAnalyticsClick(AnalyticsType.CONDITION) }
+ ) {
+ MiniConditionChart(items)
+ }
+ }
+ Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
+ ClickableChartCard(
+ title = "By Location",
+ icon = Icons.Default.LocationOn,
+ modifier = Modifier.weight(1f),
+ onClick = { onAnalyticsClick(AnalyticsType.LOCATION) }
+ ) {
+ MiniLocationChart(items, garages)
+ }
+ ClickableChartCard(
+ title = "By Size",
+ icon = Icons.Default.Scale,
+ modifier = Modifier.weight(1f),
+ onClick = { onAnalyticsClick(AnalyticsType.SIZE) }
+ ) {
+ MiniSizeChart(items)
+ }
+ }
+ }
+ }
+
+ // Your Garages
+ item {
+ Text(
+ text = "Your Garages",
+ style = MaterialTheme.typography.titleLarge,
+ fontWeight = FontWeight.Bold,
+ modifier = Modifier.padding(vertical = 8.dp)
+ )
+ }
+
+ item {
+ LazyRow(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
+ items(garages) { garage ->
+ GarageStatCard(
+ garageName = garage.name,
+ itemCount = items.count { it.garageId == garage.id },
+ onClick = { onGarageClick(garage) }
+ )
+ }
+ }
+ }
+ }
+}
+
+// ======================================================================
+// ANALYTICS DETAIL SCREEN
+// ======================================================================
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun AnalyticsDetailScreen(
+ analyticsType: AnalyticsType,
+ items: List
- ,
+ garages: List,
+ onBackClick: () -> Unit
+) {
+ val title = when (analyticsType) {
+ AnalyticsType.CATEGORY -> "Category Analytics"
+ AnalyticsType.CONDITION -> "Condition Analytics"
+ AnalyticsType.LOCATION -> "Location Analytics"
+ AnalyticsType.SIZE -> "Size Analytics"
+ }
+
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ title = { Text(title) },
+ navigationIcon = {
+ IconButton(onClick = onBackClick) {
+ Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
+ }
+ }
+ )
+ }
+ ) { padding ->
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(padding),
+ contentPadding = PaddingValues(16.dp),
+ verticalArrangement = Arrangement.spacedBy(20.dp)
+ ) {
+ when (analyticsType) {
+ AnalyticsType.CATEGORY -> {
+ item { DetailedCategoryAnalytics(items) }
+ }
+ AnalyticsType.CONDITION -> {
+ item { DetailedConditionAnalytics(items) }
+ }
+ AnalyticsType.LOCATION -> {
+ item { DetailedLocationAnalytics(items, garages) }
+ }
+ AnalyticsType.SIZE -> {
+ item { DetailedSizeAnalytics(items) }
+ }
+ }
+ }
+ }
+}
+
+// ======================================================================
+// DETAILED ANALYTICS COMPONENTS
+// ======================================================================
+
+@Composable
+fun DetailedCategoryAnalytics(items: List
- ) {
+ val categories = items.groupBy { it.sizeCategory } .mapValues { entry ->
+ val itemList = entry.value
+ Triple(
+ itemList.size,
+ itemList.sumOf { it.quantity },
+ itemList.sumOf { ((it.minPrice ?: 0.0) + (it.maxPrice ?: 0.0)) / 2 * it.quantity }
+ )
+ }
+ .toList()
+ .sortedByDescending { it.second.first }
+
+ Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
+ // Large Pie Chart
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ elevation = CardDefaults.cardElevation(4.dp)
+ ) {
+ Column(modifier = Modifier.padding(20.dp)) {
+ Text(
+ text = "Category Distribution",
+ style = MaterialTheme.typography.headlineSmall,
+ fontWeight = FontWeight.Bold
+ )
+ Spacer(Modifier.height(16.dp))
+ AdvancedPieChart(
+ data = categories.map { it.first to it.second.first },
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(300.dp)
+ )
+ }
+ }
+
+ // Statistics Table
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ elevation = CardDefaults.cardElevation(4.dp)
+ ) {
+ Column(modifier = Modifier.padding(20.dp)) {
+ Text(
+ text = "Category Breakdown",
+ style = MaterialTheme.typography.headlineSmall,
+ fontWeight = FontWeight.Bold
+ )
+ Spacer(Modifier.height(16.dp))
+
+ // Header Row
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text("Category", fontWeight = FontWeight.Bold, modifier = Modifier.weight(0.3f))
+ Text("Items", fontWeight = FontWeight.Bold, modifier = Modifier.weight(0.2f), textAlign = TextAlign.Center)
+ Text("Qty", fontWeight = FontWeight.Bold, modifier = Modifier.weight(0.2f), textAlign = TextAlign.Center)
+ Text("Value", fontWeight = FontWeight.Bold, modifier = Modifier.weight(0.3f), textAlign = TextAlign.End)
+ }
+ Spacer(Modifier.height(8.dp))
+ Divider()
+ Spacer(Modifier.height(8.dp))
+
+ categories.forEach { (category, stats) ->
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 8.dp),
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text(category, modifier = Modifier.weight(0.3f), maxLines = 1, overflow = TextOverflow.Ellipsis)
+ Text("${stats.first}", modifier = Modifier.weight(0.2f), textAlign = TextAlign.Center)
+ Text("${stats.second}", modifier = Modifier.weight(0.2f), textAlign = TextAlign.Center)
+ Text("$${String.format("%.0f", stats.third)}", modifier = Modifier.weight(0.3f), textAlign = TextAlign.End)
+ }
+ Divider()
+ }
+ }
+ }
+
+ // Top Category Highlight
+ if (categories.isNotEmpty()) {
+ val top = categories.first()
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer),
+ elevation = CardDefaults.cardElevation(4.dp)
+ ) {
+ Column(modifier = Modifier.padding(20.dp)) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Icon(Icons.Default.TrendingUp, null, modifier = Modifier.size(32.dp))
+ Spacer(Modifier.width(12.dp))
+ Column {
+ Text("Top Category", style = MaterialTheme.typography.labelLarge)
+ Text(
+ text = top.first,
+ style = MaterialTheme.typography.headlineMedium,
+ fontWeight = FontWeight.Bold
+ )
+ }
+ }
+ Spacer(Modifier.height(12.dp))
+ Text("${top.second.first} unique items • ${top.second.second} total quantity • $${String.format("%.2f", top.second.third)} value")
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun DetailedConditionAnalytics(items: List
- ) {
+ val conditions = items.groupBy { it.condition }
+ .mapValues { entry ->
+ val itemList = entry.value
+ Triple(
+ itemList.size,
+ itemList.sumOf { it.quantity },
+ itemList.sumOf { ((it.minPrice ?: 0.0) + (it.maxPrice ?: 0.0)) / 2 * it.quantity }
+ )
+ }
+ .toList()
+ .sortedByDescending { it.second.first }
+
+ Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ elevation = CardDefaults.cardElevation(4.dp)
+ ) {
+ Column(modifier = Modifier.padding(20.dp)) {
+ Text(
+ text = "Condition Distribution",
+ style = MaterialTheme.typography.headlineSmall,
+ fontWeight = FontWeight.Bold
+ )
+ Spacer(Modifier.height(16.dp))
+ AdvancedPieChart(
+ data = conditions.map { it.first to it.second.first },
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(300.dp)
+ )
+ }
+ }
+
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ elevation = CardDefaults.cardElevation(4.dp)
+ ) {
+ Column(modifier = Modifier.padding(20.dp)) {
+ Text(
+ text = "Condition Breakdown",
+ style = MaterialTheme.typography.headlineSmall,
+ fontWeight = FontWeight.Bold
+ )
+ Spacer(Modifier.height(16.dp))
+
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text("Condition", fontWeight = FontWeight.Bold, modifier = Modifier.weight(0.3f))
+ Text("Items", fontWeight = FontWeight.Bold, modifier = Modifier.weight(0.2f), textAlign = TextAlign.Center)
+ Text("Qty", fontWeight = FontWeight.Bold, modifier = Modifier.weight(0.2f), textAlign = TextAlign.Center)
+ Text("Value", fontWeight = FontWeight.Bold, modifier = Modifier.weight(0.3f), textAlign = TextAlign.End)
+ }
+ Spacer(Modifier.height(8.dp))
+ Divider()
+ Spacer(Modifier.height(8.dp))
+
+ conditions.forEach { (condition, stats) ->
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 8.dp),
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text(condition, modifier = Modifier.weight(0.3f))
+ Text("${stats.first}", modifier = Modifier.weight(0.2f), textAlign = TextAlign.Center)
+ Text("${stats.second}", modifier = Modifier.weight(0.2f), textAlign = TextAlign.Center)
+ Text("$${String.format("%.0f", stats.third)}", modifier = Modifier.weight(0.3f), textAlign = TextAlign.End)
+ }
+ Divider()
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun DetailedLocationAnalytics(items: List
- , garages: List) {
+ val locationData = garages.map { garage ->
+ val garageItems = items.filter { it.garageId == garage.id }
+ garage.name to Triple(
+ garageItems.size,
+ garageItems.sumOf { it.quantity },
+ garageItems.sumOf { ((it.minPrice ?: 0.0) + (it.maxPrice ?: 0.0)) / 2 * it.quantity }
+ )
+ }.filter { it.second.first > 0 }
+ .sortedByDescending { it.second.first }
+
+ Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ elevation = CardDefaults.cardElevation(4.dp)
+ ) {
+ Column(modifier = Modifier.padding(20.dp)) {
+ Text(
+ text = "Location Distribution",
+ style = MaterialTheme.typography.headlineSmall,
+ fontWeight = FontWeight.Bold
+ )
+ Spacer(Modifier.height(16.dp))
+ AdvancedBarChart(
+ data = locationData.map { it.first to it.second.first },
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(300.dp)
+ )
+ }
+ }
+
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ elevation = CardDefaults.cardElevation(4.dp)
+ ) {
+ Column(modifier = Modifier.padding(20.dp)) {
+ Text(
+ text = "Location Breakdown",
+ style = MaterialTheme.typography.headlineSmall,
+ fontWeight = FontWeight.Bold
+ )
+ Spacer(Modifier.height(16.dp))
+
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text("Location", fontWeight = FontWeight.Bold, modifier = Modifier.weight(0.3f))
+ Text("Items", fontWeight = FontWeight.Bold, modifier = Modifier.weight(0.2f), textAlign = TextAlign.Center)
+ Text("Qty", fontWeight = FontWeight.Bold, modifier = Modifier.weight(0.2f), textAlign = TextAlign.Center)
+ Text("Value", fontWeight = FontWeight.Bold, modifier = Modifier.weight(0.3f), textAlign = TextAlign.End)
+ }
+ Spacer(Modifier.height(8.dp))
+ Divider()
+ Spacer(Modifier.height(8.dp))
+
+ locationData.forEach { (location, stats) ->
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 8.dp),
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text(location, modifier = Modifier.weight(0.3f), maxLines = 1, overflow = TextOverflow.Ellipsis)
+ Text("${stats.first}", modifier = Modifier.weight(0.2f), textAlign = TextAlign.Center)
+ Text("${stats.second}", modifier = Modifier.weight(0.2f), textAlign = TextAlign.Center)
+ Text("$${String.format("%.0f", stats.third)}", modifier = Modifier.weight(0.3f), textAlign = TextAlign.End)
+ }
+ Divider()
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun DetailedSizeAnalytics(items: List
- ) {
+ val sizes = items.groupBy { it.sizeCategory }
+ .mapValues { entry ->
+ val itemList = entry.value
+ Triple(
+ itemList.size,
+ itemList.sumOf { it.quantity },
+ itemList.sumOf { ((it.minPrice ?: 0.0) + (it.maxPrice ?: 0.0)) / 2 * it.quantity }
+ )
+ }
+ .toList()
+ .sortedByDescending { it.second.first }
+
+ Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ elevation = CardDefaults.cardElevation(4.dp)
+ ) {
+ Column(modifier = Modifier.padding(20.dp)) {
+ Text(
+ text = "Size Distribution",
+ style = MaterialTheme.typography.headlineSmall,
+ fontWeight = FontWeight.Bold
+ )
+ Spacer(Modifier.height(16.dp))
+ AdvancedPieChart(
+ data = sizes.map { it.first to it.second.first },
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(300.dp)
+ )
+ }
+ }
+
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ elevation = CardDefaults.cardElevation(4.dp)
+ ) {
+ Column(modifier = Modifier.padding(20.dp)) {
+ Text(
+ text = "Size Breakdown",
+ style = MaterialTheme.typography.headlineSmall,
+ fontWeight = FontWeight.Bold
+ )
+ Spacer(Modifier.height(16.dp))
+
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text("Size", fontWeight = FontWeight.Bold, modifier = Modifier.weight(0.3f))
+ Text("Items", fontWeight = FontWeight.Bold, modifier = Modifier.weight(0.2f), textAlign = TextAlign.Center)
+ Text("Qty", fontWeight = FontWeight.Bold, modifier = Modifier.weight(0.2f), textAlign = TextAlign.Center)
+ Text("Value", fontWeight = FontWeight.Bold, modifier = Modifier.weight(0.3f), textAlign = TextAlign.End)
+ }
+ Spacer(Modifier.height(8.dp))
+ Divider()
+ Spacer(Modifier.height(8.dp))
+
+ sizes.forEach { (size, stats) ->
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 8.dp),
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text(size, modifier = Modifier.weight(0.3f))
+ Text("${stats.first}", modifier = Modifier.weight(0.2f), textAlign = TextAlign.Center)
+ Text("${stats.second}", modifier = Modifier.weight(0.2f), textAlign = TextAlign.Center)
+ Text("$${String.format("%.0f", stats.third)}", modifier = Modifier.weight(0.3f), textAlign = TextAlign.End)
+ }
+ Divider()
+ }
+ }
+ }
+ }
+}
+
+// ======================================================================
+// ADVANCED CHART COMPONENTS
+// ======================================================================
+
+@Composable
+fun AdvancedPieChart(
+ data: List>,
+ modifier: Modifier = Modifier
+) {
+ val total = data.sumOf { it.second }.toFloat()
+ val colors = listOf(
+ Color(0xFF6200EE),
+ Color(0xFF03DAC5),
+ Color(0xFFFF6B6B),
+ Color(0xFF4ECDC4),
+ Color(0xFFFFBE0B),
+ Color(0xFFFFA500),
+ Color(0xFF9C27B0),
+ Color(0xFF00BCD4)
+ )
+
+ Row(
+ modifier = modifier,
+ horizontalArrangement = Arrangement.SpaceEvenly,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ // Pie Chart
+ Canvas(
+ modifier = Modifier
+ .size(200.dp)
+ .weight(1f)
+ ) {
+ val canvasSize = size.minDimension
+ val radius = canvasSize / 2.5f
+ val center = Offset(size.width / 2, size.height / 2)
+
+ var startAngle = -90f
+ data.forEachIndexed { index, (_, value) ->
+ val sweepAngle = (value / total) * 360f
+ drawArc(
+ color = colors[index % colors.size],
+ startAngle = startAngle,
+ sweepAngle = sweepAngle,
+ useCenter = true,
+ topLeft = Offset(center.x - radius, center.y - radius),
+ size = Size(radius * 2, radius * 2)
+ )
+ startAngle += sweepAngle
+ }
+
+ // Inner white circle for donut effect
+ drawCircle(
+ color = Color.White,
+ radius = radius * 0.5f,
+ center = center
+ )
+ }
+
+ // Legend
+ Column(
+ modifier = Modifier.weight(1f),
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ data.forEachIndexed { index, (label, value) ->
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Box(
+ modifier = Modifier
+ .size(16.dp)
+ .background(colors[index % colors.size], CircleShape)
+ )
+ Spacer(Modifier.width(8.dp))
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ text = label,
+ style = MaterialTheme.typography.bodyMedium,
+ fontWeight = FontWeight.Medium,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ Text(
+ text = "$value items (${String.format("%.1f", (value / total) * 100)}%)",
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun AdvancedBarChart(
+ data: List>,
+ modifier: Modifier = Modifier
+) {
+ val maxValue = data.maxOfOrNull { it.second }?.toFloat() ?: 1f
+ val barColor = MaterialTheme.colorScheme.primary
+
+ Column(
+ modifier = modifier,
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ data.forEach { (label, value) ->
+ Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text(
+ text = label,
+ style = MaterialTheme.typography.bodyMedium,
+ fontWeight = FontWeight.Medium,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ modifier = Modifier.weight(1f)
+ )
+ Text(
+ text = value.toString(),
+ style = MaterialTheme.typography.bodyMedium,
+ fontWeight = FontWeight.Bold
+ )
+ }
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(32.dp)
+ .background(
+ MaterialTheme.colorScheme.surfaceVariant,
+ RoundedCornerShape(8.dp)
+ )
+ ) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth(value / maxValue)
+ .height(32.dp)
+ .background(
+ barColor,
+ RoundedCornerShape(8.dp)
+ )
+ )
+ }
+ }
+ }
+ }
+}
+
+// ======================================================================
+// MINI CHART COMPONENTS FOR DASHBOARD
+// ======================================================================
+
+@Composable
+fun MiniCategoryChart(items: List
- ) {
+ val categories = items.groupBy { it.sizeCategory }
+ .mapValues { it.value.size }
+ .toList()
+ .sortedByDescending { it.second }
+ .take(3)
+
+ if (categories.isEmpty()) {
+ Text("No data", style = MaterialTheme.typography.bodySmall)
+ return
+ }
+
+ MiniPieChart(categories)
+}
+
+@Composable
+fun MiniConditionChart(items: List
- ) {
+ val conditions = items.groupBy { it.condition }
+ .mapValues { it.value.size }
+ .toList()
+ .take(3)
+
+ if (conditions.isEmpty()) {
+ Text("No data", style = MaterialTheme.typography.bodySmall)
+ return
+ }
+
+ MiniPieChart(conditions)
+}
+
+@Composable
+fun MiniLocationChart(items: List
- , garages: List) {
+ val locationData = garages.map { garage ->
+ garage.name to items.count { it.garageId == garage.id }
+ }.filter { it.second > 0 }
+ .take(3)
+
+ if (locationData.isEmpty()) {
+ Text("No data", style = MaterialTheme.typography.bodySmall)
+ return
+ }
+
+ MiniPieChart(locationData)
+}
+
+@Composable
+fun MiniSizeChart(items: List
- ) {
+ val sizes = items.groupBy { it.sizeCategory }
+ .mapValues { it.value.size }
+ .toList()
+ .take(3)
+
+ if (sizes.isEmpty()) {
+ Text("No data", style = MaterialTheme.typography.bodySmall)
+ return
+ }
+
+ MiniPieChart(sizes)
+}
+
+@Composable
+fun MiniPieChart(data: List>) {
+ val total = data.sumOf { it.second }.toFloat()
+ val colors = listOf(
+ Color(0xFF6200EE),
+ Color(0xFF03DAC5),
+ Color(0xFFFF6B6B)
+ )
+
+ Canvas(
+ modifier = Modifier.size(80.dp)
+ ) {
+ val canvasSize = size.minDimension
+ val radius = canvasSize / 2
+ val center = Offset(size.width / 2, size.height / 2)
+
+ var startAngle = -90f
+ data.forEachIndexed { index, (_, value) ->
+ val sweepAngle = (value / total) * 360f
+ drawArc(
+ color = colors[index % colors.size],
+ startAngle = startAngle,
+ sweepAngle = sweepAngle,
+ useCenter = true,
+ topLeft = Offset(center.x - radius, center.y - radius),
+ size = Size(radius * 2, radius * 2)
+ )
+ startAngle += sweepAngle
+ }
+ }
+}
+
+// ======================================================================
+// EXPANDABLE STAT CARD
+// ======================================================================
+
+@Composable
+fun ExpandableStatCard(
+ title: String,
+ value: String,
+ icon: ImageVector,
+ color: Color,
+ isExpanded: Boolean,
+ onToggle: () -> Unit,
+ content: @Composable () -> Unit
+) {
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable(onClick = onToggle),
+ colors = CardDefaults.cardColors(containerColor = color),
+ elevation = CardDefaults.cardElevation(4.dp)
+ ) {
+ Column(modifier = Modifier.padding(16.dp)) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Icon(icon, contentDescription = null, modifier = Modifier.size(32.dp))
+ Spacer(Modifier.width(12.dp))
+ Column {
+ Text(
+ text = value,
+ style = MaterialTheme.typography.headlineMedium,
+ fontWeight = FontWeight.Bold
+ )
+ Text(
+ text = title,
+ style = MaterialTheme.typography.bodyLarge
+ )
+ }
+ }
+ Icon(
+ imageVector = if (isExpanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore,
+ contentDescription = "Expand",
+ modifier = Modifier.size(28.dp)
+ )
+ }
+
+ AnimatedVisibility(
+ visible = isExpanded,
+ enter = expandVertically() + fadeIn(),
+ exit = shrinkVertically() + fadeOut()
+ ) {
+ Column {
+ Spacer(Modifier.height(12.dp))
+ Divider()
+ Spacer(Modifier.height(12.dp))
+ content()
+ }
+ }
+ }
+ }
+}
+
+// ======================================================================
+// CLICKABLE CHART CARD
+// ======================================================================
+
+@Composable
+fun ClickableChartCard(
+ title: String,
+ icon: ImageVector,
+ modifier: Modifier = Modifier,
+ onClick: () -> Unit,
+ content: @Composable () -> Unit
+) {
+ Card(
+ modifier = modifier
+ .height(180.dp)
+ .clickable(onClick = onClick),
+ elevation = CardDefaults.cardElevation(2.dp)
+ ) {
+ Column(
+ modifier = Modifier
+ .padding(12.dp)
+ .fillMaxSize()
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(6.dp)
+ ) {
+ Icon(
+ imageVector = icon,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.primary,
+ modifier = Modifier.size(18.dp)
+ )
+ Text(
+ text = title,
+ fontWeight = FontWeight.SemiBold,
+ style = MaterialTheme.typography.titleSmall,
+ fontSize = 13.sp
+ )
+ }
+ Spacer(Modifier.height(8.dp))
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ content()
+ }
+ }
+ }
+}
+
+// ======================================================================
+// BREAKDOWN COMPONENTS
+// ======================================================================
+
+@Composable
+fun ItemsBreakdown(items: List
- , garages: List) {
+ Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
+ garages.forEach { garage ->
+ val garageItems = items.filter { it.garageId == garage.id }
+ if (garageItems.isNotEmpty()) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 4.dp),
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text(
+ text = garage.name,
+ fontWeight = FontWeight.Medium,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ modifier = Modifier.weight(1f)
+ )
+ Text(
+ text = "${garageItems.size} items",
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+ }
+ Divider(modifier = Modifier.padding(vertical = 4.dp))
+
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text("Unique Items", fontWeight = FontWeight.Bold)
+ Text(items.size.toString(), fontWeight = FontWeight.Bold)
+ }
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text("Total Quantity", fontWeight = FontWeight.Bold)
+ Text(items.sumOf { it.quantity }.toString(), fontWeight = FontWeight.Bold)
+ }
+ }
+}
+
+@Composable
+fun ValueBreakdown(items: List
- , garages: List) {
+ Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
+ garages.forEach { garage ->
+ val garageItems = items.filter { it.garageId == garage.id }
+ if (garageItems.isNotEmpty()) {
+ val garageValue = garageItems.sumOf {
+ ((it.minPrice ?: 0.0) + (it.maxPrice ?: 0.0)) / 2 * it.quantity
+ }
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 4.dp),
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text(
+ text = garage.name,
+ fontWeight = FontWeight.Medium,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ modifier = Modifier.weight(1f)
+ )
+ Text(
+ text = "$${String.format("%.2f", garageValue)}",
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+ }
+ Divider(modifier = Modifier.padding(vertical = 4.dp))
+
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text("Average Item Value", fontWeight = FontWeight.Bold)
+ val avgValue = if (items.isNotEmpty())
+ items.sumOf { ((it.minPrice ?: 0.0) + (it.maxPrice ?: 0.0)) / 2 } / items.size
+ else 0.0
+ Text("$${String.format("%.2f", avgValue)}", fontWeight = FontWeight.Bold)
+ }
+ }
+}
+
+// ======================================================================
+// GARAGE ITEMS LIST VIEW
+// ======================================================================
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun GarageItemsView(
+ garage: Garage,
+ items: List
- ,
+ onBackClick: () -> Unit,
+ onItemClick: (Item) -> Unit
+) {
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ title = { Text(garage.name) },
+ navigationIcon = {
+ IconButton(onClick = onBackClick) {
+ Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
+ }
+ }
+ )
+ }
+ ) { padding ->
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(padding),
+ contentPadding = PaddingValues(16.dp),
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ if (items.isEmpty()) {
+ item {
+ Box(
+ modifier = Modifier.fillParentMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ text = "No items found in this garage.",
+ style = MaterialTheme.typography.bodyLarge,
+ color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
+ )
+ }
+ }
+ } else {
+ items(items) { item ->
+ CompactItemCard(
+ item = item,
+ onClick = { onItemClick(item) }
+ )
+ }
+ }
+ }
+ }
+}
+
+// ======================================================================
+// ITEM DETAIL SCREEN
+// ======================================================================
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun ItemDetailScreen(
+ item: Item,
+ garage: Garage,
+ onBackClick: () -> Unit
+) {
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ title = {
+ Text(
+ text = item.name,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ },
+ navigationIcon = {
+ IconButton(onClick = onBackClick) {
+ Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
+ }
+ }
+ )
+ }
+ ) { padding ->
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(padding),
+ contentPadding = PaddingValues(16.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ item {
+ Text(
+ text = item.name,
+ style = MaterialTheme.typography.headlineMedium,
+ fontWeight = FontWeight.Bold
+ )
+ }
+
+ item {
+ InfoSection(title = "Basic Information") {
+ DetailRow("Quantity", item.quantity.toString())
+ DetailRow("Condition", item.condition)
+ DetailRow("Functionality", item.functionality)
+ DetailRow("Size Category", item.sizeCategory)
+ item.modelNumber?.let { DetailRow("Model Number", it) }
+ }
+ }
+
+ item {
+ InfoSection(title = "Location") {
+ DetailRow("Garage", garage.name)
+ }
+ }
+
+ if (item.dimensions != null || item.weight != null) {
+ item {
+ InfoSection(title = "Physical Specifications") {
+ item.dimensions?.let { DetailRow("Dimensions", it) }
+ item.weight?.let { DetailRow("Weight", "${it} lbs") }
+ }
+ }
+ }
+
+ val avgPrice = ((item.minPrice ?: 0.0) + (item.maxPrice ?: 0.0)) / 2
+ if (avgPrice > 0) {
+ item {
+ InfoSection(title = "Pricing") {
+ item.minPrice?.let {
+ DetailRow("Min Price", "$${String.format("%.2f", it)}")
+ }
+ item.maxPrice?.let {
+ DetailRow("Max Price", "$${String.format("%.2f", it)}")
+ }
+ DetailRow("Est. Value", "$${String.format("%.2f", avgPrice)}")
+ DetailRow(
+ "Total Value",
+ "$${String.format("%.2f", avgPrice * item.quantity)}"
+ )
+ }
+ }
+ }
+
+ item.description?.let { desc ->
+ if (desc.isNotBlank()) {
+ item {
+ InfoSection(title = "Description") {
+ Text(
+ text = desc,
+ style = MaterialTheme.typography.bodyMedium
+ )
+ }
+ }
+ }
+ }
+
+ item.webLink?.let { link ->
+ if (link.isNotBlank()) {
+ item {
+ InfoSection(title = "Web Link") {
+ Text(
+ text = link,
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.primary
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+// ======================================================================
+// HELPER COMPOSABLES
+// ======================================================================
+
+@Composable
+fun GarageStatCard(
+ garageName: String,
+ itemCount: Int,
+ onClick: () -> Unit
+) {
+ Card(
+ modifier = Modifier
+ .size(width = 140.dp, height = 100.dp)
+ .clickable(onClick = onClick),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.tertiaryContainer
+ ),
+ elevation = CardDefaults.cardElevation(2.dp)
+ ) {
+ Column(
+ modifier = Modifier
+ .padding(12.dp)
+ .fillMaxSize(),
+ verticalArrangement = Arrangement.SpaceBetween
+ ) {
+ Icon(
+ imageVector = Icons.Default.HomeWork,
+ contentDescription = null,
+ modifier = Modifier.size(24.dp),
+ tint = MaterialTheme.colorScheme.onTertiaryContainer
+ )
+ Column {
+ Text(
+ text = garageName,
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.Bold,
+ color = MaterialTheme.colorScheme.onTertiaryContainer,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ Text(
+ text = "$itemCount items",
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onTertiaryContainer.copy(alpha = 0.8f)
+ )
+ }
+ }
+ }
+}
+
+@Composable
+fun CompactItemCard(
+ item: Item,
+ onClick: () -> Unit
+) {
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable(onClick = onClick),
+ elevation = CardDefaults.cardElevation(2.dp)
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ text = item.name,
+ fontWeight = FontWeight.Bold,
+ style = MaterialTheme.typography.titleMedium,
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis
+ )
+ Spacer(Modifier.height(4.dp))
+ Text(
+ text = "Qty: ${item.quantity} • ${item.condition}",
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ Icon(
+ imageVector = Icons.Default.ExpandMore,
+ contentDescription = "View details",
+ modifier = Modifier.size(24.dp),
+ tint = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+}
+
+@Composable
+fun InfoSection(
+ title: String,
+ content: @Composable () -> Unit
+) {
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ elevation = CardDefaults.cardElevation(2.dp)
+ ) {
+ Column(modifier = Modifier.padding(16.dp)) {
+ Text(
+ text = title,
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.Bold,
+ color = MaterialTheme.colorScheme.primary
+ )
+ Spacer(Modifier.height(12.dp))
+ content()
+ }
+ }
+}
+
+@Composable
+fun DetailRow(label: String, value: String) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 4.dp),
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text(
+ text = label,
+ fontWeight = FontWeight.Medium,
+ style = MaterialTheme.typography.bodyMedium,
+ modifier = Modifier.weight(0.4f)
+ )
+ Text(
+ text = value,
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ modifier = Modifier.weight(0.6f),
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/samuel/inventorymanager/screens/DataModels.kt b/app/src/main/java/com/samuel/inventorymanager/screens/DataModels.kt
new file mode 100644
index 0000000..e37e203
--- /dev/null
+++ b/app/src/main/java/com/samuel/inventorymanager/screens/DataModels.kt
@@ -0,0 +1,54 @@
+package com.samuel.inventorymanager.screens
+
+// --- Core Data Structures ---
+data class Item(
+ val id: String,
+ val name: String,
+ val modelNumber: String?,
+ val description: String?,
+ val webLink: String?,
+ val condition: String,
+ val functionality: String,
+ val garageId: String,
+ val cabinetId: String,
+ val shelfId: String,
+ val boxId: String?,
+ val quantity: Int,
+ val minPrice: Double?,
+ val maxPrice: Double?,
+ val weight: Double?,
+ val sizeCategory: String,
+ val dimensions: String?,
+ val images: List
+)
+
+data class Box(val id: String, val name: String)
+data class Shelf(val id: String, val name: String, val boxes: List)
+data class Cabinet(val id: String, val name: String, val shelves: List)
+data class Garage(val id: String, val name: String, val cabinets: List)
+
+// --- History Tracking ---
+sealed class HistoryAction {
+ object Added : HistoryAction()
+ object Updated : HistoryAction()
+ object Removed : HistoryAction()
+ data class QuantityChanged(val oldQuantity: Int, val newQuantity: Int) : HistoryAction()
+ data class CheckedOut(val userId: String) : HistoryAction()
+ data class CheckedIn(val userId: String) : HistoryAction()
+}
+
+data class HistoryEntry(
+ val id: String,
+ val itemId: String,
+ val itemName: String,
+ val action: HistoryAction,
+ val description: String,
+ val timestamp: Long = System.currentTimeMillis()
+)
+
+// --- App Data Bundle for Saving/Loading ---
+data class AppData(
+ val garages: List,
+ val items: List
- ,
+ val history: List
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/samuel/inventorymanager/screens/HelpScreen.kt b/app/src/main/java/com/samuel/inventorymanager/screens/HelpScreen.kt
new file mode 100644
index 0000000..948aed0
--- /dev/null
+++ b/app/src/main/java/com/samuel/inventorymanager/screens/HelpScreen.kt
@@ -0,0 +1,372 @@
+package com.samuel.inventorymanager.screens
+
+import android.content.Intent
+import android.net.Uri
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Code
+import androidx.compose.material.icons.filled.Email
+import androidx.compose.material.icons.filled.ExpandMore
+import androidx.compose.material.icons.filled.Info
+import androidx.compose.material.icons.filled.RocketLaunch
+import androidx.compose.material.icons.filled.SdStorage
+import androidx.compose.material.icons.filled.Settings
+import androidx.compose.material.icons.filled.ShoppingBag
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.Divider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.rotate
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+
+@Composable
+fun HelpScreen() {
+ val context = LocalContext.current
+
+ LazyColumn(
+ modifier = Modifier.fillMaxSize(),
+ contentPadding = PaddingValues(16.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ item {
+ // --- HEADER ---
+ Column(
+ modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text("❓", fontSize = 56.sp)
+ Spacer(Modifier.height(8.dp))
+ Text(
+ "Help & Support Center",
+ style = MaterialTheme.typography.headlineMedium,
+ fontWeight = FontWeight.Bold
+ )
+ Text(
+ "Your guide to mastering Android Inventory Pro",
+ style = MaterialTheme.typography.bodyLarge,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+
+ // --- GETTING STARTED ---
+ item {
+ CollapsibleHelpSection(
+ title = "Getting Started Guide",
+ icon = Icons.Default.RocketLaunch,
+ initiallyExpanded = true // Start with this one open
+ ) {
+ HelpContentBlock(
+ question = "Step 1: Create Your First Location",
+ answer = "Go to the 'Locations' tab and tap the 'Add Garage' button. A 'Garage' can be any main area, like a 'Workshop,' 'Storage Unit,' or 'Kitchen.'"
+ )
+ HelpContentBlock(
+ question = "Step 2: Build Your Hierarchy",
+ answer = "Inside your new Garage, you can add Cabinets. Inside Cabinets, add Shelves. Inside Shelves, add Boxes. This structure helps you know exactly where everything is."
+ )
+ HelpContentBlock(
+ question = "Step 3: Add Your First Item",
+ answer = "Navigate to the 'Items' tab and tap 'New'. Fill in the details. The most important fields are Item Name and its Location (Garage)."
+ )
+ HelpContentBlock(
+ question = "Step 4: Add Photos",
+ answer = "While editing an item, tap 'Camera' to take a photo or 'Upload' to select one from your device. A picture is worth a thousand words!"
+ )
+ HelpContentBlock(
+ question = "Step 5: Find Your Items",
+ answer = "Use the 'Search' tab to instantly find anything you've added. For a complete spreadsheet-like view of all your items, visit the 'Overview' tab."
+ )
+ }
+ }
+
+ // --- MANAGING ITEMS ---
+ item {
+ CollapsibleHelpSection(
+ title = "Managing Your Items",
+ icon = Icons.Default.ShoppingBag
+ ) {
+ HelpContentBlock(
+ question = "How does Auto-Save work?",
+ answer = "The app automatically saves your changes a few seconds after you stop typing in the 'Items' form. A '💾 Auto-saved' message will briefly appear. You can disable this in 'Settings' > 'Data Management' if you prefer to save manually."
+ )
+ HelpContentBlock(
+ question = "What are OCR and AI?",
+ answer = "These are powerful tools to help you add items faster. After taking a picture:\n• OCR scans the image for text (like a model number).\n• AI analyzes the image and tries to identify the item, suggesting a name and description for you."
+ )
+ HelpContentBlock(
+ question = "How do I add custom options for Condition or Functionality?",
+ answer = "In the 'Items' tab, next to the 'Item Condition' or 'Functionality' dropdowns, tap the '+ Add' button. This lets you create your own custom options (e.g., 'Slightly Scratched' or 'Needs Batteries') that will be saved for future use."
+ )
+ }
+ }
+
+ // --- DATA & BACKUP ---
+ item {
+ CollapsibleHelpSection(
+ title = "Data, Backup & Sync",
+ icon = Icons.Default.SdStorage
+ ) {
+ HelpContentBlock(
+ question = "Where is my data stored?",
+ answer = "Your inventory data is stored locally in the app's private storage on your device. This means it works offline and is completely private. For safety, it is highly recommended you create regular backups."
+ )
+ HelpContentBlock(
+ question = "How do I back up and restore my data?",
+ answer = "Go to 'Settings' > 'Data Management'.\n• Export Data: This saves a complete copy of your inventory to a single '.json' file in your device's Downloads folder.\n• Import Data: This lets you load an inventory from a file. Warning: Importing will overwrite all of your current data.\n• Backup to Google Drive: Sign in to your Google account and save your backup file directly to a private folder in Google Drive."
+ )
+ HelpContentBlock(
+ question = "How do I sync with the computer app?",
+ answer = "The 'Sync' tab connects your device to our companion desktop app.\n1. Tap 'Generate' on your phone to get a Sync Key.\n2. Enter this key into the computer app.\n3. Enter the key from the computer app into your phone and tap 'Pair'."
+ )
+ }
+ }
+
+ // --- ADVANCED SETTINGS ---
+ item {
+ CollapsibleHelpSection(
+ title = "Advanced Settings & APIs",
+ icon = Icons.Default.Settings
+ ) {
+ HelpContentBlock(
+ question = "What are API Keys and do I need them?",
+ answer = "API Keys are special codes that let this app connect to powerful online services for OCR and AI. The app works perfectly fine without them using its free, built-in tools. However, adding keys can provide even more accurate results."
+ )
+ HelpContentBlock(
+ question = "How does the Priority Fallback system work?",
+ answer = "In the OCR and AI settings, you can reorder the list of services. When you use a feature, the app tries the first service. If it fails, it automatically 'falls back' to the next one, ensuring you always get a result from the best available service."
+ )
+ }
+ }
+
+ // --- APP INFO AND CONTACT CARD ---
+ item {
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ elevation = CardDefaults.cardElevation(2.dp),
+ colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)
+ ) {
+ Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
+ // App Info Section
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Icon(Icons.Default.Info, null, tint = MaterialTheme.colorScheme.primary)
+ Spacer(Modifier.width(12.dp))
+ Text("App Information", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
+ }
+ InfoRow("Version:", "2.0 (Native)")
+ InfoRow("Platform:", "Android (Jetpack Compose)")
+ InfoRow("Data Storage:", "Local Device Storage")
+
+ Divider(modifier = Modifier.padding(vertical = 8.dp))
+
+ // Contact Section
+ ContactRow(
+ icon = Icons.Default.Code,
+ title = "GitHub of Parminder",
+ subtitle = "github.com/JohnJackson12",
+ onClick = {
+ val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/JohnJackson12"))
+ context.startActivity(intent)
+ }
+ )
+ Spacer(Modifier.height(16.dp))
+ ContactRow(
+ icon = Icons.Default.Code,
+ title = "GitHub of Samuel",
+ subtitle = "github.com/SamS34",
+ onClick = {
+ val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/SamS34"))
+ context.startActivity(intent)
+ }
+ )
+ Spacer(Modifier.height(16.dp))
+ ContactRow(
+ icon = Icons.Default.Email,
+ title = "Email of Parminder",
+ subtitle = "parminder.nz@gmail.com",
+ onClick = {
+ val intent = Intent(Intent.ACTION_SENDTO).apply {
+ data = Uri.parse("mailto:parminder.nz@gmail.com")
+ }
+ context.startActivity(intent)
+ }
+ )
+ Spacer(Modifier.height(16.dp))
+ ContactRow(
+ icon = Icons.Default.Email,
+ title = "Email of Samuel",
+ subtitle = "sam.of.s34@gmail.com",
+ onClick = {
+ val intent = Intent(Intent.ACTION_SENDTO).apply {
+ data = Uri.parse("mailto:sam.of.s34@gmail.com")
+ }
+ context.startActivity(intent)
+ }
+ )
+ }
+ }
+ }
+ }
+}
+
+
+// ======================================================================
+// HELPER COMPOSABLES
+// These are the reusable building blocks that make the screen work! ✨
+// ======================================================================
+
+/**
+ * A reusable, collapsible Card for displaying FAQ-style help content.
+ */
+@Composable
+private fun CollapsibleHelpSection(
+ title: String,
+ icon: ImageVector,
+ initiallyExpanded: Boolean = false,
+ content: @Composable ColumnScope.() -> Unit
+) {
+ var expanded by remember { mutableStateOf(initiallyExpanded) }
+ val rotationAngle by animateFloatAsState(targetValue = if (expanded) 180f else 0f, label = "rotation")
+
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ elevation = CardDefaults.cardElevation(2.dp),
+ colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface)
+ ) {
+ Column {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable { expanded = !expanded }
+ .padding(16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(icon, contentDescription = null, tint = MaterialTheme.colorScheme.primary)
+ Spacer(Modifier.width(16.dp))
+ Text(
+ text = title,
+ style = MaterialTheme.typography.titleLarge,
+ fontWeight = FontWeight.SemiBold,
+ modifier = Modifier.weight(1f)
+ )
+ Icon(
+ imageVector = Icons.Default.ExpandMore,
+ contentDescription = if (expanded) "Collapse" else "Expand",
+ modifier = Modifier.rotate(rotationAngle)
+ )
+ }
+
+ AnimatedVisibility(visible = expanded) {
+ Column(
+ modifier = Modifier
+ .padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
+ .fillMaxWidth(),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ Divider()
+ Spacer(Modifier.height(4.dp))
+ content()
+ }
+ }
+ }
+ }
+}
+
+
+/**
+ * Formats a question and answer block within a help section.
+ */
+@Composable
+private fun HelpContentBlock(question: String, answer: String) {
+ Column {
+ Text(
+ text = question,
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.Bold
+ )
+ Spacer(Modifier.height(4.dp))
+ Text(
+ text = answer,
+ style = MaterialTheme.typography.bodyLarge,
+ lineHeight = 24.sp
+ )
+ }
+}
+
+/**
+ * Displays a clickable row for contact information.
+ */
+@Composable
+private fun ContactRow(
+ icon: ImageVector,
+ title: String,
+ subtitle: String,
+ onClick: () -> Unit
+) {
+ Row(
+ modifier = Modifier.fillMaxWidth().clickable(onClick = onClick),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(icon, null, modifier = Modifier.size(32.dp), tint = MaterialTheme.colorScheme.onSurfaceVariant)
+ Spacer(Modifier.width(16.dp))
+ Column {
+ Text(title, style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.Medium)
+ Text(
+ subtitle,
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.primary,
+ textDecoration = TextDecoration.Underline
+ )
+ }
+ }
+}
+
+
+/**
+ * Displays a simple "Label: Value" row for app information.
+ */
+@Composable
+private fun InfoRow(label: String, value: String) {
+ Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
+ Text(
+ text = label,
+ style = MaterialTheme.typography.bodyLarge,
+ fontWeight = FontWeight.SemiBold,
+ modifier = Modifier.weight(0.4f)
+ )
+ Text(
+ text = value,
+ style = MaterialTheme.typography.bodyLarge,
+ modifier = Modifier.weight(0.6f),
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/samuel/inventorymanager/screens/HistoryScreen.kt b/app/src/main/java/com/samuel/inventorymanager/screens/HistoryScreen.kt
new file mode 100644
index 0000000..27bb1fd
--- /dev/null
+++ b/app/src/main/java/com/samuel/inventorymanager/screens/HistoryScreen.kt
@@ -0,0 +1,350 @@
+package com.samuel.inventorymanager.screens
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+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.filled.Add
+import androidx.compose.material.icons.filled.AddCircle
+import androidx.compose.material.icons.filled.Clear
+import androidx.compose.material.icons.filled.Delete
+import androidx.compose.material.icons.filled.DeleteForever
+import androidx.compose.material.icons.filled.Edit
+import androidx.compose.material.icons.filled.HistoryToggleOff
+import androidx.compose.material.icons.filled.Login
+import androidx.compose.material.icons.filled.Logout
+import androidx.compose.material.icons.filled.Search
+import androidx.compose.material.icons.filled.SwapVert
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.FilterChip
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun HistoryScreen(
+ history: List,
+ items: List
- ,
+ onItemClick: (Item) -> Unit,
+ onClearHistory: () -> Unit
+) {
+ var searchQuery by remember { mutableStateOf("") }
+ var selectedActionType by remember { mutableStateOf(null) }
+
+ // Filter history based on search and selected action type
+ val filteredHistory = remember(searchQuery, selectedActionType, history) {
+ history.filter { entry ->
+ val matchesAction = selectedActionType == null ||
+ when (entry.action) {
+ is HistoryAction.Added -> selectedActionType == ActionType.ADDED
+ is HistoryAction.Updated -> selectedActionType == ActionType.UPDATED
+ is HistoryAction.Removed -> selectedActionType == ActionType.REMOVED
+ is HistoryAction.QuantityChanged -> selectedActionType == ActionType.QUANTITY_CHANGED
+ is HistoryAction.CheckedOut -> selectedActionType == ActionType.CHECKED_OUT
+ is HistoryAction.CheckedIn -> selectedActionType == ActionType.CHECKED_IN
+ }
+
+ val matchesSearch = searchQuery.isBlank() ||
+ entry.itemName.contains(searchQuery, ignoreCase = true) ||
+ entry.description.contains(searchQuery, ignoreCase = true)
+
+ matchesAction && matchesSearch
+ }
+ }
+
+ Column(modifier = Modifier.fillMaxSize()) {
+ HistoryHeader(
+ searchQuery = searchQuery,
+ onSearchQueryChange = { searchQuery = it },
+ selectedActionType = selectedActionType,
+ onActionSelect = { actionType ->
+ selectedActionType = if (selectedActionType == actionType) null else actionType
+ },
+ onClearHistory = onClearHistory
+ )
+
+ if (filteredHistory.isEmpty()) {
+ EmptyHistoryState()
+ } else {
+ LazyColumn(
+ modifier = Modifier.fillMaxSize(),
+ contentPadding = PaddingValues(16.dp),
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ items(filteredHistory, key = { it.id }) { entry ->
+ HistoryItemCard(
+ entry = entry,
+ onClick = {
+ items.find { it.id == entry.itemId }?.let { item ->
+ onItemClick(item)
+ }
+ }
+ )
+ }
+ }
+ }
+ }
+}
+
+// Helper enum to simplify filtering
+enum class ActionType {
+ ADDED, UPDATED, REMOVED, QUANTITY_CHANGED, CHECKED_OUT, CHECKED_IN
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun HistoryHeader(
+ searchQuery: String,
+ onSearchQueryChange: (String) -> Unit,
+ selectedActionType: ActionType?,
+ onActionSelect: (ActionType) -> Unit,
+ onClearHistory: () -> Unit
+) {
+ Surface(
+ modifier = Modifier.fillMaxWidth(),
+ shadowElevation = 4.dp
+ ) {
+ Column(
+ modifier = Modifier.padding(16.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ OutlinedTextField(
+ value = searchQuery,
+ onValueChange = onSearchQueryChange,
+ modifier = Modifier.fillMaxWidth(),
+ placeholder = { Text("Search by item name or change...") },
+ leadingIcon = { Icon(Icons.Default.Search, null) },
+ trailingIcon = {
+ if (searchQuery.isNotEmpty()) {
+ IconButton({ onSearchQueryChange("") }) {
+ Icon(Icons.Default.Clear, "Clear")
+ }
+ }
+ },
+ shape = RoundedCornerShape(12.dp)
+ )
+
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ modifier = Modifier.weight(1f)
+ ) {
+ FilterChip(
+ selected = selectedActionType == ActionType.ADDED,
+ onClick = { onActionSelect(ActionType.ADDED) },
+ label = { Text("Added") },
+ leadingIcon = {
+ Icon(Icons.Default.Add, null, modifier = Modifier.size(18.dp))
+ }
+ )
+ FilterChip(
+ selected = selectedActionType == ActionType.UPDATED,
+ onClick = { onActionSelect(ActionType.UPDATED) },
+ label = { Text("Updated") },
+ leadingIcon = {
+ Icon(Icons.Default.Edit, null, modifier = Modifier.size(18.dp))
+ }
+ )
+ FilterChip(
+ selected = selectedActionType == ActionType.REMOVED,
+ onClick = { onActionSelect(ActionType.REMOVED) },
+ label = { Text("Removed") },
+ leadingIcon = {
+ Icon(Icons.Default.Delete, null, modifier = Modifier.size(18.dp))
+ }
+ )
+ }
+ IconButton(onClick = onClearHistory) {
+ Icon(
+ Icons.Default.DeleteForever,
+ "Clear History",
+ tint = MaterialTheme.colorScheme.error
+ )
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun HistoryItemCard(entry: HistoryEntry, onClick: () -> Unit) {
+ val actionDetails = when (entry.action) {
+ is HistoryAction.Added -> ActionDetails(
+ Icons.Default.AddCircle,
+ "Added",
+ MaterialTheme.colorScheme.primary
+ )
+ is HistoryAction.Updated -> ActionDetails(
+ Icons.Default.Edit,
+ "Updated",
+ Color(0xFFF59E0B)
+ )
+ is HistoryAction.Removed -> ActionDetails(
+ Icons.Default.Delete,
+ "Removed",
+ MaterialTheme.colorScheme.error
+ )
+ is HistoryAction.QuantityChanged -> {
+ val qtyChange = entry.action as HistoryAction.QuantityChanged
+ ActionDetails(
+ Icons.Default.SwapVert,
+ "Qty: ${qtyChange.oldQuantity} → ${qtyChange.newQuantity}",
+ Color(0xFF8B5CF6)
+ )
+ }
+ is HistoryAction.CheckedOut -> {
+ val checkout = entry.action as HistoryAction.CheckedOut
+ ActionDetails(
+ Icons.Default.Logout,
+ "Checked Out (${checkout.userId})",
+ Color(0xFFEF4444)
+ )
+ }
+ is HistoryAction.CheckedIn -> {
+ val checkin = entry.action as HistoryAction.CheckedIn
+ ActionDetails(
+ Icons.Default.Login,
+ "Checked In (${checkin.userId})",
+ Color(0xFF10B981)
+ )
+ }
+ }
+
+ Card(
+ modifier = Modifier.fillMaxWidth().clickable(onClick = onClick),
+ elevation = CardDefaults.cardElevation(2.dp)
+ ) {
+ Row(
+ modifier = Modifier.padding(16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ imageVector = actionDetails.icon,
+ contentDescription = actionDetails.label,
+ modifier = Modifier.size(32.dp),
+ tint = actionDetails.color
+ )
+
+ Spacer(Modifier.width(16.dp))
+
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ text = entry.itemName,
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.Bold,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ Text(
+ text = entry.description,
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ Text(
+ text = actionDetails.label,
+ style = MaterialTheme.typography.bodySmall,
+ color = actionDetails.color,
+ fontWeight = FontWeight.Medium
+ )
+ }
+
+ Spacer(Modifier.width(8.dp))
+
+ Text(
+ text = formatTimestamp(entry.timestamp),
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
+ )
+ }
+ }
+}
+
+private data class ActionDetails(
+ val icon: ImageVector,
+ val label: String,
+ val color: Color
+)
+
+@Composable
+private fun EmptyHistoryState() {
+ Box(
+ modifier = Modifier.fillMaxSize().padding(16.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ Icon(
+ Icons.Default.HistoryToggleOff,
+ "No History",
+ modifier = Modifier.size(80.dp),
+ tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f)
+ )
+ Text(
+ "No History Yet",
+ style = MaterialTheme.typography.headlineSmall
+ )
+ Text(
+ "Changes you make to items and locations will appear here.",
+ style = MaterialTheme.typography.bodyMedium,
+ textAlign = TextAlign.Center,
+ color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
+ )
+ }
+ }
+}
+
+private fun formatTimestamp(timestamp: Long): String {
+ val now = System.currentTimeMillis()
+ val diff = now - timestamp
+
+ return when {
+ diff < 60_000 -> "Just now"
+ diff < 3600_000 -> "${diff / 60_000}m ago"
+ diff < 86400_000 -> "${diff / 3600_000}h ago"
+ diff < 604800_000 -> "${diff / 86400_000}d ago"
+ else -> {
+ val format = SimpleDateFormat("MMM d", Locale.getDefault())
+ format.format(Date(timestamp))
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/samuel/inventorymanager/screens/ImagesScreen.kt b/app/src/main/java/com/samuel/inventorymanager/screens/ImagesScreen.kt
new file mode 100644
index 0000000..a8c4af9
--- /dev/null
+++ b/app/src/main/java/com/samuel/inventorymanager/screens/ImagesScreen.kt
@@ -0,0 +1,167 @@
+package com.samuel.inventorymanager.screens
+
+import android.net.Uri
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.lazy.grid.items
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ImageSearch
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import coil.compose.rememberAsyncImagePainter
+
+// We create a simple data class to make the logic cleaner.
+// It bundles an image with the item it belongs to.
+data class ImageEntry(
+ val item: Item,
+ val imageUrl: String
+)
+
+@Composable
+fun ImagesScreen(
+ items: List
- ,
+ onItemClick: (Item) -> Unit // This function will trigger the navigation
+) {
+ // We first gather all images from all items into a single, flat list.
+ val allImages = items.flatMap { item ->
+ item.images.map { imageUrl ->
+ ImageEntry(item = item, imageUrl = imageUrl)
+ }
+ }
+
+ if (allImages.isEmpty()) {
+ // Show a helpful message if no images have been added yet.
+ EmptyState()
+ } else {
+ // Display the images in a responsive, vertical grid.
+ LazyVerticalGrid(
+ columns = GridCells.Adaptive(minSize = 120.dp), // This makes the grid look good on any screen size!
+ modifier = Modifier.fillMaxSize(),
+ contentPadding = PaddingValues(16.dp),
+ horizontalArrangement = Arrangement.spacedBy(16.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ items(allImages) { imageEntry ->
+ ImageCard(
+ imageEntry = imageEntry,
+ onClick = {
+ // When an image is clicked, we call the navigation function
+ // with the item that the image belongs to.
+ onItemClick(imageEntry.item)
+ }
+ )
+ }
+ }
+ }
+}
+
+
+// ======================================================================
+// HELPER COMPOSABLES
+// These are the building blocks that make the screen look polished. ✨
+// ======================================================================
+
+@Composable
+private fun ImageCard(
+ imageEntry: ImageEntry,
+ onClick: () -> Unit
+) {
+ Card(
+ modifier = Modifier
+ .aspectRatio(1f) // This makes the card a perfect square
+ .clickable(onClick = onClick),
+ elevation = CardDefaults.cardElevation(4.dp),
+ shape = MaterialTheme.shapes.medium
+ ) {
+ Box(modifier = Modifier.fillMaxSize()) {
+ // The Image itself, which fills the entire card
+ Image(
+ painter = rememberAsyncImagePainter(model = Uri.parse(imageEntry.imageUrl)),
+ contentDescription = imageEntry.item.name,
+ modifier = Modifier.fillMaxSize(),
+ contentScale = ContentScale.Crop // This ensures the image fills the square without stretching
+ )
+
+ // A semi-transparent overlay at the bottom to make the text readable
+ Box(
+ modifier = Modifier.run {
+ fillMaxWidth()
+ .fillMaxHeight(0.4f)
+ .background(
+ brush = androidx.compose.ui.graphics.Brush.verticalGradient(
+ colors = listOf(Color.Transparent, Color.Black.copy(alpha = 0.8f))
+ )
+ )
+ .align(Alignment.BottomCenter)
+ }
+ )
+
+ // The name of the item the image belongs to
+ Text(
+ text = imageEntry.item.name,
+ modifier = Modifier
+ .align(Alignment.BottomStart)
+ .padding(8.dp),
+ color = Color.White,
+ fontWeight = FontWeight.Bold,
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+ }
+}
+
+@Composable
+private fun EmptyState() {
+ Box(
+ modifier = Modifier.fillMaxSize().padding(16.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ Icon(
+ imageVector = Icons.Default.ImageSearch,
+ contentDescription = "No Images",
+ modifier = Modifier.size(80.dp),
+ tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f)
+ )
+ Text(
+ text = "No Images Found",
+ style = MaterialTheme.typography.headlineSmall
+ )
+ Text(
+ text = "Add items with pictures using the camera or upload button on the 'Create Item' screen.",
+ style = MaterialTheme.typography.bodyMedium,
+ textAlign = TextAlign.Center,
+ color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/samuel/inventorymanager/screens/LocationsScreen.kt b/app/src/main/java/com/samuel/inventorymanager/screens/LocationsScreen.kt
new file mode 100644
index 0000000..0619484
--- /dev/null
+++ b/app/src/main/java/com/samuel/inventorymanager/screens/LocationsScreen.kt
@@ -0,0 +1,258 @@
+package com.samuel.inventorymanager.screens
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ViewList
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.icons.filled.AttachMoney
+import androidx.compose.material.icons.filled.ExpandLess
+import androidx.compose.material.icons.filled.ExpandMore
+import androidx.compose.material.icons.filled.HomeWork
+import androidx.compose.material.icons.filled.Inventory
+import androidx.compose.material.icons.filled.Inventory2
+import androidx.compose.material.icons.filled.Kitchen
+import androidx.compose.material.icons.filled.LocationCity
+import androidx.compose.material.icons.filled.MoreVert
+import androidx.compose.material3.Button
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.FloatingActionButton
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.material3.surfaceColorAtElevation
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import java.util.Locale
+
+@Composable
+fun LocationsScreen(
+ garages: List,
+ items: List
- ,
+ onAddGarage: () -> Unit,
+ onAddCabinet: (garageId: String) -> Unit,
+ onAddShelf: (cabinetId: String) -> Unit,
+ onAddBox: (shelfId: String) -> Unit,
+ onRenameLocation: (id: String, oldName: String, type: String) -> Unit,
+ onDeleteLocation: (id: String, name: String, type: String) -> Unit
+) {
+ Scaffold(
+ floatingActionButton = {
+ FloatingActionButton(onClick = onAddGarage) {
+ Icon(Icons.Default.Add, contentDescription = "Add Garage")
+ }
+ }
+ ) { paddingValues ->
+ if (garages.isEmpty()) {
+ EmptyState(onAddGarage, modifier = Modifier.padding(paddingValues))
+ } else {
+ LazyColumn(
+ modifier = Modifier.fillMaxSize().padding(paddingValues),
+ contentPadding = PaddingValues(16.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ items(garages, key = { it.id }) { garage ->
+ val itemsInGarage = items.filter { it.garageId == garage.id }
+ GarageSection(
+ garage = garage,
+ itemsInGarage = itemsInGarage,
+ onAddCabinet = { onAddCabinet(garage.id) },
+ onAddShelf = onAddShelf,
+ onAddBox = onAddBox,
+ onRenameLocation = onRenameLocation,
+ onDeleteLocation = onDeleteLocation
+ )
+ }
+ }
+ }
+ }
+}
+
+// ======================================================================
+// SECTION COMPOSABLES
+// ======================================================================
+
+@Composable
+private fun GarageSection(garage: Garage, itemsInGarage: List
- , onAddCabinet: () -> Unit, onAddShelf: (cabinetId: String) -> Unit, onAddBox: (shelfId: String) -> Unit, onRenameLocation: (id: String, oldName: String, type: String) -> Unit, onDeleteLocation: (id: String, name: String, type: String) -> Unit) {
+ var isExpanded by remember { mutableStateOf(false) }
+ val totalValue = itemsInGarage.sumOf { ((it.minPrice ?: 0.0) + (it.maxPrice ?: 0.0)) / 2 * it.quantity }
+
+ Card(elevation = CardDefaults.cardElevation(4.dp), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp))) {
+ Column {
+ SectionHeader(
+ title = garage.name, icon = Icons.Default.HomeWork, isExpanded = isExpanded,
+ onToggleExpand = { isExpanded = !isExpanded },
+ onAddClick = onAddCabinet,
+ onRenameClick = { onRenameLocation(garage.id, garage.name, "garage") },
+ onDeleteClick = { onDeleteLocation(garage.id, garage.name, "garage") }
+ ) {
+ StatChip(icon = Icons.Default.Inventory2, text = "${itemsInGarage.size} items")
+ StatChip(icon = Icons.Default.AttachMoney, text = "$${String.format(Locale.US, "%.2f", totalValue)}")
+ }
+ AnimatedVisibility(visible = isExpanded) {
+ Column(modifier = Modifier.padding(start = 24.dp, end = 8.dp, bottom = 8.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
+ if (garage.cabinets.isEmpty()) {
+ EmptyChildState("No cabinets here. Tap '+ Add' to create one.")
+ } else {
+ garage.cabinets.forEach { cabinet ->
+ val itemsInCabinet = itemsInGarage.filter { it.cabinetId == cabinet.id }
+ CabinetSection(cabinet, itemsInCabinet, { onAddShelf(cabinet.id) }, onAddBox, onRenameLocation, onDeleteLocation)
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun CabinetSection(cabinet: Cabinet, itemsInCabinet: List
- , onAddShelf: () -> Unit, onAddBox: (shelfId: String) -> Unit, onRenameLocation: (id: String, oldName: String, type: String) -> Unit, onDeleteLocation: (id: String, name: String, type: String) -> Unit) {
+ var isExpanded by remember { mutableStateOf(false) }
+ Surface(shape = MaterialTheme.shapes.medium, border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f))) {
+ Column {
+ SectionHeader(
+ title = cabinet.name, icon = Icons.Default.Kitchen, isExpanded = isExpanded,
+ onToggleExpand = { isExpanded = !isExpanded },
+ onAddClick = onAddShelf,
+ onRenameClick = { onRenameLocation(cabinet.id, cabinet.name, "cabinet") },
+ onDeleteClick = { onDeleteLocation(cabinet.id, cabinet.name, "cabinet") }
+ ) {
+ StatChip(icon = Icons.Default.Inventory2, text = "${itemsInCabinet.size} items", small = true)
+ }
+ AnimatedVisibility(visible = isExpanded) {
+ Column(modifier = Modifier.padding(start = 24.dp, end = 8.dp, bottom = 8.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
+ if (cabinet.shelves.isEmpty()) {
+ EmptyChildState("No shelves in this cabinet.")
+ } else {
+ cabinet.shelves.forEach { shelf ->
+ val itemsInShelf = itemsInCabinet.filter { it.shelfId == shelf.id }
+ ShelfSection(shelf, itemsInShelf, { onAddBox(shelf.id) }, onRenameLocation, onDeleteLocation)
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun ShelfSection(shelf: Shelf, itemsInShelf: List
- , onAddBox: () -> Unit, onRenameLocation: (id: String, oldName: String, type: String) -> Unit, onDeleteLocation: (id: String, name: String, type: String) -> Unit) {
+ var isExpanded by remember { mutableStateOf(false) }
+ Surface(shape = MaterialTheme.shapes.medium, border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.2f))) {
+ Column {
+ SectionHeader(
+ title = shelf.name, icon = Icons.AutoMirrored.Filled.ViewList, isExpanded = isExpanded,
+ onToggleExpand = { isExpanded = !isExpanded },
+ onAddClick = onAddBox,
+ onRenameClick = { onRenameLocation(shelf.id, shelf.name, "shelf") },
+ onDeleteClick = { onDeleteLocation(shelf.id, shelf.name, "shelf") }
+ ) {
+ StatChip(icon = Icons.Default.Inventory2, text = "${itemsInShelf.size} items", small = true)
+ }
+ AnimatedVisibility(visible = isExpanded) {
+ Column(modifier = Modifier.padding(start = 24.dp, end = 8.dp, bottom = 8.dp)) {
+ if (shelf.boxes.isEmpty()) {
+ EmptyChildState("No boxes on this shelf.")
+ } else {
+ shelf.boxes.forEach { box ->
+ val itemsInBox = itemsInShelf.filter { it.boxId == box.id }
+ BoxItem(box = box, itemCount = itemsInBox.size)
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+
+@Composable
+private fun SectionHeader(title: String, icon: ImageVector, isExpanded: Boolean, onToggleExpand: () -> Unit, onAddClick: () -> Unit, onRenameClick: () -> Unit, onDeleteClick: () -> Unit, statsContent: @Composable RowScope.() -> Unit) {
+ var showMenu by remember { mutableStateOf(false) }
+ Row(modifier = Modifier.fillMaxWidth().clickable(onClick = onToggleExpand).padding(horizontal = 8.dp, vertical = 4.dp), verticalAlignment = Alignment.CenterVertically) {
+ Icon(icon, null, modifier = Modifier.size(24.dp), tint = MaterialTheme.colorScheme.primary)
+ Spacer(Modifier.width(12.dp))
+ Column(modifier = Modifier.weight(1f)) {
+ Text(text = title, fontWeight = FontWeight.Bold, style = MaterialTheme.typography.titleMedium)
+ Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) { statsContent() }
+ }
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ IconButton(onClick = onAddClick) { Icon(Icons.Default.Add, "+ Add") }
+ Box {
+ IconButton(onClick = { showMenu = true }) { Icon(Icons.Default.MoreVert, "More Options") }
+ DropdownMenu(expanded = showMenu, onDismissRequest = { showMenu = false }) {
+ DropdownMenuItem(text = { Text("Rename") }, onClick = { showMenu = false; onRenameClick() })
+ DropdownMenuItem(text = { Text("Delete", color = MaterialTheme.colorScheme.error) }, onClick = { showMenu = false; onDeleteClick() })
+ }
+ }
+ Icon(imageVector = if (isExpanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore, "Toggle Section")
+ }
+ }
+}
+
+@Composable
+private fun StatChip(icon: ImageVector, text: String, small: Boolean = false) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Icon(icon, null, modifier = Modifier.size(if (small) 12.dp else 14.dp), tint = MaterialTheme.colorScheme.onSurfaceVariant)
+ Spacer(Modifier.width(4.dp))
+ Text(text, style = if (small) MaterialTheme.typography.labelSmall else MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
+ }
+}
+
+@Composable
+private fun BoxItem(box: Box, itemCount: Int) {
+ Row(Modifier.fillMaxWidth().padding(start = 8.dp, top = 4.dp, bottom = 4.dp), verticalAlignment = Alignment.CenterVertically) {
+ Text("└─", color = MaterialTheme.colorScheme.outline); Spacer(Modifier.width(4.dp))
+ Icon(Icons.Default.Inventory, null, modifier = Modifier.size(16.dp), tint = MaterialTheme.colorScheme.onSurfaceVariant)
+ Spacer(Modifier.width(8.dp))
+ Text(box.name, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.weight(1f))
+ Text("$itemCount items", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
+ }
+}
+
+@Composable
+private fun EmptyState(onAddGarage: () -> Unit, modifier: Modifier = Modifier) {
+ Column(modifier = modifier.fillMaxSize().padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) {
+ Icon(Icons.Default.LocationCity, null, modifier = Modifier.size(64.dp), tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f))
+ Text("No Locations Created Yet", style = MaterialTheme.typography.headlineSmall, modifier = Modifier.padding(top = 16.dp))
+ Text("Get started by adding your first garage.", textAlign = TextAlign.Center, modifier = Modifier.padding(top = 8.dp))
+ Spacer(Modifier.height(24.dp))
+ Button(onClick = onAddGarage) { Icon(Icons.Default.Add, "Add Garage"); Spacer(Modifier.width(8.dp)); Text("Add Your First Garage") }
+ }
+}
+
+@Composable
+private fun EmptyChildState(message: String) {
+ Text(message, style = MaterialTheme.typography.bodySmall, modifier = Modifier.padding(16.dp).fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colorScheme.onSurfaceVariant)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/samuel/inventorymanager/screens/MainAppScreen.kt b/app/src/main/java/com/samuel/inventorymanager/screens/MainAppScreen.kt
new file mode 100644
index 0000000..52574d6
--- /dev/null
+++ b/app/src/main/java/com/samuel/inventorymanager/screens/MainAppScreen.kt
@@ -0,0 +1,653 @@
+package com.samuel.inventorymanager.screens
+
+import android.content.Context
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.lazy.grid.items
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.HelpOutline
+import androidx.compose.material.icons.automirrored.filled.List
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.icons.filled.Dashboard
+import androidx.compose.material.icons.filled.History
+import androidx.compose.material.icons.filled.Image
+import androidx.compose.material.icons.filled.Inventory2
+import androidx.compose.material.icons.filled.LocationOn
+import androidx.compose.material.icons.filled.Menu
+import androidx.compose.material.icons.filled.Search
+import androidx.compose.material.icons.filled.Settings
+import androidx.compose.material.icons.filled.Sync
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.DrawerValue
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.ModalDrawerSheet
+import androidx.compose.material3.ModalNavigationDrawer
+import androidx.compose.material3.NavigationBar
+import androidx.compose.material3.NavigationBarItem
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.material3.rememberDrawerState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.compositionLocalOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+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.compose.viewModel
+import com.google.gson.Gson
+import com.samuel.inventorymanager.data.AppSettings
+import com.samuel.inventorymanager.ui.theme.AppThemeType
+import com.samuel.inventorymanager.ui.theme.InventoryManagerTheme
+import kotlinx.coroutines.launch
+import java.io.File
+import java.io.InputStreamReader
+
+// ========================================================================================
+// SCREEN NAVIGATION DEFINITIONS
+// ========================================================================================
+
+enum class Screen {
+ Dashboard, Items, Locations, Search,
+ Overview, Images, History, Settings, Sync, Help,
+ CreateItem
+}
+
+data class NavGridItem(
+ val title: String,
+ val icon: ImageVector,
+ val screen: Screen
+)
+
+val navigationItems = listOf(
+ NavGridItem("Dashboard", Icons.Default.Dashboard, Screen.Dashboard),
+ NavGridItem("Items", Icons.Default.Inventory2, Screen.CreateItem),
+ NavGridItem("Locations", Icons.Default.LocationOn, Screen.Locations),
+ NavGridItem("Search", Icons.Default.Search, Screen.Search),
+ NavGridItem("Overview", Icons.AutoMirrored.Filled.List, Screen.Overview),
+ NavGridItem("Images", Icons.Default.Image, Screen.Images),
+ NavGridItem("History", Icons.Default.History, Screen.History),
+ NavGridItem("Settings", Icons.Default.Settings, Screen.Settings),
+ NavGridItem("Sync", Icons.Default.Sync, Screen.Sync),
+ NavGridItem("Help", Icons.AutoMirrored.Filled.HelpOutline, Screen.Help)
+)
+
+sealed class DialogState {
+ object Closed : DialogState()
+ object AddGarage : DialogState()
+ data class AddCabinet(val garageId: String) : DialogState()
+ data class AddShelf(val cabinetId: String) : DialogState()
+ data class AddBox(val shelfId: String) : DialogState()
+ data class RenameLocation(val id: String, val oldName: String, val type: String) : DialogState()
+ data class DeleteLocation(val id: String, val name: String, val type: String) : DialogState()
+ object ClearHistory : DialogState()
+}
+
+val LocalAppSettings = compositionLocalOf { AppSettings() }
+val LocalOnSettingsChange = compositionLocalOf<(AppSettings) -> Unit> { {} }
+
+// ========================================================================================
+// MAIN APPLICATION SCREEN
+// ========================================================================================
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun MainAppScreen() {
+ val context = LocalContext.current
+ var appSettings by remember { mutableStateOf(AppSettings()) }
+
+ // Load settings on first launch
+ LaunchedEffect(Unit) {
+ appSettings = loadSettingsFromFile(context)
+ }
+
+ val onSettingsChange: (AppSettings) -> Unit = { newSettings ->
+ appSettings = newSettings
+ saveSettingsToFile(context, newSettings)
+ }
+
+ // Determine theme type based on appSettings
+ val themeType = when {
+ appSettings.customTheme != null -> AppThemeType.LIGHT // Default to light for custom
+ else -> AppThemeType.LIGHT
+ }
+
+ val fontScale = appSettings.customTheme?.fontSizeScale ?: 1.0f
+
+ InventoryManagerTheme(
+ themeType = themeType,
+ fontScale = fontScale
+ ) {
+ var currentScreen by remember { mutableStateOf(Screen.Dashboard) }
+ val garages = remember { mutableStateListOf() }
+ val items = remember { mutableStateListOf
- () }
+ val history = remember { mutableStateListOf() }
+ var dialogState by remember { mutableStateOf(DialogState.Closed) }
+
+ val createItemViewModel: CreateItemViewModel = viewModel()
+
+ LaunchedEffect(Unit) {
+ loadDataFromFile(context, garages, items, history)
+ }
+
+ val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
+ val scope = rememberCoroutineScope()
+
+ val onSaveItem: (Item) -> Unit = { newItem ->
+ val index = items.indexOfFirst { it.id == newItem.id }
+ val newHistoryEntry: HistoryEntry
+ if (index != -1) {
+ newHistoryEntry = HistoryEntry(
+ id = "hist_${System.currentTimeMillis()}",
+ itemId = newItem.id,
+ itemName = newItem.name,
+ action = HistoryAction.Updated,
+ description = "Item details were modified."
+ )
+ items[index] = newItem
+ } else {
+ newHistoryEntry = HistoryEntry(
+ id = "hist_${System.currentTimeMillis()}",
+ itemId = newItem.id,
+ itemName = newItem.name,
+ action = HistoryAction.Added,
+ description = "A new item was created."
+ )
+ items.add(newItem)
+ }
+ history.add(0, newHistoryEntry)
+ saveDataToFile(context, AppData(garages.toList(), items.toList(), history.toList()))
+ }
+
+ val onClearHistory = { dialogState = DialogState.ClearHistory }
+
+ HandleDialogs(
+ dialogState = dialogState,
+ garages = garages,
+ onClearHistory = {
+ history.clear()
+ dialogState = DialogState.Closed
+ saveDataToFile(context, AppData(garages.toList(), items.toList(), history.toList()))
+ },
+ onDismiss = {
+ dialogState = DialogState.Closed
+ saveDataToFile(context, AppData(garages.toList(), items.toList(), history.toList()))
+ }
+ )
+
+ ModalNavigationDrawer(
+ drawerState = drawerState,
+ drawerContent = {
+ ModalDrawerSheet {
+ Column(modifier = Modifier.padding(16.dp)) {
+ Text(
+ "Inventory Manager",
+ style = MaterialTheme.typography.headlineSmall,
+ fontWeight = FontWeight.Bold,
+ modifier = Modifier.padding(vertical = 16.dp)
+ )
+ LazyVerticalGrid(
+ columns = GridCells.Fixed(3),
+ modifier = Modifier.padding(top = 8.dp),
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ items(navigationItems) { item ->
+ GridNavigationButton(item, currentScreen == item.screen) {
+ currentScreen = item.screen
+ scope.launch { drawerState.close() }
+ }
+ }
+ }
+ }
+ }
+ }
+ ) {
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ title = {
+ Text(
+ currentScreen.name,
+ style = MaterialTheme.typography.titleLarge,
+ fontWeight = FontWeight.Bold
+ )
+ },
+ navigationIcon = {
+ IconButton({ scope.launch { drawerState.open() } }) {
+ Icon(Icons.Default.Menu, "Menu")
+ }
+ },
+ colors = TopAppBarDefaults.topAppBarColors(
+ containerColor = MaterialTheme.colorScheme.primaryContainer,
+ titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer
+ )
+ )
+ },
+ bottomBar = {
+ NavigationBar {
+ NavigationBarItem(
+ icon = { Icon(Icons.Default.LocationOn, null) },
+ label = { Text("Locations") },
+ selected = currentScreen == Screen.Locations,
+ onClick = { currentScreen = Screen.Locations }
+ )
+ NavigationBarItem(
+ icon = { Icon(Icons.Default.Add, null) },
+ label = { Text("Add Item") },
+ selected = currentScreen == Screen.CreateItem,
+ onClick = {
+ createItemViewModel.clearFormForNewItem(garages)
+ currentScreen = Screen.CreateItem
+ }
+ )
+ NavigationBarItem(
+ icon = { Icon(Icons.AutoMirrored.Filled.List, null) },
+ label = { Text("Overview") },
+ selected = currentScreen == Screen.Overview,
+ onClick = { currentScreen = Screen.Overview }
+ )
+ }
+ }
+ ) { paddingValues ->
+ Box(modifier = Modifier.padding(paddingValues).fillMaxSize()) {
+ when (currentScreen) {
+ Screen.Dashboard -> DashboardScreen(items, garages)
+ Screen.Locations -> LocationsScreen(
+ garages = garages,
+ items = items,
+ onAddGarage = { dialogState = DialogState.AddGarage },
+ onAddCabinet = { gid -> dialogState = DialogState.AddCabinet(gid) },
+ onAddShelf = { cid -> dialogState = DialogState.AddShelf(cid) },
+ onAddBox = { sid -> dialogState = DialogState.AddBox(sid) },
+ onRenameLocation = { id, old, type ->
+ dialogState = DialogState.RenameLocation(id, old, type)
+ },
+ onDeleteLocation = { id, name, type ->
+ dialogState = DialogState.DeleteLocation(id, name, type)
+ }
+ )
+ Screen.Items, Screen.CreateItem -> CreateItemScreen(
+ garages = garages,
+ onSaveItem = onSaveItem,
+ viewModel = createItemViewModel,
+ appSettings = appSettings,
+ onSettingsChange = onSettingsChange
+ )
+ Screen.Overview -> OverviewScreen(items, garages)
+ Screen.Search -> SearchScreen(items, garages)
+ Screen.Settings -> SettingsScreen(
+ currentSettings = appSettings,
+ onSettingsChange = onSettingsChange
+ )
+ Screen.Help -> HelpScreen()
+ Screen.Images -> ImagesScreen(items) { item ->
+ createItemViewModel.loadItemForEditing(item, garages)
+ currentScreen = Screen.CreateItem
+ }
+ Screen.History -> HistoryScreen(
+ history = history,
+ items = items,
+ onItemClick = { targetItem ->
+ createItemViewModel.loadItemForEditing(targetItem, garages)
+ currentScreen = Screen.CreateItem
+ },
+ onClearHistory = onClearHistory
+ )
+ else -> PlaceholderScreen(screenName = currentScreen.name)
+ }
+ }
+ }
+ }
+ }
+}
+
+// ========================================================================================
+// HELPER COMPOSABLES
+// ========================================================================================
+
+@Composable
+private fun GridNavigationButton(item: NavGridItem, isSelected: Boolean, onClick: () -> Unit) {
+ Card(
+ modifier = Modifier
+ .aspectRatio(1f)
+ .clickable(onClick = onClick),
+ shape = MaterialTheme.shapes.medium,
+ colors = CardDefaults.cardColors(
+ containerColor = if (isSelected)
+ MaterialTheme.colorScheme.primary
+ else
+ MaterialTheme.colorScheme.surfaceVariant
+ ),
+ elevation = CardDefaults.cardElevation(if (isSelected) 4.dp else 1.dp)
+ ) {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center
+ ) {
+ Icon(
+ imageVector = item.icon,
+ contentDescription = item.title,
+ tint = if (isSelected)
+ MaterialTheme.colorScheme.onPrimary
+ else
+ MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ Spacer(Modifier.height(4.dp))
+ Text(
+ text = item.title,
+ fontSize = 12.sp,
+ fontWeight = FontWeight.SemiBold,
+ color = if (isSelected)
+ MaterialTheme.colorScheme.onPrimary
+ else
+ MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+}
+
+@Composable
+fun PlaceholderScreen(screenName: String) {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
+ Text(
+ "$screenName Screen",
+ style = MaterialTheme.typography.headlineMedium,
+ fontWeight = FontWeight.Bold
+ )
+ Text(
+ "Coming Soon",
+ style = MaterialTheme.typography.bodyLarge,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun HandleDialogs(
+ dialogState: DialogState,
+ garages: MutableList,
+ onClearHistory: () -> Unit,
+ onDismiss: () -> Unit
+) {
+ when (val state = dialogState) {
+ is DialogState.Closed -> {}
+ DialogState.AddGarage -> AddLocationDialog("Create New Garage", onDismiss) { name ->
+ garages.add(Garage("g_${System.currentTimeMillis()}", name, emptyList()))
+ }
+ is DialogState.AddCabinet -> AddLocationDialog("Add Cabinet", onDismiss) { name ->
+ val i = garages.indexOfFirst { it.id == state.garageId }
+ if (i != -1) {
+ val g = garages[i]
+ garages[i] = g.copy(
+ cabinets = g.cabinets + Cabinet("c_${System.currentTimeMillis()}", name, emptyList())
+ )
+ }
+ }
+ is DialogState.AddShelf -> AddLocationDialog("Add Shelf", onDismiss) { name ->
+ for ((gi, g) in garages.withIndex()) {
+ val ci = g.cabinets.indexOfFirst { it.id == state.cabinetId }
+ if (ci != -1) {
+ val c = g.cabinets[ci]
+ val newShelf = Shelf("s_${System.currentTimeMillis()}", name, emptyList())
+ garages[gi] = g.copy(
+ cabinets = g.cabinets.toMutableList().also { cabinetList ->
+ cabinetList[ci] = c.copy(shelves = c.shelves + newShelf)
+ }
+ )
+ break
+ }
+ }
+ }
+ is DialogState.AddBox -> AddLocationDialog("Add Box/Bin", onDismiss) { name ->
+ run loop@{
+ for ((gi, g) in garages.withIndex()) {
+ for ((ci, c) in g.cabinets.withIndex()) {
+ val si = c.shelves.indexOfFirst { it.id == state.shelfId }
+ if (si != -1) {
+ val s = c.shelves[si]
+ val newBox = Box("b_${System.currentTimeMillis()}", name)
+ garages[gi] = g.copy(
+ cabinets = g.cabinets.toMutableList().also { cabinetList ->
+ cabinetList[ci] = c.copy(
+ shelves = c.shelves.toMutableList().also { shelfList ->
+ shelfList[si] = s.copy(boxes = s.boxes + newBox)
+ }
+ )
+ }
+ )
+ return@loop
+ }
+ }
+ }
+ }
+ }
+ is DialogState.RenameLocation -> {
+ RenameDialog(state, onDismiss) { /* Rename logic */ }
+ }
+ is DialogState.DeleteLocation -> {
+ DeleteConfirmDialog(state, onDismiss) { /* Delete logic */ }
+ }
+ is DialogState.ClearHistory -> {
+ AlertDialog(
+ onDismissRequest = onDismiss,
+ title = { Text("Clear History?") },
+ text = { Text("Are you sure you want to clear all history? This cannot be undone.") },
+ confirmButton = {
+ Button(
+ onClick = onClearHistory,
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.error
+ )
+ ) {
+ Text("Clear All")
+ }
+ },
+ dismissButton = {
+ TextButton(onClick = onDismiss) {
+ Text("Cancel")
+ }
+ }
+ )
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun AddLocationDialog(title: String, onDismiss: () -> Unit, onConfirm: (String) -> Unit) {
+ var text by remember { mutableStateOf("") }
+ AlertDialog(
+ onDismissRequest = onDismiss,
+ title = { Text(title) },
+ text = {
+ OutlinedTextField(
+ value = text,
+ onValueChange = { newText -> text = newText },
+ label = { Text("Location Name") }
+ )
+ },
+ confirmButton = {
+ Button(
+ onClick = {
+ if (text.isNotBlank()) onConfirm(text)
+ onDismiss()
+ }
+ ) {
+ Text("Add")
+ }
+ },
+ dismissButton = {
+ TextButton(onClick = onDismiss) {
+ Text("Cancel")
+ }
+ }
+ )
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun RenameDialog(
+ state: DialogState.RenameLocation,
+ onDismiss: () -> Unit,
+ onConfirm: (String) -> Unit
+) {
+ var text by remember { mutableStateOf(state.oldName) }
+ AlertDialog(
+ onDismissRequest = onDismiss,
+ title = { Text("Rename ${state.type.replaceFirstChar { it.uppercase() }}") },
+ text = {
+ OutlinedTextField(
+ value = text,
+ onValueChange = { newText -> text = newText },
+ label = { Text("New Name") }
+ )
+ },
+ confirmButton = {
+ Button(
+ onClick = {
+ if (text.isNotBlank()) onConfirm(text)
+ onDismiss()
+ }
+ ) {
+ Text("Rename")
+ }
+ },
+ dismissButton = {
+ TextButton(onDismiss) {
+ Text("Cancel")
+ }
+ }
+ )
+}
+
+@Composable
+private fun DeleteConfirmDialog(
+ state: DialogState.DeleteLocation,
+ onDismiss: () -> Unit,
+ onConfirm: () -> Unit
+) {
+ AlertDialog(
+ onDismissRequest = onDismiss,
+ title = { Text("Delete ${state.type.replaceFirstChar { it.uppercase() }}?") },
+ text = {
+ Text("Are you sure you want to delete '${state.name}'? This cannot be undone.")
+ },
+ confirmButton = {
+ Button(
+ onClick = {
+ onConfirm()
+ onDismiss()
+ },
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.error
+ )
+ ) {
+ Text("Delete")
+ }
+ },
+ dismissButton = {
+ TextButton(onDismiss) {
+ Text("Cancel")
+ }
+ }
+ )
+}
+
+// ========================================================================================
+// DATA PERSISTENCE FUNCTIONS
+// ========================================================================================
+
+private fun saveDataToFile(context: Context, appData: AppData) {
+ try {
+ context.openFileOutput("inventory.json", Context.MODE_PRIVATE).use {
+ it.write(Gson().toJson(appData).toByteArray())
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+}
+
+private fun loadDataFromFile(
+ context: Context,
+ garages: MutableList,
+ items: MutableList
- ,
+ history: MutableList
+) {
+ val file = File(context.filesDir, "inventory.json")
+ if (!file.exists()) return
+ try {
+ context.openFileInput("inventory.json").use { stream ->
+ InputStreamReader(stream).use { reader ->
+ val data = Gson().fromJson(reader, AppData::class.java)
+ garages.clear()
+ items.clear()
+ history.clear()
+ garages.addAll(data.garages)
+ items.addAll(data.items)
+ history.addAll(data.history)
+ }
+ }
+ } catch (_: Exception) {
+ // Failed to load data, starting fresh
+ }
+}
+
+private fun saveSettingsToFile(context: Context, settings: AppSettings) {
+ try {
+ context.openFileOutput("app_settings.json", Context.MODE_PRIVATE).use {
+ it.write(Gson().toJson(settings).toByteArray())
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+}
+
+private fun loadSettingsFromFile(context: Context): AppSettings {
+ val file = File(context.filesDir, "app_settings.json")
+ return if (file.exists()) {
+ try {
+ Gson().fromJson(file.readText(), AppSettings::class.java)
+ } catch (e: Exception) {
+ AppSettings()
+ }
+ } else {
+ AppSettings()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/samuel/inventorymanager/screens/OverviewScreen.kt b/app/src/main/java/com/samuel/inventorymanager/screens/OverviewScreen.kt
new file mode 100644
index 0000000..2037b9c
--- /dev/null
+++ b/app/src/main/java/com/samuel/inventorymanager/screens/OverviewScreen.kt
@@ -0,0 +1,795 @@
+package com.samuel.inventorymanager.screens
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.expandVertically
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.shrinkVertically
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ArrowDropDown
+import androidx.compose.material.icons.filled.AttachMoney
+import androidx.compose.material.icons.filled.Category
+import androidx.compose.material.icons.filled.CheckCircle
+import androidx.compose.material.icons.filled.Clear
+import androidx.compose.material.icons.filled.ExpandLess
+import androidx.compose.material.icons.filled.ExpandMore
+import androidx.compose.material.icons.filled.FileDownload
+import androidx.compose.material.icons.filled.Inventory
+import androidx.compose.material.icons.filled.List
+import androidx.compose.material.icons.filled.LocationCity
+import androidx.compose.material.icons.filled.LocationOn
+import androidx.compose.material.icons.filled.Numbers
+import androidx.compose.material.icons.filled.Refresh
+import androidx.compose.material.icons.filled.Scale
+import androidx.compose.material.icons.filled.Search
+import androidx.compose.material.icons.filled.Straighten
+import androidx.compose.material.icons.filled.TrendingUp
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.Divider
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedButton
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Tab
+import androidx.compose.material3.TabRow
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+
+// ======================================================================
+// MAIN OVERVIEW SCREEN - REDESIGNED
+// ======================================================================
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun OverviewScreen(
+ items: List
- ,
+ garages: List
+) {
+ var searchQuery by remember { mutableStateOf("") }
+ var selectedLocation by remember { mutableStateOf("All Locations") }
+ var selectedSize by remember { mutableStateOf("All Sizes") }
+ var selectedCondition by remember { mutableStateOf("All Conditions") }
+ var showLocationMenu by remember { mutableStateOf(false) }
+ var showSizeMenu by remember { mutableStateOf(false) }
+ var showConditionMenu by remember { mutableStateOf(false) }
+ var selectedTab by remember { mutableStateOf(0) }
+ var expandedStats by remember { mutableStateOf(true) }
+ var expandedAnalytics by remember { mutableStateOf(true) }
+
+ val filteredItems = remember(searchQuery, selectedLocation, selectedSize, selectedCondition, items) {
+ items.filter { item ->
+ val matchesSearch = searchQuery.isBlank() ||
+ item.name.contains(searchQuery, ignoreCase = true) ||
+ item.modelNumber?.contains(searchQuery, ignoreCase = true) == true
+ val matchesLocation = selectedLocation == "All Locations" ||
+ (garages.find { it.id == item.garageId }?.name == selectedLocation)
+ val matchesSize = selectedSize == "All Sizes" ||
+ item.sizeCategory.equals(selectedSize, ignoreCase = true)
+ val matchesCondition = selectedCondition == "All Conditions" ||
+ item.condition.equals(selectedCondition, ignoreCase = true)
+ matchesSearch && matchesLocation && matchesSize && matchesCondition
+ }
+ }
+
+ val totalItemsCount = filteredItems.size
+ val totalQuantity = filteredItems.sumOf { it.quantity }
+ val totalValue = filteredItems.sumOf { ((it.minPrice ?: 0.0) + (it.maxPrice ?: 0.0)) / 2 * it.quantity }
+ val totalWeight = filteredItems.sumOf { (it.weight ?: 0.0) * it.quantity }
+ val locationsUsed = filteredItems.map { it.garageId }.distinct().count()
+ val avgItemValue = if (totalItemsCount > 0) totalValue / totalItemsCount else 0.0
+
+ val conditions = items.map { it.condition }.distinct().sorted()
+
+ LazyColumn(
+ modifier = Modifier.fillMaxSize(),
+ contentPadding = PaddingValues(16.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ // Header
+ item {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.padding(vertical = 4.dp)
+ ) {
+ Icon(
+ Icons.Default.List,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.primary,
+ modifier = Modifier.size(32.dp)
+ )
+ Spacer(Modifier.width(12.dp))
+ Text(
+ "Inventory Overview",
+ style = MaterialTheme.typography.headlineMedium,
+ fontWeight = FontWeight.Bold
+ )
+ Spacer(Modifier.weight(1f))
+ IconButton(onClick = { /* TODO: Export */ }) {
+ Icon(Icons.Default.FileDownload, "Export", tint = MaterialTheme.colorScheme.primary)
+ }
+ IconButton(onClick = { /* TODO: Refresh */ }) {
+ Icon(Icons.Default.Refresh, "Refresh", tint = MaterialTheme.colorScheme.primary)
+ }
+ }
+ }
+
+ // Search Bar
+ item {
+ OutlinedTextField(
+ value = searchQuery,
+ onValueChange = { searchQuery = it },
+ modifier = Modifier.fillMaxWidth(),
+ placeholder = { Text("Search by name or model number...") },
+ leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
+ trailingIcon = {
+ if (searchQuery.isNotEmpty()) {
+ IconButton(onClick = { searchQuery = "" }) {
+ Icon(Icons.Default.Clear, "Clear")
+ }
+ }
+ },
+ shape = RoundedCornerShape(16.dp),
+ singleLine = true
+ )
+ }
+
+ // Filter Row
+ item {
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(12.dp),
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ FilterDropdown(
+ label = selectedLocation,
+ icon = Icons.Default.LocationOn,
+ expanded = showLocationMenu,
+ onExpandChange = { showLocationMenu = it },
+ options = listOf("All Locations") + garages.map { it.name },
+ onSelect = { selectedLocation = it; showLocationMenu = false },
+ modifier = Modifier.weight(1f)
+ )
+
+ FilterDropdown(
+ label = selectedSize,
+ icon = Icons.Default.Straighten,
+ expanded = showSizeMenu,
+ onExpandChange = { showSizeMenu = it },
+ options = listOf("All Sizes", "Small", "Medium", "Large"),
+ onSelect = { selectedSize = it; showSizeMenu = false },
+ modifier = Modifier.weight(1f)
+ )
+
+ FilterDropdown(
+ label = selectedCondition,
+ icon = Icons.Default.CheckCircle,
+ expanded = showConditionMenu,
+ onExpandChange = { showConditionMenu = it },
+ options = listOf("All Conditions") + conditions,
+ onSelect = { selectedCondition = it; showConditionMenu = false },
+ modifier = Modifier.weight(1f)
+ )
+ }
+ }
+
+ // Statistics Section
+ item {
+ ExpandableSection(
+ title = "Key Statistics",
+ subtitle = "${filteredItems.size} items",
+ icon = Icons.Default.TrendingUp,
+ isExpanded = expandedStats,
+ onToggle = { expandedStats = !expandedStats }
+ ) {
+ Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
+ Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
+ ModernStatCard(
+ title = "Unique Items",
+ value = totalItemsCount.toString(),
+ icon = Icons.Default.Inventory,
+ color = Color(0xFF6200EE),
+ modifier = Modifier.weight(1f)
+ )
+ ModernStatCard(
+ title = "Total Quantity",
+ value = totalQuantity.toString(),
+ icon = Icons.Default.Numbers,
+ color = Color(0xFF03DAC5),
+ modifier = Modifier.weight(1f)
+ )
+ }
+ Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
+ ModernStatCard(
+ title = "Total Value",
+ value = "$${String.format("%.0f", totalValue)}",
+ icon = Icons.Default.AttachMoney,
+ color = Color(0xFF047857),
+ modifier = Modifier.weight(1f)
+ )
+ ModernStatCard(
+ title = "Avg Value",
+ value = "$${String.format("%.0f", avgItemValue)}",
+ icon = Icons.Default.TrendingUp,
+ color = Color(0xFFB45309),
+ modifier = Modifier.weight(1f)
+ )
+ }
+ Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
+ ModernStatCard(
+ title = "Total Weight",
+ value = "${String.format("%.1f", totalWeight)} lbs",
+ icon = Icons.Default.Scale,
+ color = Color(0xFF0369A1),
+ modifier = Modifier.weight(1f)
+ )
+ ModernStatCard(
+ title = "Locations Used",
+ value = "$locationsUsed/${garages.size}",
+ icon = Icons.Default.LocationCity,
+ color = Color(0xFFBE185D),
+ modifier = Modifier.weight(1f)
+ )
+ }
+ }
+ }
+ }
+
+ // Advanced Analytics Section
+ item {
+ ExpandableSection(
+ title = "Advanced Analytics",
+ subtitle = "Visual breakdown",
+ icon = Icons.Default.Category,
+ isExpanded = expandedAnalytics,
+ onToggle = { expandedAnalytics = !expandedAnalytics }
+ ) {
+ Column {
+ TabRow(
+ selectedTabIndex = selectedTab,
+ containerColor = Color.Transparent
+ ) {
+ Tab(
+ selected = selectedTab == 0,
+ onClick = { selectedTab = 0 },
+ text = { Text("Category", fontSize = 13.sp) }
+ )
+ Tab(
+ selected = selectedTab == 1,
+ onClick = { selectedTab = 1 },
+ text = { Text("Condition", fontSize = 13.sp) }
+ )
+ Tab(
+ selected = selectedTab == 2,
+ onClick = { selectedTab = 2 },
+ text = { Text("Location", fontSize = 13.sp) }
+ )
+ Tab(
+ selected = selectedTab == 3,
+ onClick = { selectedTab = 3 },
+ text = { Text("Size", fontSize = 13.sp) }
+ )
+ }
+
+ Spacer(Modifier.height(20.dp))
+
+ when (selectedTab) {
+ 0 -> AdvancedAnalyticsView(
+ groupedItems = filteredItems.groupBy { it.sizeCategory },
+ label = "Category"
+ )
+ 1 -> AdvancedAnalyticsView(
+ groupedItems = filteredItems.groupBy { it.condition },
+ label = "Condition"
+ )
+ 2 -> {
+ val locationData = garages.associate { garage ->
+ garage.name to filteredItems.filter { it.garageId == garage.id }
+ }.filter { it.value.isNotEmpty() }
+ AdvancedAnalyticsView(locationData, "Location")
+ }
+ 3 -> AdvancedAnalyticsView(
+ groupedItems = filteredItems.groupBy { it.sizeCategory },
+ label = "Size"
+ )
+ }
+ }
+ }
+ }
+
+ // Item List
+ item {
+ Text(
+ "Item List (${filteredItems.size})",
+ style = MaterialTheme.typography.titleLarge,
+ fontWeight = FontWeight.Bold,
+ modifier = Modifier.padding(top = 8.dp)
+ )
+ }
+
+ items(filteredItems) { item ->
+ ModernItemCard(item = item, garages = garages)
+ }
+ }
+}
+
+// ======================================================================
+// MODERN COMPONENTS
+// ======================================================================
+
+@Composable
+fun FilterDropdown(
+ label: String,
+ icon: ImageVector,
+ expanded: Boolean,
+ onExpandChange: (Boolean) -> Unit,
+ options: List,
+ onSelect: (String) -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Box(modifier = modifier) {
+ OutlinedButton(
+ onClick = { onExpandChange(true) },
+ modifier = Modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(12.dp)
+ ) {
+ Icon(icon, null, modifier = Modifier.size(18.dp))
+ Spacer(Modifier.width(6.dp))
+ Text(
+ label,
+ modifier = Modifier.weight(1f),
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ fontSize = 13.sp
+ )
+ Icon(Icons.Default.ArrowDropDown, null, modifier = Modifier.size(20.dp))
+ }
+ DropdownMenu(
+ expanded = expanded,
+ onDismissRequest = { onExpandChange(false) }
+ ) {
+ options.forEach { option ->
+ DropdownMenuItem(
+ text = { Text(option) },
+ onClick = { onSelect(option) }
+ )
+ }
+ }
+ }
+}
+
+@Composable
+fun ModernStatCard(
+ title: String,
+ value: String,
+ icon: ImageVector,
+ color: Color,
+ modifier: Modifier = Modifier
+) {
+ Card(
+ modifier = modifier.height(100.dp),
+ shape = RoundedCornerShape(16.dp),
+ colors = CardDefaults.cardColors(containerColor = color.copy(alpha = 0.1f)),
+ elevation = CardDefaults.cardElevation(2.dp)
+ ) {
+ Column(
+ modifier = Modifier
+ .padding(16.dp)
+ .fillMaxSize(),
+ verticalArrangement = Arrangement.SpaceBetween
+ ) {
+ Icon(
+ icon,
+ contentDescription = null,
+ tint = color,
+ modifier = Modifier.size(28.dp)
+ )
+ Column {
+ Text(
+ value,
+ style = MaterialTheme.typography.headlineSmall,
+ fontWeight = FontWeight.Bold,
+ color = color
+ )
+ Text(
+ title,
+ style = MaterialTheme.typography.bodySmall,
+ color = color.copy(alpha = 0.8f)
+ )
+ }
+ }
+ }
+}
+
+@Composable
+fun ExpandableSection(
+ title: String,
+ subtitle: String,
+ icon: ImageVector,
+ isExpanded: Boolean,
+ onToggle: () -> Unit,
+ content: @Composable () -> Unit
+) {
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable(onClick = onToggle),
+ elevation = CardDefaults.cardElevation(4.dp),
+ shape = RoundedCornerShape(16.dp)
+ ) {
+ Column(modifier = Modifier.padding(20.dp)) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Icon(
+ icon,
+ null,
+ tint = MaterialTheme.colorScheme.primary,
+ modifier = Modifier.size(28.dp)
+ )
+ Spacer(Modifier.width(12.dp))
+ Column {
+ Text(
+ title,
+ style = MaterialTheme.typography.titleLarge,
+ fontWeight = FontWeight.Bold
+ )
+ Text(
+ subtitle,
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+ Icon(
+ if (isExpanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore,
+ "Toggle",
+ modifier = Modifier.size(28.dp)
+ )
+ }
+
+ AnimatedVisibility(
+ visible = isExpanded,
+ enter = expandVertically() + fadeIn(),
+ exit = shrinkVertically() + fadeOut()
+ ) {
+ Column {
+ Spacer(Modifier.height(20.dp))
+ content()
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun AdvancedAnalyticsView(
+ groupedItems: Map>,
+ label: String
+) {
+ val sortedData = groupedItems.map { (key, items) ->
+ val totalValue = items.sumOf { ((it.minPrice ?: 0.0) + (it.maxPrice ?: 0.0)) / 2 * it.quantity }
+ val totalQty = items.sumOf { it.quantity }
+ AnalyticsData(key, items.size, totalQty, totalValue)
+ }.sortedByDescending { it.totalValue }
+
+ val maxValue = sortedData.maxOfOrNull { it.totalValue } ?: 1.0
+ val total = sortedData.sumOf { it.itemCount }
+
+ val colors = listOf(
+ Color(0xFF6200EE),
+ Color(0xFF03DAC5),
+ Color(0xFFFF6B6B),
+ Color(0xFF4ECDC4),
+ Color(0xFFFFBE0B),
+ Color(0xFFFFA500)
+ )
+
+ Column(verticalArrangement = Arrangement.spacedBy(20.dp)) {
+ // Pie Chart with Legend
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceEvenly,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Canvas(
+ modifier = Modifier.size(180.dp)
+ ) {
+ val canvasSize = size.minDimension
+ val radius = canvasSize / 2
+ val center = Offset(size.width / 2, size.height / 2)
+
+ var startAngle = -90f
+ sortedData.forEachIndexed { index, data ->
+ val sweepAngle = (data.itemCount.toFloat() / total) * 360f
+ drawArc(
+ color = colors[index % colors.size],
+ startAngle = startAngle,
+ sweepAngle = sweepAngle,
+ useCenter = true,
+ topLeft = Offset(center.x - radius, center.y - radius),
+ size = Size(radius * 2, radius * 2)
+ )
+ startAngle += sweepAngle
+ }
+
+ drawCircle(
+ color = Color.White,
+ radius = radius * 0.5f,
+ center = center
+ )
+ }
+
+ Column(
+ modifier = Modifier.weight(1f),
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ sortedData.take(6).forEachIndexed { index, data ->
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Box(
+ modifier = Modifier
+ .size(12.dp)
+ .background(colors[index % colors.size], CircleShape)
+ )
+ Spacer(Modifier.width(8.dp))
+ Text(
+ "${data.name} (${data.itemCount})",
+ style = MaterialTheme.typography.bodySmall,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+ }
+ }
+ }
+
+ Divider()
+
+ // Data Table
+ Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text(
+ label,
+ fontWeight = FontWeight.Bold,
+ modifier = Modifier.weight(0.3f),
+ fontSize = 13.sp
+ )
+ Text(
+ "Items",
+ fontWeight = FontWeight.Bold,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.weight(0.2f),
+ fontSize = 13.sp
+ )
+ Text(
+ "Qty",
+ fontWeight = FontWeight.Bold,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.weight(0.2f),
+ fontSize = 13.sp
+ )
+ Text(
+ "Value",
+ fontWeight = FontWeight.Bold,
+ textAlign = TextAlign.End,
+ modifier = Modifier.weight(0.3f),
+ fontSize = 13.sp
+ )
+ }
+
+ sortedData.forEachIndexed { index, data ->
+ Column {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 8.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Row(
+ modifier = Modifier.weight(0.3f),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Box(
+ modifier = Modifier
+ .size(8.dp)
+ .background(colors[index % colors.size], CircleShape)
+ )
+ Spacer(Modifier.width(6.dp))
+ Text(
+ data.name,
+ fontSize = 13.sp,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+ Text(
+ "${data.itemCount}",
+ textAlign = TextAlign.Center,
+ modifier = Modifier.weight(0.2f),
+ fontSize = 13.sp
+ )
+ Text(
+ "${data.totalQuantity}",
+ textAlign = TextAlign.Center,
+ modifier = Modifier.weight(0.2f),
+ fontSize = 13.sp
+ )
+ Text(
+ "$${String.format("%.0f", data.totalValue)}",
+ textAlign = TextAlign.End,
+ modifier = Modifier.weight(0.3f),
+ fontSize = 13.sp,
+ fontWeight = FontWeight.Bold,
+ color = MaterialTheme.colorScheme.primary
+ )
+ }
+
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(8.dp)
+ .clip(RoundedCornerShape(4.dp))
+ .background(MaterialTheme.colorScheme.surfaceVariant)
+ ) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth(fraction = (data.totalValue / maxValue).toFloat())
+ .height(8.dp)
+ .clip(RoundedCornerShape(4.dp))
+ .background(colors[index % colors.size])
+ )
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun ModernItemCard(
+ item: Item,
+ garages: List
+) {
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ elevation = CardDefaults.cardElevation(2.dp),
+ shape = RoundedCornerShape(12.dp)
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ item.name,
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.Bold,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ Spacer(Modifier.height(4.dp))
+ Text(
+ getReadableLocation(item, garages),
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+
+ Spacer(Modifier.width(16.dp))
+
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(12.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Column(horizontalAlignment = Alignment.End) {
+ Text(
+ "Qty: ${item.quantity}",
+ style = MaterialTheme.typography.bodyMedium,
+ fontWeight = FontWeight.SemiBold
+ )
+ val avgPrice = ((item.minPrice ?: 0.0) + (item.maxPrice ?: 0.0)) / 2
+ if (avgPrice > 0) {
+ Text(
+ "$${String.format("%.0f", avgPrice)}",
+ style = MaterialTheme.typography.bodySmall,
+ color = Color(0xFF047857),
+ fontWeight = FontWeight.Bold
+ )
+ }
+ }
+
+ ConditionIndicator(item.condition)
+ }
+ }
+ }
+}
+
+@Composable
+fun ConditionIndicator(condition: String) {
+ val color = when (condition.lowercase()) {
+ "new" -> Color(0xFF047857)
+ "like new" -> Color(0xFF1E40AF)
+ "good" -> Color(0xFFB45309)
+ "fair" -> Color(0xFFC2410C)
+ "poor" -> Color(0xFF9F1239)
+ else -> Color(0xFF4B5563)
+ }
+
+ Box(
+ modifier = Modifier
+ .size(12.dp)
+ .background(color, CircleShape)
+ )
+}
+
+fun getReadableLocation(item: Item, garages: List): String {
+ val garage = garages.find { it.id == item.garageId } ?: return "Unknown"
+ val cabinet = garage.cabinets.find { it.id == item.cabinetId } ?: return garage.name
+ val shelf = cabinet.shelves.find { it.id == item.shelfId } ?: return "${garage.name} > ${cabinet.name}"
+ val box = shelf.boxes.find { it.id == item.boxId }
+
+ val shelfAndBox = if (box != null) "${shelf.name} > ${box.name}" else shelf.name
+
+ return if (garage.name.equals(cabinet.name, ignoreCase = true)) {
+ shelfAndBox
+ } else {
+ "${cabinet.name} > $shelfAndBox"
+ }
+}
+
+data class AnalyticsData(
+ val name: String,
+ val itemCount: Int,
+ val totalQuantity: Int,
+ val totalValue: Double
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/samuel/inventorymanager/screens/SearchScreen.kt b/app/src/main/java/com/samuel/inventorymanager/screens/SearchScreen.kt
new file mode 100644
index 0000000..cae82d9
--- /dev/null
+++ b/app/src/main/java/com/samuel/inventorymanager/screens/SearchScreen.kt
@@ -0,0 +1,484 @@
+package com.samuel.inventorymanager.screens
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.expandVertically
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.shrinkVertically
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+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.filled.ArrowDropDown
+import androidx.compose.material.icons.filled.Clear
+import androidx.compose.material.icons.filled.FilterList
+import androidx.compose.material.icons.filled.KeyboardArrowRight
+import androidx.compose.material.icons.filled.Search
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExposedDropdownMenuBox
+import androidx.compose.material3.ExposedDropdownMenuDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedButton
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.rotate
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun SearchScreen(
+ items: List
- ,
+ garages: List,
+ onItemClick: ((Item) -> Unit)? = null
+) {
+ var searchQuery by remember { mutableStateOf("") }
+ var selectedCategory by remember { mutableStateOf("All Categories") }
+ var selectedLocation by remember { mutableStateOf("All Locations") }
+ var isFiltersExpanded by remember { mutableStateOf(false) }
+
+ // Create dropdown options
+ val categories = remember(items) {
+ listOf("All Categories") + items.map { it.sizeCategory }.distinct().sorted()
+ }
+
+ val locations = remember(garages) {
+ listOf("All Locations") + garages.map { it.name }.sorted()
+ }
+
+ // Filter items based on search criteria
+ val filteredItems = remember(searchQuery, selectedCategory, selectedLocation, items) {
+ items.filter { item ->
+ val matchesSearch = searchQuery.isBlank() ||
+ item.name.contains(searchQuery, ignoreCase = true) ||
+ item.modelNumber?.contains(searchQuery, ignoreCase = true) == true ||
+ item.description?.contains(searchQuery, ignoreCase = true) == true
+
+ val matchesCategory = selectedCategory == "All Categories" ||
+ item.sizeCategory == selectedCategory
+
+ val matchesLocation = selectedLocation == "All Locations" ||
+ garages.find { it.id == item.garageId }?.name == selectedLocation
+
+ matchesSearch && matchesCategory && matchesLocation
+ }
+ }
+
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(MaterialTheme.colorScheme.background)
+ ) {
+ // Compact Search Header
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.surface
+ ),
+ shape = RoundedCornerShape(0.dp),
+ elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
+ ) {
+ Column(
+ modifier = Modifier.padding(16.dp)
+ ) {
+ // Main Search Bar
+ OutlinedTextField(
+ value = searchQuery,
+ onValueChange = { searchQuery = it },
+ modifier = Modifier.fillMaxWidth(),
+ placeholder = { Text("Search by name, model, or description") },
+ leadingIcon = {
+ Icon(Icons.Default.Search, contentDescription = "Search")
+ },
+ trailingIcon = {
+ if (searchQuery.isNotEmpty()) {
+ IconButton(onClick = { searchQuery = "" }) {
+ Icon(Icons.Default.Clear, contentDescription = "Clear")
+ }
+ }
+ },
+ singleLine = true,
+ shape = RoundedCornerShape(12.dp)
+ )
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ // Filters Toggle Button
+ OutlinedButton(
+ onClick = { isFiltersExpanded = !isFiltersExpanded },
+ modifier = Modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(12.dp),
+ colors = ButtonDefaults.outlinedButtonColors(
+ containerColor = if (isFiltersExpanded)
+ MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)
+ else
+ MaterialTheme.colorScheme.surface
+ )
+ ) {
+ Icon(
+ Icons.Default.FilterList,
+ contentDescription = null,
+ modifier = Modifier.size(20.dp)
+ )
+ Spacer(modifier = Modifier.width(8.dp))
+ Text(
+ "Advanced Filters",
+ modifier = Modifier.weight(1f)
+ )
+ Icon(
+ Icons.Default.ArrowDropDown,
+ contentDescription = null,
+ modifier = Modifier.rotate(if (isFiltersExpanded) 180f else 0f)
+ )
+ }
+
+ // Expandable Filters Section
+ AnimatedVisibility(
+ visible = isFiltersExpanded,
+ enter = expandVertically() + fadeIn(),
+ exit = shrinkVertically() + fadeOut()
+ ) {
+ Column(
+ modifier = Modifier.padding(top = 16.dp)
+ ) {
+ // Category Dropdown
+ DropdownMenuField(
+ label = "Category",
+ selectedValue = selectedCategory,
+ options = categories,
+ onValueChange = { selectedCategory = it },
+ modifier = Modifier.fillMaxWidth()
+ )
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ // Location Dropdown
+ DropdownMenuField(
+ label = "Location",
+ selectedValue = selectedLocation,
+ options = locations,
+ onValueChange = { selectedLocation = it },
+ modifier = Modifier.fillMaxWidth()
+ )
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ // Clear Filters Button
+ OutlinedButton(
+ onClick = {
+ selectedCategory = "All Categories"
+ selectedLocation = "All Locations"
+ },
+ modifier = Modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(12.dp)
+ ) {
+ Icon(
+ Icons.Default.Clear,
+ contentDescription = null,
+ modifier = Modifier.size(18.dp)
+ )
+ Spacer(modifier = Modifier.width(8.dp))
+ Text("Clear Filters")
+ }
+ }
+ }
+ }
+ }
+
+ // Results Count Bar
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(MaterialTheme.colorScheme.surfaceVariant)
+ .padding(horizontal = 16.dp, vertical = 12.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ text = "${filteredItems.size} item${if (filteredItems.size != 1) "s" else ""} found",
+ style = MaterialTheme.typography.titleSmall,
+ fontWeight = FontWeight.Bold,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+
+ if (searchQuery.isNotEmpty() || selectedCategory != "All Categories" || selectedLocation != "All Locations") {
+ Text(
+ text = "Clear All",
+ style = MaterialTheme.typography.labelMedium,
+ color = MaterialTheme.colorScheme.primary,
+ fontWeight = FontWeight.Bold,
+ modifier = Modifier.clickable {
+ searchQuery = ""
+ selectedCategory = "All Categories"
+ selectedLocation = "All Locations"
+ }
+ )
+ }
+ }
+
+ // Results List
+ if (filteredItems.isEmpty()) {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center
+ ) {
+ Icon(
+ Icons.Default.Search,
+ contentDescription = null,
+ modifier = Modifier.size(64.dp),
+ tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f)
+ )
+ Spacer(modifier = Modifier.height(16.dp))
+ Text(
+ text = if (searchQuery.isEmpty() && selectedCategory == "All Categories" && selectedLocation == "All Locations") {
+ "Enter search terms or use filters"
+ } else {
+ "No items found"
+ },
+ style = MaterialTheme.typography.bodyLarge,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+ } else {
+ LazyColumn(
+ modifier = Modifier.fillMaxSize(),
+ contentPadding = androidx.compose.foundation.layout.PaddingValues(16.dp),
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ items(filteredItems) { item ->
+ CleanItemCard(
+ item = item,
+ garages = garages,
+ onClick = { onItemClick?.invoke(item) }
+ )
+ }
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun DropdownMenuField(
+ label: String,
+ selectedValue: String,
+ options: List,
+ onValueChange: (String) -> Unit,
+ modifier: Modifier = Modifier
+) {
+ var expanded by remember { mutableStateOf(false) }
+
+ ExposedDropdownMenuBox(
+ expanded = expanded,
+ onExpandedChange = { expanded = !expanded },
+ modifier = modifier
+ ) {
+ OutlinedTextField(
+ value = selectedValue,
+ onValueChange = {},
+ readOnly = true,
+ label = { Text(label) },
+ trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
+ modifier = Modifier
+ .menuAnchor()
+ .fillMaxWidth(),
+ shape = RoundedCornerShape(12.dp)
+ )
+
+ ExposedDropdownMenu(
+ expanded = expanded,
+ onDismissRequest = { expanded = false }
+ ) {
+ options.forEach { option ->
+ DropdownMenuItem(
+ text = { Text(option) },
+ onClick = {
+ onValueChange(option)
+ expanded = false
+ }
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun CleanItemCard(
+ item: Item,
+ garages: List,
+ onClick: () -> Unit
+) {
+ val locationPath = remember(item, garages) {
+ buildLocationPath(item, garages)
+ }
+
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable(onClick = onClick),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.surface
+ ),
+ elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
+ shape = RoundedCornerShape(12.dp)
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ // Left Content
+ Column(
+ modifier = Modifier.weight(1f)
+ ) {
+ // Item Name
+ Text(
+ text = item.name,
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.Bold,
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis,
+ color = MaterialTheme.colorScheme.onSurface
+ )
+
+ if (item.modelNumber != null) {
+ Spacer(modifier = Modifier.height(4.dp))
+ Text(
+ text = "Model: ${item.modelNumber}",
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ // Location
+ Row(
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ imageVector = Icons.Default.KeyboardArrowRight,
+ contentDescription = null,
+ modifier = Modifier.size(16.dp),
+ tint = MaterialTheme.colorScheme.primary
+ )
+ Spacer(modifier = Modifier.width(4.dp))
+ Text(
+ text = locationPath,
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.primary,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+
+ // Additional Info Row
+ Spacer(modifier = Modifier.height(8.dp))
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ if (item.quantity > 0) {
+ InfoChip(
+ label = "Qty",
+ value = item.quantity.toString()
+ )
+ }
+ if (item.sizeCategory.isNotEmpty()) {
+ InfoChip(
+ label = "Size",
+ value = item.sizeCategory
+ )
+ }
+ if (item.condition.isNotEmpty()) {
+ InfoChip(
+ label = "Condition",
+ value = item.condition
+ )
+ }
+ }
+ }
+
+ // Right Arrow
+ Icon(
+ imageVector = Icons.Default.KeyboardArrowRight,
+ contentDescription = "View Details",
+ tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f),
+ modifier = Modifier.size(24.dp)
+ )
+ }
+ }
+}
+
+@Composable
+private fun InfoChip(
+ label: String,
+ value: String
+) {
+ Row(
+ modifier = Modifier
+ .background(
+ MaterialTheme.colorScheme.secondaryContainer,
+ RoundedCornerShape(6.dp)
+ )
+ .padding(horizontal = 8.dp, vertical = 4.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ text = "$label: ",
+ style = MaterialTheme.typography.labelSmall,
+ color = MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.7f)
+ )
+ Text(
+ text = value,
+ style = MaterialTheme.typography.labelSmall,
+ fontWeight = FontWeight.Bold,
+ color = MaterialTheme.colorScheme.onSecondaryContainer
+ )
+ }
+}
+
+private fun buildLocationPath(item: Item, garages: List): String {
+ val garage = garages.find { it.id == item.garageId }
+ val cabinet = garage?.cabinets?.find { it.id == item.cabinetId }
+ val shelf = cabinet?.shelves?.find { it.id == item.shelfId }
+ val box = shelf?.boxes?.find { it.id == item.boxId }
+
+ return buildString {
+ garage?.let { append(it.name) }
+ cabinet?.let { append(" → ${it.name}") }
+ shelf?.let { append(" → ${it.name}") }
+ box?.let { append(" → ${it.name}") }
+ }.ifEmpty { "Unknown Location" }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/samuel/inventorymanager/screens/SettingScreen.kt b/app/src/main/java/com/samuel/inventorymanager/screens/SettingScreen.kt
new file mode 100644
index 0000000..cc87f1f
--- /dev/null
+++ b/app/src/main/java/com/samuel/inventorymanager/screens/SettingScreen.kt
@@ -0,0 +1,1356 @@
+@file:Suppress("DEPRECATION")
+
+package com.samuel.inventorymanager.screens
+
+import android.Manifest
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.net.Uri
+import android.os.Build
+import android.os.Environment
+import android.provider.Settings
+import android.widget.Toast
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ArrowDownward
+import androidx.compose.material.icons.filled.ArrowUpward
+import androidx.compose.material.icons.filled.Backup
+import androidx.compose.material.icons.filled.Check
+import androidx.compose.material.icons.filled.CheckCircle
+import androidx.compose.material.icons.filled.Cloud
+import androidx.compose.material.icons.filled.DocumentScanner
+import androidx.compose.material.icons.filled.Download
+import androidx.compose.material.icons.filled.ExpandLess
+import androidx.compose.material.icons.filled.ExpandMore
+import androidx.compose.material.icons.filled.Folder
+import androidx.compose.material.icons.filled.Login
+import androidx.compose.material.icons.filled.Logout
+import androidx.compose.material.icons.filled.Palette
+import androidx.compose.material.icons.filled.Psychology
+import androidx.compose.material.icons.filled.RestartAlt
+import androidx.compose.material.icons.filled.Save
+import androidx.compose.material.icons.filled.Storage
+import androidx.compose.material.icons.filled.Upload
+import androidx.compose.material.icons.filled.Visibility
+import androidx.compose.material.icons.filled.VisibilityOff
+import androidx.compose.material.icons.filled.Warning
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedButton
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Slider
+import androidx.compose.material3.Switch
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.input.PasswordVisualTransformation
+import androidx.compose.ui.text.input.VisualTransformation
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.core.content.ContextCompat
+import com.google.android.gms.auth.api.signin.GoogleSignIn
+import com.google.android.gms.common.api.ApiException
+import com.samuel.inventorymanager.auth.GoogleAuthManager
+import com.samuel.inventorymanager.data.AISettings
+import com.samuel.inventorymanager.data.AppSettings
+import com.samuel.inventorymanager.data.AppTheme
+import com.samuel.inventorymanager.data.AutoFeatures
+import com.samuel.inventorymanager.data.CustomTheme
+import com.samuel.inventorymanager.data.FontSize
+import com.samuel.inventorymanager.data.GoogleSettings
+import com.samuel.inventorymanager.data.OCRSettings
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun SettingsScreen(
+ currentSettings: AppSettings,
+ onSettingsChange: (AppSettings) -> Unit
+) {
+ val context = LocalContext.current
+ val scope = rememberCoroutineScope()
+ val authManager = remember { GoogleAuthManager(context) }
+
+ var settings by remember { mutableStateOf(currentSettings) }
+ var showColorPicker by remember { mutableStateOf(false) }
+ var showResetDialog by remember { mutableStateOf(false) }
+ var isProcessing by remember { mutableStateOf(false) }
+ var feedbackMessage by remember { mutableStateOf(null) }
+
+ LaunchedEffect(currentSettings) {
+ settings = currentSettings
+ }
+
+ LaunchedEffect(feedbackMessage) {
+ feedbackMessage?.let { message ->
+ Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
+ kotlinx.coroutines.delay(2000)
+ feedbackMessage = null
+ }
+ }
+
+ val storagePermissionLauncher = rememberLauncherForActivityResult(
+ ActivityResultContracts.RequestMultiplePermissions()
+ ) { permissions ->
+ val granted = permissions.values.all { it }
+ feedbackMessage = if (granted) "✅ Storage permission granted" else "❌ Storage permission denied"
+ }
+
+ val manageStorageLauncher = rememberLauncherForActivityResult(
+ ActivityResultContracts.StartActivityForResult()
+ ) {
+ val granted = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ Environment.isExternalStorageManager()
+ } else true
+ feedbackMessage = if (granted) "✅ Storage access granted" else "❌ Storage access denied"
+ }
+
+ val requestStoragePermission = {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ if (!Environment.isExternalStorageManager()) {
+ val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION).apply {
+ data = Uri.parse("package:${context.packageName}")
+ }
+ manageStorageLauncher.launch(intent)
+ }
+ } else {
+ storagePermissionLauncher.launch(
+ arrayOf(
+ Manifest.permission.READ_EXTERNAL_STORAGE,
+ Manifest.permission.WRITE_EXTERNAL_STORAGE
+ )
+ )
+ }
+ }
+
+ // Firebase Google Sign-In Launcher
+ val googleSignInLauncher = rememberLauncherForActivityResult(
+ ActivityResultContracts.StartActivityForResult()
+ ) { result ->
+ if (result.resultCode == Activity.RESULT_OK) {
+ val task = GoogleSignIn.getSignedInAccountFromIntent(result.data)
+ try {
+ val account = task.getResult(ApiException::class.java)
+ val idToken = account?.idToken
+
+ if (idToken != null) {
+ authManager.handleSignInResult(
+ idToken,
+ onSuccess = { message ->
+ settings = settings.copy(
+ googleSettings = settings.googleSettings.copy(
+ signedIn = true,
+ userEmail = authManager.getCurrentUserEmail()
+ )
+ )
+ onSettingsChange(settings)
+ feedbackMessage = message
+ },
+ onError = { error ->
+ feedbackMessage = error
+ }
+ )
+ }
+ } catch (e: ApiException) {
+ feedbackMessage = "❌ Sign in failed: ${e.message}"
+ }
+ } else {
+ feedbackMessage = "❌ Sign in cancelled"
+ }
+ }
+
+ val onSignInClick: () -> Unit = {
+ val signInIntent = authManager.getSignInIntent()
+ googleSignInLauncher.launch(signInIntent)
+ }
+
+ val importLauncher = rememberLauncherForActivityResult(
+ ActivityResultContracts.GetContent()
+ ) { uri: Uri? ->
+ uri?.let {
+ scope.launch {
+ isProcessing = true
+ val result = importSettings(context, it)
+ isProcessing = false
+ result?.let { importedSettings ->
+ settings = importedSettings
+ onSettingsChange(importedSettings)
+ feedbackMessage = "✅ Settings imported successfully"
+ } ?: run {
+ feedbackMessage = "❌ Failed to import settings"
+ }
+ }
+ }
+ }
+
+ val exportLauncher = rememberLauncherForActivityResult(
+ ActivityResultContracts.CreateDocument("application/json")
+ ) { uri: Uri? ->
+ uri?.let {
+ scope.launch {
+ isProcessing = true
+ val success = exportSettings(context, settings, it)
+ isProcessing = false
+ feedbackMessage = if (success) "✅ Settings exported successfully"
+ else "❌ Failed to export settings"
+ }
+ }
+ }
+
+ var themeExpanded by remember { mutableStateOf(true) }
+ var ocrExpanded by remember { mutableStateOf(false) }
+ var aiExpanded by remember { mutableStateOf(false) }
+ var googleExpanded by remember { mutableStateOf(false) }
+ var dataExpanded by remember { mutableStateOf(false) }
+
+ Box(modifier = Modifier.fillMaxSize()) {
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(16.dp),
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ item {
+ feedbackMessage?.let { message ->
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ colors = CardDefaults.cardColors(
+ containerColor = if (message.startsWith("✅"))
+ MaterialTheme.colorScheme.primaryContainer
+ else MaterialTheme.colorScheme.errorContainer
+ )
+ ) {
+ Text(
+ message,
+ modifier = Modifier.padding(16.dp),
+ style = MaterialTheme.typography.bodyMedium
+ )
+ }
+ }
+ }
+
+ item {
+ ExpandableCard(
+ title = "Theme Settings",
+ icon = Icons.Default.Palette,
+ expanded = themeExpanded,
+ onToggle = { themeExpanded = !themeExpanded }
+ ) {
+ ThemeSettingsContent(
+ settings = settings,
+ onThemeChange = { newTheme ->
+ settings = settings.copy(theme = newTheme)
+ onSettingsChange(settings)
+ },
+ onFontSizeChange = { newFontSize ->
+ settings = settings.copy(fontSize = newFontSize)
+ onSettingsChange(settings)
+ },
+ onCustomThemeClick = { showColorPicker = true }
+ )
+ }
+ }
+
+ item {
+ ExpandableCard(
+ title = "OCR Settings & Fallback Priority",
+ icon = Icons.Default.DocumentScanner,
+ expanded = ocrExpanded,
+ onToggle = { ocrExpanded = !ocrExpanded }
+ ) {
+ OCRSettingsContent(
+ ocrSettings = settings.ocrSettings,
+ onUpdate = { newOcrSettings ->
+ settings = settings.copy(ocrSettings = newOcrSettings)
+ onSettingsChange(settings)
+ }
+ )
+ }
+ }
+
+ item {
+ ExpandableCard(
+ title = "AI Settings & Fallback Priority",
+ icon = Icons.Default.Psychology,
+ expanded = aiExpanded,
+ onToggle = { aiExpanded = !aiExpanded }
+ ) {
+ AISettingsContent(
+ aiSettings = settings.aiSettings,
+ onUpdate = { newAiSettings ->
+ settings = settings.copy(aiSettings = newAiSettings)
+ onSettingsChange(settings)
+ }
+ )
+ }
+ }
+
+ item {
+ ExpandableCard(
+ title = "Google Settings",
+ icon = Icons.Default.Cloud,
+ expanded = googleExpanded,
+ onToggle = { googleExpanded = !googleExpanded }
+ ) {
+ GoogleSettingsContent(
+ authManager = authManager,
+ googleSettings = settings.googleSettings,
+ onSignInClick = onSignInClick,
+ onUpdate = { newGoogleSettings ->
+ settings = settings.copy(googleSettings = newGoogleSettings)
+ onSettingsChange(settings)
+ },
+ onBackupNow = {
+ scope.launch {
+ isProcessing = true
+ authManager.uploadToDrive(
+ "inventory_backup_${System.currentTimeMillis()}.json",
+ onSuccess = { message ->
+ isProcessing = false
+ val updated = settings.copy(
+ googleSettings = settings.googleSettings.copy(
+ lastBackupTime = System.currentTimeMillis()
+ )
+ )
+ settings = updated
+ onSettingsChange(updated)
+ feedbackMessage = message
+ }
+ )
+ }
+ }
+ )
+ }
+ }
+
+ item {
+ ExpandableCard(
+ title = "Data Management & Android Features",
+ icon = Icons.Default.Storage,
+ expanded = dataExpanded,
+ onToggle = { dataExpanded = !dataExpanded }
+ ) {
+ DataManagementContent(
+ autoFeatures = settings.autoFeatures,
+ onRequestPermissions = requestStoragePermission,
+ onUpdate = { newAutoFeatures ->
+ settings = settings.copy(autoFeatures = newAutoFeatures)
+ onSettingsChange(settings)
+ },
+ onExport = {
+ val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault())
+ .format(Date())
+ exportLauncher.launch("inventory_settings_$timestamp.json")
+ },
+ onImport = {
+ importLauncher.launch("application/json")
+ },
+ onLocalSaveNow = {
+ scope.launch {
+ isProcessing = true
+ val success = performLocalSave(context, settings)
+ isProcessing = false
+ if (success) {
+ val updated = settings.copy(
+ autoFeatures = settings.autoFeatures.copy(
+ lastLocalSaveTime = System.currentTimeMillis()
+ )
+ )
+ settings = updated
+ onSettingsChange(updated)
+ feedbackMessage = "✅ Local save completed"
+ } else {
+ feedbackMessage = "❌ Local save failed"
+ }
+ }
+ }
+ )
+ }
+ }
+
+ item {
+ Button(
+ onClick = { showResetDialog = true },
+ modifier = Modifier.fillMaxWidth(),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.error
+ )
+ ) {
+ Icon(Icons.Default.RestartAlt, null)
+ Spacer(Modifier.width(8.dp))
+ Text("Reset All Settings")
+ }
+ }
+ }
+
+ if (isProcessing) {
+ Box(
+ Modifier
+ .fillMaxSize()
+ .background(Color.Black.copy(alpha = 0.5f)),
+ contentAlignment = Alignment.Center
+ ) {
+ CircularProgressIndicator(color = MaterialTheme.colorScheme.primary)
+ }
+ }
+ }
+
+ if (showColorPicker) {
+ CustomThemeDialog(
+ currentTheme = settings.customTheme ?: CustomTheme(),
+ onDismiss = { showColorPicker = false },
+ onConfirm = { customTheme ->
+ settings = settings.copy(theme = AppTheme.CUSTOM, customTheme = customTheme)
+ onSettingsChange(settings)
+ showColorPicker = false
+ }
+ )
+ }
+
+ if (showResetDialog) {
+ AlertDialog(
+ onDismissRequest = { showResetDialog = false },
+ title = { Text("Reset Settings") },
+ text = { Text("Are you sure you want to reset all settings to default?") },
+ confirmButton = {
+ Button(
+ onClick = {
+ settings = AppSettings()
+ onSettingsChange(settings)
+ showResetDialog = false
+ feedbackMessage = "✅ Settings reset to default"
+ },
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.error
+ )
+ ) { Text("Reset") }
+ },
+ dismissButton = {
+ TextButton({ showResetDialog = false }) { Text("Cancel") }
+ }
+ )
+ }
+}
+
+// ========================================================================================
+// HELPER FUNCTIONS
+// ========================================================================================
+
+suspend fun exportSettings(context: Context, settings: AppSettings, uri: Uri): Boolean {
+ return withContext(Dispatchers.IO) {
+ try {
+ context.contentResolver.openOutputStream(uri)?.use { outputStream ->
+ outputStream.write(settings.toJson().toByteArray())
+ }
+ true
+ } catch (e: Exception) {
+ e.printStackTrace()
+ false
+ }
+ }
+}
+
+suspend fun importSettings(context: Context, uri: Uri): AppSettings? {
+ return withContext(Dispatchers.IO) {
+ try {
+ context.contentResolver.openInputStream(uri)?.use { inputStream ->
+ val json = inputStream.bufferedReader().readText()
+ AppSettings.fromJson(json)
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ null
+ }
+ }
+}
+
+suspend fun performLocalSave(context: Context, settings: AppSettings): Boolean {
+ return withContext(Dispatchers.IO) {
+ try {
+ val file = java.io.File(context.filesDir, "app_settings.json")
+ file.writeText(settings.toJson())
+ true
+ } catch (e: Exception) {
+ e.printStackTrace()
+ false
+ }
+ }
+}
+
+// ========================================================================================
+// COMPOSABLE COMPONENTS
+// ========================================================================================
+
+@Composable
+fun ExpandableCard(
+ title: String,
+ icon: ImageVector,
+ expanded: Boolean,
+ onToggle: () -> Unit,
+ content: @Composable ColumnScope.() -> Unit
+) {
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ elevation = CardDefaults.cardElevation(2.dp)
+ ) {
+ Column {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable(onClick = onToggle)
+ .padding(16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(icon, null, tint = MaterialTheme.colorScheme.primary)
+ Spacer(Modifier.width(16.dp))
+ Text(
+ title,
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.Bold,
+ modifier = Modifier.weight(1f)
+ )
+ Icon(
+ if (expanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore,
+ null
+ )
+ }
+ AnimatedVisibility(visible = expanded) {
+ Column(
+ Modifier
+ .padding(horizontal = 16.dp, vertical = 8.dp)
+ .fillMaxWidth(),
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ content = content
+ )
+ }
+ }
+ }
+}
+
+@Composable
+fun ThemeSettingsContent(
+ settings: AppSettings,
+ onThemeChange: (AppTheme) -> Unit,
+ onFontSizeChange: (FontSize) -> Unit,
+ onCustomThemeClick: () -> Unit
+) {
+ val themeOptions = listOf(
+ "☀️ Light" to Color(0xFFFFFBFE),
+ "🌙 Dark" to Color(0xFF1A1A1A),
+ "🧛 Dracula" to Color(0xFFBD93F9),
+ "🧟 Vampire" to Color(0xFFFF1493),
+ "🌊 Ocean" to Color(0xFF00B4D8),
+ "🌲 Forest" to Color(0xFF2D6A4F),
+ "🌅 Sunset" to Color(0xFFFF6B35),
+ "⚙️ Cyberpunk" to Color(0xFFFF006E),
+ "⚡ Neon" to Color(0xFF39FF14)
+ )
+
+ Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
+ Text("Select Theme", style = MaterialTheme.typography.labelLarge)
+
+ // --- FIX START ---
+ // Replace LazyVerticalGrid with a standard Column and Rows to build the grid
+ Column(
+ modifier = Modifier.fillMaxWidth(),
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ // Group the theme options into rows of 3
+ themeOptions.chunked(3).forEach { rowItems ->
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ // Create a ThemeOptionCard for each item in the row, giving it equal weight
+ rowItems.forEach { (name, color) ->
+ Box(modifier = Modifier.weight(1f)) {
+ ThemeOptionCard(name, color, settings.theme == AppTheme.CUSTOM) {
+ onThemeChange(AppTheme.CUSTOM)
+ onCustomThemeClick()
+ }
+ }
+ }
+ // Add invisible spacers to fill the row if it has fewer than 3 items.
+ // This ensures items in the last row align correctly with the rows above.
+ if (rowItems.size < 3) {
+ repeat(3 - rowItems.size) {
+ Spacer(modifier = Modifier.weight(1f))
+ }
+ }
+ }
+ }
+ }
+ // --- FIX END ---
+
+ HorizontalDivider(Modifier.padding(vertical = 8.dp))
+
+ Text("Font & Icon Size", style = MaterialTheme.typography.labelLarge)
+ Text(
+ "Select default text size",
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
+ FontSize.entries.forEach { size ->
+ FontSizeButton(
+ size.name.lowercase().replaceFirstChar { it.uppercase() },
+ settings.fontSize == size
+ ) { onFontSizeChange(size) }
+ }
+ }
+ }
+}
+
+@Composable
+fun ThemeOptionCard(
+ themeName: String,
+ themeColor: Color,
+ isSelected: Boolean,
+ onClick: () -> Unit
+) {
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(80.dp)
+ .clickable(onClick = onClick),
+ colors = CardDefaults.cardColors(
+ containerColor = themeColor.copy(alpha = 0.2f)
+ ),
+ border = if (isSelected) BorderStroke(3.dp, themeColor) else null,
+ elevation = CardDefaults.cardElevation(if (isSelected) 4.dp else 1.dp)
+ ) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(
+ brush = Brush.linearGradient(
+ colors = listOf(
+ themeColor.copy(alpha = 0.4f),
+ themeColor.copy(alpha = 0.1f)
+ )
+ )
+ ),
+ contentAlignment = Alignment.Center
+ ) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center
+ ) {
+ Text(
+ themeName,
+ style = MaterialTheme.typography.labelMedium,
+ color = Color.Black,
+ fontWeight = FontWeight.Bold
+ )
+ if (isSelected) {
+ Icon(
+ Icons.Default.Check,
+ null,
+ tint = themeColor,
+ modifier = Modifier.size(16.dp)
+ )
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun RowScope.FontSizeButton(text: String, selected: Boolean, onClick: () -> Unit) {
+ Button(
+ onClick,
+ Modifier.weight(1f),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = if (selected)
+ MaterialTheme.colorScheme.primary
+ else
+ MaterialTheme.colorScheme.surfaceVariant
+ )
+ ) {
+ Text(text, fontSize = 11.sp, maxLines = 1)
+ }
+}
+
+@Composable
+fun OCRSettingsContent(ocrSettings: OCRSettings, onUpdate: (OCRSettings) -> Unit) {
+ Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
+ Text(
+ "OCR Priority (Use Fallback)",
+ style = MaterialTheme.typography.titleSmall,
+ fontWeight = FontWeight.Bold
+ )
+ Text(
+ "Higher priority providers are tried first",
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+
+ ocrSettings.providerPriority.forEachIndexed { index, provider ->
+ ProviderPriorityItem(
+ name = provider.name.replace("_", " ").lowercase()
+ .split(" ").joinToString(" ") { it.replaceFirstChar { c -> c.uppercase() } },
+ priority = index + 1,
+ onMoveUp = if (index > 0) {
+ {
+ val newList = ocrSettings.providerPriority.toMutableList()
+ val temp = newList[index]
+ newList[index] = newList[index - 1]
+ newList[index - 1] = temp
+ onUpdate(ocrSettings.copy(providerPriority = newList))
+ }
+ } else null,
+ onMoveDown = if (index < ocrSettings.providerPriority.size - 1) {
+ {
+ val newList = ocrSettings.providerPriority.toMutableList()
+ val temp = newList[index]
+ newList[index] = newList[index + 1]
+ newList[index + 1] = temp
+ onUpdate(ocrSettings.copy(providerPriority = newList))
+ }
+ } else null
+ )
+ }
+
+ Button(
+ onClick = { onUpdate(ocrSettings.copy(providerPriority = OCRSettings().providerPriority)) },
+ modifier = Modifier.fillMaxWidth(),
+ colors = ButtonDefaults.outlinedButtonColors()
+ ) {
+ Icon(Icons.Default.RestartAlt, null)
+ Spacer(Modifier.width(8.dp))
+ Text("Reset to Default Priority")
+ }
+
+ HorizontalDivider(Modifier.padding(vertical = 8.dp))
+
+ APIKeyField("Roboflow API Key", ocrSettings.roboflowApiKey) {
+ onUpdate(ocrSettings.copy(roboflowApiKey = it))
+ }
+ APIKeyField("OCR Space API Key", ocrSettings.ocrSpaceApiKey) {
+ onUpdate(ocrSettings.copy(ocrSpaceApiKey = it))
+ }
+ APIKeyField("Google Vision API Key", ocrSettings.googleVisionApiKey) {
+ onUpdate(ocrSettings.copy(googleVisionApiKey = it))
+ }
+ }
+}
+
+@Composable
+fun AISettingsContent(aiSettings: AISettings, onUpdate: (AISettings) -> Unit) {
+ Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
+ Text(
+ "AI Priority (Use Fallback)",
+ style = MaterialTheme.typography.titleSmall,
+ fontWeight = FontWeight.Bold
+ )
+ Text(
+ "Higher priority providers are tried first",
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+
+ aiSettings.providerPriority.forEachIndexed { index, provider ->
+ ProviderPriorityItem(
+ name = provider.name.replace("_", " ").lowercase()
+ .split(" ").joinToString(" ") { it.replaceFirstChar { c -> c.uppercase() } },
+ priority = index + 1,
+ onMoveUp = if (index > 0) {
+ {
+ val newList = aiSettings.providerPriority.toMutableList()
+ val temp = newList[index]
+ newList[index] = newList[index - 1]
+ newList[index - 1] = temp
+ onUpdate(aiSettings.copy(providerPriority = newList))
+ }
+ } else null,
+ onMoveDown = if (index < aiSettings.providerPriority.size - 1) {
+ {
+ val newList = aiSettings.providerPriority.toMutableList()
+ val temp = newList[index]
+ newList[index] = newList[index + 1]
+ newList[index + 1] = temp
+ onUpdate(aiSettings.copy(providerPriority = newList))
+ }
+ } else null
+ )
+ }
+
+ Icon(Icons.Default.RestartAlt, null)
+ Spacer(Modifier.width(8.dp))
+ Text("Reset to Default Priority")
+ }
+
+ HorizontalDivider(Modifier.padding(vertical = 8.dp))
+
+ APIKeyField("Google Gemini API Key", aiSettings.googleGeminiApiKey) {
+ onUpdate(aiSettings.copy(googleGeminiApiKey = it))
+ }
+ APIKeyField("ChatGPT API Key", aiSettings.openAIApiKey) {
+ onUpdate(aiSettings.copy(openAIApiKey = it))
+ }
+}
+
+
+@Composable
+fun GoogleSettingsContent(
+ authManager: GoogleAuthManager,
+ googleSettings: GoogleSettings,
+ onSignInClick: () -> Unit,
+ onUpdate: (GoogleSettings) -> Unit,
+ onBackupNow: () -> Unit
+) {
+ Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
+ if (authManager.isSignedIn()) {
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.primaryContainer
+ )
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ Icons.Default.CheckCircle,
+ "Signed In",
+ tint = MaterialTheme.colorScheme.primary,
+ modifier = Modifier.size(32.dp)
+ )
+ Spacer(Modifier.width(12.dp))
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ "✅ Signed In",
+ fontWeight = FontWeight.Bold,
+ color = MaterialTheme.colorScheme.onPrimaryContainer
+ )
+ Text(
+ authManager.getCurrentUserEmail(),
+ style = MaterialTheme.typography.bodySmall,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ color = MaterialTheme.colorScheme.onPrimaryContainer
+ )
+ }
+ }
+ }
+
+ SwitchRow(
+ "🔄 Auto Backup to Drive",
+ googleSettings.autoBackupToDrive
+ ) {
+ onUpdate(googleSettings.copy(autoBackupToDrive = it))
+ }
+
+ if (googleSettings.lastBackupTime > 0) {
+ Text(
+ "Last backup: ${SimpleDateFormat("MMM dd, yyyy HH:mm", Locale.getDefault()).format(Date(googleSettings.lastBackupTime))}",
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+
+ Button(
+ onClick = onBackupNow,
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Icon(Icons.Default.Backup, null)
+ Spacer(Modifier.width(8.dp))
+ Text("Backup Now")
+ }
+
+ Button(
+ onClick = {
+ authManager.signOut()
+ onUpdate(GoogleSettings())
+ },
+ modifier = Modifier.fillMaxWidth(),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.error
+ )
+ ) {
+ Icon(Icons.Default.Logout, null)
+ Spacer(Modifier.width(8.dp))
+ Text("Sign Out")
+ }
+ } else {
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.surfaceVariant
+ )
+ ) {
+ Column(
+ modifier = Modifier.padding(16.dp),
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ Text(
+ "ℹ️ Why Sign In?",
+ fontWeight = FontWeight.Bold
+ )
+ Text(
+ "• Backup your inventory to Google Drive\n" +
+ "• Access data across devices\n" +
+ "• Never lose your data",
+ style = MaterialTheme.typography.bodySmall
+ )
+ }
+ }
+
+ Button(
+ onClick = onSignInClick,
+ modifier = Modifier.fillMaxWidth(),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = Color(0xFF4285F4)
+ )
+ ) {
+ Icon(Icons.Default.Login, null)
+ Spacer(Modifier.width(8.dp))
+ Text("Sign in with Google")
+ }
+ }
+ }
+}
+
+@Composable
+fun DataManagementContent(
+ autoFeatures: AutoFeatures,
+ onRequestPermissions: () -> Unit,
+ onUpdate: (AutoFeatures) -> Unit,
+ onExport: () -> Unit,
+ onImport: () -> Unit,
+ onLocalSaveNow: () -> Unit
+) {
+ val context = LocalContext.current
+ val hasStoragePermission = remember {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ Environment.isExternalStorageManager()
+ } else {
+ ContextCompat.checkSelfPermission(
+ context,
+ Manifest.permission.WRITE_EXTERNAL_STORAGE
+ ) == PackageManager.PERMISSION_GRANTED
+ }
+ }
+
+ Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
+ if (!hasStoragePermission) {
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.errorContainer
+ )
+ ) {
+ Column(
+ modifier = Modifier.padding(16.dp),
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Icon(
+ Icons.Default.Warning,
+ "Warning",
+ tint = MaterialTheme.colorScheme.error
+ )
+ Spacer(Modifier.width(8.dp))
+ Text(
+ "Storage Permission Required",
+ fontWeight = FontWeight.Bold,
+ color = MaterialTheme.colorScheme.onErrorContainer
+ )
+ }
+ Text(
+ "To save data locally, grant storage access",
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onErrorContainer
+ )
+ Button(
+ onClick = onRequestPermissions,
+ modifier = Modifier.fillMaxWidth(),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.error
+ )
+ ) {
+ Icon(Icons.Default.Folder, null)
+ Spacer(Modifier.width(8.dp))
+ Text("Grant Storage Permission")
+ }
+ }
+ }
+ } else {
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.primaryContainer
+ )
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ Icons.Default.CheckCircle,
+ "Granted",
+ tint = MaterialTheme.colorScheme.primary
+ )
+ Spacer(Modifier.width(12.dp))
+ Text(
+ "✅ Storage Permission Granted",
+ fontWeight = FontWeight.SemiBold,
+ color = MaterialTheme.colorScheme.onPrimaryContainer
+ )
+ }
+ }
+ }
+
+ HorizontalDivider()
+
+ SwitchRow("📦 Auto Local Save", autoFeatures.autoLocalSave) {
+ onUpdate(autoFeatures.copy(autoLocalSave = it))
+ }
+ SwitchRow("☁️ Auto Google Backup", autoFeatures.autoGoogleBackup) {
+ onUpdate(autoFeatures.copy(autoGoogleBackup = it))
+ }
+
+ if (autoFeatures.lastLocalSaveTime > 0) {
+ Text(
+ "Last local save: ${SimpleDateFormat("MMM dd, yyyy HH:mm", Locale.getDefault()).format(Date(autoFeatures.lastLocalSaveTime))}",
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+
+ Button(
+ onClick = onLocalSaveNow,
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Icon(Icons.Default.Save, null)
+ Spacer(Modifier.width(8.dp))
+ Text("Save Locally Now")
+ }
+
+ HorizontalDivider()
+
+ Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
+ OutlinedButton(
+ onClick = onExport,
+ modifier = Modifier.weight(1f)
+ ) {
+ Icon(Icons.Default.Upload, null)
+ Spacer(Modifier.width(4.dp))
+ Text("Export")
+ }
+ OutlinedButton(
+ onClick = onImport,
+ modifier = Modifier.weight(1f)
+ ) {
+ Icon(Icons.Default.Download, null)
+ Spacer(Modifier.width(4.dp))
+ Text("Import")
+ }
+ }
+ }
+}
+
+@Composable
+fun APIKeyField(label: String, value: String, onValueChange: (String) -> Unit) {
+ var showKey by remember { mutableStateOf(false) }
+ OutlinedTextField(
+ value = value,
+ onValueChange = onValueChange,
+ label = { Text(label) },
+ modifier = Modifier.fillMaxWidth(),
+ visualTransformation = if (showKey)
+ VisualTransformation.None
+ else
+ PasswordVisualTransformation(),
+ trailingIcon = {
+ IconButton({ showKey = !showKey }) {
+ Icon(
+ if (showKey) Icons.Default.Visibility else Icons.Default.VisibilityOff,
+ if (showKey) "Hide" else "Show"
+ )
+ }
+ },
+ singleLine = true
+ )
+}
+
+@Composable
+fun SwitchRow(label: String, checked: Boolean, onCheckedChange: (Boolean) -> Unit) {
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .clickable { onCheckedChange(!checked) }
+ .padding(vertical = 8.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(label, Modifier.weight(1f))
+ Switch(checked, onCheckedChange)
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun CustomThemeDialog(
+ currentTheme: CustomTheme,
+ onDismiss: () -> Unit,
+ onConfirm: (CustomTheme) -> Unit
+) {
+ var theme by remember { mutableStateOf(currentTheme) }
+ var fontSizeScale by remember { mutableFloatStateOf(currentTheme.fontSizeScale) }
+
+ val colorOptions = listOf(
+ "Red" to Color(0xFFE53E3E),
+ "Orange" to Color(0xFFDD6B20),
+ "Yellow" to Color(0xFFD69E2E),
+ "Green" to Color(0xFF38A169),
+ "Teal" to Color(0xFF319795),
+ "Blue" to Color(0xFF3182CE),
+ "Purple" to Color(0xFF805AD5),
+ "Pink" to Color(0xFFD53F8C)
+ )
+
+ AlertDialog(
+ onDismissRequest = onDismiss,
+ title = { Text("🎨 Custom Theme") },
+ text = {
+ LazyColumn(
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ item {
+ Text("Select Primary Color", fontWeight = FontWeight.Bold)
+ }
+ item {
+ colorOptions.chunked(4).forEach { rowColors ->
+ Row(
+ Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ rowColors.forEach { (name, color) ->
+ Box(
+ Modifier
+ .weight(1f)
+ .aspectRatio(1f)
+ .clip(RoundedCornerShape(12.dp))
+ .background(color)
+ .clickable {
+ theme = theme.copy(
+ primaryColor = color.toArgb().toLong()
+ )
+ },
+ contentAlignment = Alignment.Center
+ ) {
+ if (Color(theme.primaryColor.toULong()) == color) {
+ Icon(
+ Icons.Default.Check,
+ null,
+ tint = Color.White,
+ modifier = Modifier.size(24.dp)
+ )
+ }
+ }
+ }
+ }
+ Spacer(Modifier.height(8.dp))
+ }
+ }
+
+ item { HorizontalDivider(Modifier.padding(vertical = 8.dp)) }
+
+ item {
+ Text("Select Background", fontWeight = FontWeight.Bold)
+ }
+ item {
+ Row(
+ Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ listOf(
+ "Light" to Color(0xFFFFFFFF),
+ "Dark" to Color(0xFF1A202C)
+ ).forEach { (name, color) ->
+ Box(
+ Modifier
+ .weight(1f)
+ .height(60.dp)
+ .clip(RoundedCornerShape(12.dp))
+ .background(color)
+ .clickable {
+ theme = theme.copy(
+ backgroundColor = color.toArgb().toLong()
+ )
+ },
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ name,
+ color = if (color == Color.White) Color.Black else Color.White,
+ fontWeight = if (Color(theme.backgroundColor.toULong()) == color)
+ FontWeight.Bold else FontWeight.Normal
+ )
+ }
+ }
+ }
+ }
+
+ item { HorizontalDivider(Modifier.padding(vertical = 8.dp)) }
+
+ item {
+ Text("Font & Icon Size Scale", fontWeight = FontWeight.Bold)
+ }
+ item {
+ Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
+ Slider(
+ value = fontSizeScale,
+ onValueChange = { fontSizeScale = it },
+ valueRange = 0.8f..1.5f,
+ steps = 13,
+ modifier = Modifier.fillMaxWidth()
+ )
+ Text(
+ "Scale: %.2f (0.8x to 1.5x)".format(fontSizeScale),
+ style = MaterialTheme.typography.bodySmall
+ )
+ }
+ }
+ }
+ },
+ confirmButton = {
+ Button({
+ onConfirm(theme.copy(fontSizeScale = fontSizeScale))
+ }) { Text("Apply Theme") }
+ },
+ dismissButton = {
+ TextButton(onDismiss) { Text("Cancel") }
+ }
+ )
+}
+
+@Composable
+fun ProviderPriorityItem(
+ name: String,
+ priority: Int,
+ onMoveUp: (() -> Unit)?,
+ onMoveDown: (() -> Unit)?
+) {
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.surfaceVariant
+ )
+ ) {
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .padding(12.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Box(
+ Modifier
+ .size(36.dp)
+ .clip(CircleShape)
+ .background(MaterialTheme.colorScheme.primary),
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ priority.toString(),
+ color = MaterialTheme.colorScheme.onPrimary,
+ fontWeight = FontWeight.Bold,
+ fontSize = 16.sp
+ )
+ }
+ Spacer(Modifier.width(12.dp))
+ Text(name, fontWeight = FontWeight.Medium)
+ }
+ Row {
+ IconButton(
+ onClick = { onMoveUp?.invoke() },
+ enabled = onMoveUp != null
+ ) {
+ Icon(
+ Icons.Default.ArrowUpward,
+ "Move Up",
+ tint = if (onMoveUp != null)
+ MaterialTheme.colorScheme.primary
+ else
+ Color.Gray
+ )
+ }
+ IconButton(
+ onClick = { onMoveDown?.invoke() },
+ enabled = onMoveDown != null
+ ) {
+ Icon(
+ Icons.Default.ArrowDownward,
+ "Move Down",
+ tint = if (onMoveDown != null)
+ MaterialTheme.colorScheme.primary
+ else
+ Color.Gray
+ )
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/samuel/inventorymanager/ui/theme/Color.kt b/app/src/main/java/com/samuel/inventorymanager/ui/theme/Color.kt
new file mode 100644
index 0000000..e6078fe
--- /dev/null
+++ b/app/src/main/java/com/samuel/inventorymanager/ui/theme/Color.kt
@@ -0,0 +1,11 @@
+package com.samuel.inventorymanager.ui.theme
+
+import androidx.compose.ui.graphics.Color
+
+val Purple80 = Color(0xFFD0BCFF)
+val PurpleGrey80 = Color(0xFFCCC2DC)
+val Pink80 = Color(0xFFEFB8C8)
+
+val Purple40 = Color(0xFF6650a4)
+val PurpleGrey40 = Color(0xFF625b71)
+val Pink40 = Color(0xFF7D5260)
\ No newline at end of file
diff --git a/app/src/main/java/com/samuel/inventorymanager/ui/theme/Theme.kt b/app/src/main/java/com/samuel/inventorymanager/ui/theme/Theme.kt
new file mode 100644
index 0000000..fa781d8
--- /dev/null
+++ b/app/src/main/java/com/samuel/inventorymanager/ui/theme/Theme.kt
@@ -0,0 +1,315 @@
+package com.samuel.inventorymanager.ui.theme
+
+import android.os.Build
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Typography
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+
+// ========================================================================================
+// COLOR DEFINITIONS
+// ========================================================================================
+
+private val PurpleLight40 = Color(0xFF6650a4)
+private val PurpleGreyLight40 = Color(0xFF625b71)
+private val PinkLight40 = Color(0xFF7D5260)
+
+private val PurpleDark80 = Color(0xFFEADDF5)
+private val PurpleGreyDark80 = Color(0xFFCCC7DB)
+private val PinkDark80 = Color(0xFFEFB8C8)
+
+// Additional Theme Colors
+private val LightBackground = Color(0xFFFFFBFE)
+private val DarkBackground = Color(0xFF1A1A1A)
+
+private val DraculaPrimary = Color(0xFFBD93F9)
+private val VampirePrimary = Color(0xFFFF1493)
+private val OceanPrimary = Color(0xFF00B4D8)
+private val ForestPrimary = Color(0xFF2D6A4F)
+private val SunsetPrimary = Color(0xFFFF6B35)
+private val CyberpunkPrimary = Color(0xFFFF006E)
+private val NeonPrimary = Color(0xFF39FF14)
+
+// ========================================================================================
+// COLOR SCHEMES
+// ========================================================================================
+
+private val LightColorScheme = lightColorScheme(
+ primary = PurpleLight40,
+ secondary = PurpleGreyLight40,
+ tertiary = PinkLight40,
+ background = LightBackground,
+ surface = Color.White,
+ onPrimary = Color.White,
+ onSecondary = Color.White,
+ onTertiary = Color.White,
+ onBackground = Color(0xFF1C1B1F),
+ onSurface = Color(0xFF1C1B1F)
+)
+
+private val DarkColorScheme = darkColorScheme(
+ primary = PurpleDark80,
+ secondary = PurpleGreyDark80,
+ tertiary = PinkDark80,
+ background = DarkBackground,
+ surface = Color(0xFF2A2A2A),
+ onPrimary = Color.Black,
+ onSecondary = Color.Black,
+ onTertiary = Color.Black,
+ onBackground = Color.White,
+ onSurface = Color.White
+)
+
+private val DraculaLightScheme = lightColorScheme(
+ primary = DraculaPrimary,
+ secondary = Color(0xFF8BE9FD),
+ tertiary = Color(0xFFFF79C6),
+ background = Color(0xFFF8F8F2),
+ surface = Color.White
+)
+
+private val DraculaDarkScheme = darkColorScheme(
+ primary = DraculaPrimary,
+ secondary = Color(0xFF8BE9FD),
+ tertiary = Color(0xFFFF79C6),
+ background = Color(0xFF282A36),
+ surface = Color(0xFF21222C)
+)
+
+private val VampireLightScheme = lightColorScheme(
+ primary = VampirePrimary,
+ secondary = Color(0xFFE91E63),
+ tertiary = Color(0xFFC2185B),
+ background = Color(0xFFFFF1F5),
+ surface = Color.White
+)
+
+private val VampireDarkScheme = darkColorScheme(
+ primary = VampirePrimary,
+ secondary = Color(0xFFE91E63),
+ tertiary = Color(0xFFC2185B),
+ background = Color(0xFF1A0015),
+ surface = Color(0xFF2A0A1F)
+)
+
+private val OceanLightScheme = lightColorScheme(
+ primary = OceanPrimary,
+ secondary = Color(0xFF0096C7),
+ tertiary = Color(0xFF00B4D8),
+ background = Color(0xFFF0F8FF),
+ surface = Color.White
+)
+
+private val OceanDarkScheme = darkColorScheme(
+ primary = OceanPrimary,
+ secondary = Color(0xFF0096C7),
+ tertiary = Color(0xFF00B4D8),
+ background = Color(0xFF001F3F),
+ surface = Color(0xFF003D5C)
+)
+
+private val ForestLightScheme = lightColorScheme(
+ primary = ForestPrimary,
+ secondary = Color(0xFF40916C),
+ tertiary = Color(0xFF52B788),
+ background = Color(0xFFF1FAEE),
+ surface = Color.White
+)
+
+private val ForestDarkScheme = darkColorScheme(
+ primary = ForestPrimary,
+ secondary = Color(0xFF40916C),
+ tertiary = Color(0xFF52B788),
+ background = Color(0xFF1B4332),
+ surface = Color(0xFF2D6A4F)
+)
+
+private val SunsetLightScheme = lightColorScheme(
+ primary = SunsetPrimary,
+ secondary = Color(0xFFFFA500),
+ tertiary = Color(0xFFFFB703),
+ background = Color(0xFFFFF8F3),
+ surface = Color.White
+)
+
+private val SunsetDarkScheme = darkColorScheme(
+ primary = SunsetPrimary,
+ secondary = Color(0xFFFFA500),
+ tertiary = Color(0xFFFFB703),
+ background = Color(0xFF3D2817),
+ surface = Color(0xFF5C3D2E)
+)
+
+private val CyberpunkLightScheme = lightColorScheme(
+ primary = CyberpunkPrimary,
+ secondary = Color(0xFF00F5FF),
+ tertiary = Color(0xFFFFBE0B),
+ background = Color(0xFFF8F9FF),
+ surface = Color.White
+)
+
+private val CyberpunkDarkScheme = darkColorScheme(
+ primary = CyberpunkPrimary,
+ secondary = Color(0xFF00F5FF),
+ tertiary = Color(0xFFFFBE0B),
+ background = Color(0xFF0A0E27),
+ surface = Color(0xFF1A1F3A)
+)
+
+private val NeonLightScheme = lightColorScheme(
+ primary = NeonPrimary,
+ secondary = Color(0xFFFF006E),
+ tertiary = Color(0xFF00F5FF),
+ background = Color(0xFFFFFFF0),
+ surface = Color.White
+)
+
+private val NeonDarkScheme = darkColorScheme(
+ primary = NeonPrimary,
+ secondary = Color(0xFFFF006E),
+ tertiary = Color(0xFF00F5FF),
+ background = Color(0xFF0D0221),
+ surface = Color(0xFF1A0033)
+)
+
+// ========================================================================================
+// CUSTOM TYPOGRAPHY WITH FONT SCALING
+// ========================================================================================
+
+@Composable
+fun getScaledTypography(scale: Float): Typography {
+ return Typography(
+ displayLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Bold,
+ fontSize = (57 * scale).sp
+ ),
+ displayMedium = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Bold,
+ fontSize = (45 * scale).sp
+ ),
+ displaySmall = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Bold,
+ fontSize = (36 * scale).sp
+ ),
+ headlineLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Bold,
+ fontSize = (32 * scale).sp
+ ),
+ headlineMedium = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Bold,
+ fontSize = (28 * scale).sp
+ ),
+ headlineSmall = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Bold,
+ fontSize = (24 * scale).sp
+ ),
+ titleLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Bold,
+ fontSize = (22 * scale).sp
+ ),
+ titleMedium = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.SemiBold,
+ fontSize = (16 * scale).sp
+ ),
+ titleSmall = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.SemiBold,
+ fontSize = (14 * scale).sp
+ ),
+ bodyLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = (16 * scale).sp
+ ),
+ bodyMedium = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = (14 * scale).sp
+ ),
+ bodySmall = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = (12 * scale).sp
+ ),
+ labelLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.SemiBold,
+ fontSize = (14 * scale).sp
+ ),
+ labelMedium = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.SemiBold,
+ fontSize = (12 * scale).sp
+ ),
+ labelSmall = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.SemiBold,
+ fontSize = (11 * scale).sp
+ )
+ )
+}
+
+// ========================================================================================
+// THEME ENUM
+// ========================================================================================
+
+enum class AppThemeType {
+ LIGHT, DARK, DRACULA, VAMPIRE, OCEAN, FOREST, SUNSET, CYBERPUNK, NEON
+}
+
+// ========================================================================================
+// MAIN THEME COMPOSABLE
+// ========================================================================================
+
+@Composable
+fun InventoryManagerTheme(
+ themeType: AppThemeType = AppThemeType.LIGHT,
+ darkTheme: Boolean = isSystemInDarkTheme(),
+ fontScale: Float = 1.0f,
+ dynamicColor: Boolean = true,
+ content: @Composable () -> Unit
+) {
+ val colorScheme = when {
+ themeType == AppThemeType.LIGHT -> LightColorScheme
+ themeType == AppThemeType.DARK -> DarkColorScheme
+ themeType == AppThemeType.DRACULA -> if (darkTheme) DraculaDarkScheme else DraculaLightScheme
+ themeType == AppThemeType.VAMPIRE -> if (darkTheme) VampireDarkScheme else VampireLightScheme
+ themeType == AppThemeType.OCEAN -> if (darkTheme) OceanDarkScheme else OceanLightScheme
+ themeType == AppThemeType.FOREST -> if (darkTheme) ForestDarkScheme else ForestLightScheme
+ themeType == AppThemeType.SUNSET -> if (darkTheme) SunsetDarkScheme else SunsetLightScheme
+ themeType == AppThemeType.CYBERPUNK -> if (darkTheme) CyberpunkDarkScheme else CyberpunkLightScheme
+ themeType == AppThemeType.NEON -> if (darkTheme) NeonDarkScheme else NeonLightScheme
+ dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
+ val context = LocalContext.current
+ if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
+ }
+ darkTheme -> DarkColorScheme
+ else -> LightColorScheme
+ }
+
+ val scaledTypography = getScaledTypography(fontScale)
+
+ MaterialTheme(
+ colorScheme = colorScheme,
+ typography = scaledTypography,
+ content = content
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/samuel/inventorymanager/ui/theme/Type.kt b/app/src/main/java/com/samuel/inventorymanager/ui/theme/Type.kt
new file mode 100644
index 0000000..b906bb8
--- /dev/null
+++ b/app/src/main/java/com/samuel/inventorymanager/ui/theme/Type.kt
@@ -0,0 +1,34 @@
+package com.samuel.inventorymanager.ui.theme
+
+import androidx.compose.material3.Typography
+import androidx.compose.ui.text.TextStyle
+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
+val Typography = Typography(
+ bodyLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp,
+ 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
+ )
+ */
+)
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..07d5da9
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 0000000..2b068d1
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..6f3b755
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..6f3b755
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 0000000..c209e78
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..b2dfe3d
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 0000000..4f0f1d6
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..62b611d
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 0000000..948a307
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..1b9a695
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..28d4b77
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9287f50
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..aa7d642
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9126ae3
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..f8c6127
--- /dev/null
+++ b/app/src/main/res/values/colors.xml
@@ -0,0 +1,10 @@
+
+
+ #FFBB86FC
+ #FF6200EE
+ #FF3700B3
+ #FF03DAC5
+ #FF018786
+ #FF000000
+ #FFFFFFFF
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..cfae3cc
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ InventoryManager
+
\ No newline at end of file
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
new file mode 100644
index 0000000..d67f102
--- /dev/null
+++ b/app/src/main/res/values/themes.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml
new file mode 100644
index 0000000..4df9255
--- /dev/null
+++ b/app/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,13 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000..9ee9997
--- /dev/null
+++ b/app/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/provider_paths.xml b/app/src/main/res/xml/provider_paths.xml
new file mode 100644
index 0000000..9ac162a
--- /dev/null
+++ b/app/src/main/res/xml/provider_paths.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/test/java/com/Samuel/inventorymanager/ExampleUnitTest.kt b/app/src/test/java/com/Samuel/inventorymanager/ExampleUnitTest.kt
new file mode 100644
index 0000000..93dcfcd
--- /dev/null
+++ b/app/src/test/java/com/Samuel/inventorymanager/ExampleUnitTest.kt
@@ -0,0 +1,16 @@
+package com.samuel.inventorymanager
+
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 0000000..f7b5371
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,7 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+plugins {
+ alias(libs.plugins.android.application) apply false
+ alias(libs.plugins.kotlin.android) apply false
+ alias(libs.plugins.kotlin.compose) apply false
+ id("com.google.gms.google-services") version "4.4.4" apply false
+}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..20e2a01
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,23 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. For more details, visit
+# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
+# Enables namespacing of each library's R class so that its R class includes only the
+# resources declared in the library itself and none from the library's dependencies,
+# thereby reducing the size of the R class for that library
+android.nonTransitiveRClass=true
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
new file mode 100644
index 0000000..1c3d1c4
--- /dev/null
+++ b/gradle/libs.versions.toml
@@ -0,0 +1,32 @@
+[versions]
+agp = "8.13.0"
+kotlin = "2.0.21"
+coreKtx = "1.17.0"
+junit = "4.13.2"
+junitVersion = "1.3.0"
+espressoCore = "3.7.0"
+lifecycleRuntimeKtx = "2.9.4"
+activityCompose = "1.11.0"
+composeBom = "2024.09.00"
+
+[libraries]
+androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
+junit = { group = "junit", name = "junit", version.ref = "junit" }
+androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
+androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
+androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
+androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
+androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
+androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }
+androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
+androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
+androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
+androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
+androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
+androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
+
+[plugins]
+android-application = { id = "com.android.application", version.ref = "agp" }
+kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
+kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
+
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..e708b1c
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..e3612c0
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Tue Nov 04 18:37:18 EST 2025
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100644
index 0000000..4f906e0
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,185 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=`expr $i + 1`
+ done
+ case $i in
+ 0) set -- ;;
+ 1) set -- "$args0" ;;
+ 2) set -- "$args0" "$args1" ;;
+ 3) set -- "$args0" "$args1" "$args2" ;;
+ 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..107acd3
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/settings.gradle.kts b/settings.gradle.kts
new file mode 100644
index 0000000..fc48852
--- /dev/null
+++ b/settings.gradle.kts
@@ -0,0 +1,24 @@
+pluginManagement {
+ repositories {
+ google {
+ content {
+ includeGroupByRegex("com\\.android.*")
+ includeGroupByRegex("com\\.google.*")
+ includeGroupByRegex("androidx.*")
+ }
+ }
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+rootProject.name = "InventoryManager"
+include(":app")
+
\ No newline at end of file