diff --git a/README.md b/README.md index 8f55839..5dda6bd 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,10 @@ Android application developed in Kotlin using Jetpack Compose that scans barcode ## 🚀 Features -- **Instant Scan**: The camera opens automatically upon application startup. -- **Connection Status**: Real-time indicator at the top showing if the app can reach the MariaDB server. +- **Instant Scan**: The camera opens when adding a new SAI. +- **Multi-Entity Management**: Dedicated sections for SAIs, Batteries, and People. +- **CRUD Operations**: Add, View, and Delete records for all managed entities. +- **Connection Status**: Real-time indicator on the home screen. - **Visual Overlay**: Centered scanning guide with a red line to help align barcodes. - **ML Kit Integration**: High-performance barcode detection (HD 720p analysis). - **Confirmation Flow**: Scanned results are shown in a popup dialog before saving. @@ -31,13 +33,27 @@ The app features a professional scanning interface: ## ⚙️ Configuration ### 1. Database Setup -Ensure your MariaDB database has a table named `scans`. You can use the following SQL: +Ensure your MariaDB database has the following tables: ```sql -CREATE TABLE scans ( - id INT AUTO_INCREMENT PRIMARY KEY, - barcode VARCHAR(255) NOT NULL, - timestamp DATETIME DEFAULT CURRENT_TIMESTAMP +-- Table for SAIs +CREATE TABLE SAIs ( + id_sai INT AUTO_INCREMENT PRIMARY KEY, + ns VARCHAR(255) NOT NULL +); + +-- Table for Batteries +CREATE TABLE baterias ( + id_bateria INT AUTO_INCREMENT PRIMARY KEY, + modelo VARCHAR(255) NOT NULL, + estado VARCHAR(255) NOT NULL +); + +-- Table for People +CREATE TABLE personas ( + id_persona INT AUTO_INCREMENT PRIMARY KEY, + nombre VARCHAR(255) NOT NULL, + puesto VARCHAR(255) NOT NULL ); ``` diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c853d6e..15b454a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -46,6 +46,8 @@ dependencies { implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.activity.compose) implementation(libs.androidx.compose.material3) + implementation(libs.androidx.compose.material.icons.extended) + implementation(libs.androidx.navigation.compose) implementation(libs.androidx.compose.ui) implementation(libs.androidx.compose.ui.graphics) implementation(libs.androidx.compose.ui.tooling.preview) diff --git a/app/src/main/java/com/example/saipp/MainActivity.kt b/app/src/main/java/com/example/saipp/MainActivity.kt index 59f197f..34648f9 100644 --- a/app/src/main/java/com/example/saipp/MainActivity.kt +++ b/app/src/main/java/com/example/saipp/MainActivity.kt @@ -1,50 +1,11 @@ package com.example.saipp -import android.Manifest -import android.content.pm.PackageManager import android.os.Bundle -import android.util.Log -import android.widget.Toast import androidx.activity.ComponentActivity -import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.foundation.layout.Box -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.shape.CircleShape -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Button -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Surface -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.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.Color -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.core.content.ContextCompat -import com.example.saipp.data.ScannerRepository -import com.example.saipp.ui.scanner.BarcodeScannerView +import com.example.saipp.ui.AppNavigation import com.example.saipp.ui.theme.SAIppTheme -import kotlinx.coroutines.launch class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -52,181 +13,8 @@ class MainActivity : ComponentActivity() { enableEdgeToEdge() setContent { SAIppTheme { - MainScreen() + AppNavigation() } } } } - -@Composable -fun MainScreen() { - val context = LocalContext.current - val scope = rememberCoroutineScope() - val repository = remember { ScannerRepository() } - - var hasCameraPermission by remember { - mutableStateOf( - ContextCompat.checkSelfPermission( - context, - Manifest.permission.CAMERA - ) == PackageManager.PERMISSION_GRANTED - ) - } - - val launcher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.RequestPermission(), - onResult = { granted -> - hasCameraPermission = granted - } - ) - - LaunchedEffect(key1 = true) { - if (!hasCameraPermission) { - launcher.launch(Manifest.permission.CAMERA) - } - } - - var connectionStatus by remember { mutableStateOf?>(null) } - var isCheckingConnection by remember { mutableStateOf(true) } - - LaunchedEffect(key1 = true) { - isCheckingConnection = true - connectionStatus = repository.testConnection() - isCheckingConnection = false - } - - var scannedBarcode by remember { mutableStateOf(null) } - var isSaving by remember { mutableStateOf(false) } - - Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> - Box( - modifier = Modifier - .fillMaxSize() - .padding(innerPadding) - ) { - if (hasCameraPermission) { - // Only scan if no barcode is currently being shown in the popup - if (scannedBarcode == null) { - BarcodeScannerView(onBarcodeDetected = { barcode -> - Log.d("MainActivity", "Barcode detected: $barcode") - scannedBarcode = barcode - }) - } - - // Connection Status Indicator at the top - ConnectionIndicator( - modifier = Modifier - .align(Alignment.TopCenter) - .padding(16.dp), - isChecking = isCheckingConnection, - status = connectionStatus - ) - - if (isSaving) { - CircularProgressIndicator( - modifier = Modifier.align(Alignment.Center) - ) - } - } else { - Text( - text = stringResource(R.string.camera_permission_required), - modifier = Modifier.align(Alignment.Center) - ) - } -// ... rest of the code - - // Popup Dialog for scanned results - scannedBarcode?.let { barcode -> - ResultDialog( - barcode = barcode, - onDismiss = { scannedBarcode = null }, - onConfirm = { - scannedBarcode = null - isSaving = true - scope.launch { - val result = repository.saveBarcode(barcode) - isSaving = false - result.onSuccess { - Toast.makeText(context, R.string.save_success, Toast.LENGTH_SHORT).show() - } - result.onFailure { e -> - val errorMessage = when (e.message) { - "connection_failed" -> context.getString(R.string.db_connection_failed) - "db_connection_error" -> context.getString(R.string.db_connection_error) - "no_rows_inserted" -> context.getString(R.string.no_rows_inserted) - else -> e.message ?: "Unknown error" - } - Toast.makeText(context, context.getString(R.string.save_error, errorMessage), Toast.LENGTH_LONG).show() - } - } - } - ) - } - } - } -} - -@Composable -fun ResultDialog( - barcode: String, - onDismiss: () -> Unit, - onConfirm: () -> Unit -) { - AlertDialog( - onDismissRequest = onDismiss, - title = { Text(text = stringResource(R.string.dialog_title)) }, - text = { Text(text = stringResource(R.string.dialog_message, barcode)) }, - confirmButton = { - Button(onClick = onConfirm) { - Text(stringResource(R.string.button_save)) - } - }, - dismissButton = { - TextButton(onClick = onDismiss) { - Text(stringResource(R.string.button_close)) - } - } - ) -} - -@Composable -fun ConnectionIndicator( - modifier: Modifier = Modifier, - isChecking: Boolean, - status: Result? -) { - Surface( - modifier = modifier, - color = Color.Black.copy(alpha = 0.6f), - shape = CircleShape - ) { - Row( - modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp), - verticalAlignment = Alignment.CenterVertically - ) { - val color = when { - isChecking -> Color.Gray - status?.isSuccess == true -> Color.Green - else -> Color.Red - } - - Surface( - modifier = Modifier.size(8.dp), - color = color, - shape = CircleShape - ) {} - - Spacer(modifier = Modifier.width(8.dp)) - - Text( - text = when { - isChecking -> stringResource(R.string.checking_server) - status?.isSuccess == true -> stringResource(R.string.connected_status) - else -> stringResource(R.string.disconnected_status) - }, - color = Color.White, - style = MaterialTheme.typography.bodySmall - ) - } - } -} diff --git a/app/src/main/java/com/example/saipp/data/BatteryRepository.kt b/app/src/main/java/com/example/saipp/data/BatteryRepository.kt new file mode 100644 index 0000000..8797b67 --- /dev/null +++ b/app/src/main/java/com/example/saipp/data/BatteryRepository.kt @@ -0,0 +1,78 @@ +package com.example.saipp.data + +import com.example.saipp.data.model.Battery +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.sql.SQLException + +class BatteryRepository { + suspend fun getAll(): Result> = withContext(Dispatchers.IO) { + val connection = MariaDbConnection.getConnection() ?: return@withContext Result.failure(Exception("db_connection_error")) + try { + val sql = "SELECT id_bateria, modelo, estado FROM baterias" + val statement = connection.prepareStatement(sql) + val resultSet = statement.executeQuery() + val items = mutableListOf() + while (resultSet.next()) { + items.add(Battery( + id = resultSet.getInt("id_bateria"), + model = resultSet.getString("modelo"), + status = resultSet.getString("estado") + )) + } + Result.success(items) + } catch (e: SQLException) { + Result.failure(e) + } finally { + connection.close() + } + } + + suspend fun insert(item: Battery): Result = withContext(Dispatchers.IO) { + val connection = MariaDbConnection.getConnection() ?: return@withContext Result.failure(Exception("db_connection_error")) + try { + val sql = "INSERT INTO baterias (modelo, estado) VALUES (?, ?)" + val statement = connection.prepareStatement(sql) + statement.setString(1, item.model) + statement.setString(2, item.status) + if (statement.executeUpdate() > 0) Result.success(Unit) + else Result.failure(Exception("no_rows_inserted")) + } catch (e: SQLException) { + Result.failure(e) + } finally { + connection.close() + } + } + + suspend fun update(item: Battery): Result = withContext(Dispatchers.IO) { + val connection = MariaDbConnection.getConnection() ?: return@withContext Result.failure(Exception("db_connection_error")) + try { + val sql = "UPDATE baterias SET modelo = ?, estado = ? WHERE id_bateria = ?" + val statement = connection.prepareStatement(sql) + statement.setString(1, item.model) + statement.setString(2, item.status) + statement.setInt(3, item.id) + if (statement.executeUpdate() > 0) Result.success(Unit) + else Result.failure(Exception("update_failed")) + } catch (e: SQLException) { + Result.failure(e) + } finally { + connection.close() + } + } + + suspend fun delete(id: Int): Result = withContext(Dispatchers.IO) { + val connection = MariaDbConnection.getConnection() ?: return@withContext Result.failure(Exception("db_connection_error")) + try { + val sql = "DELETE FROM baterias WHERE id_bateria = ?" + val statement = connection.prepareStatement(sql) + statement.setInt(1, id) + if (statement.executeUpdate() > 0) Result.success(Unit) + else Result.failure(Exception("delete_failed")) + } catch (e: SQLException) { + Result.failure(e) + } finally { + connection.close() + } + } +} diff --git a/app/src/main/java/com/example/saipp/data/MariaDbConnection.kt b/app/src/main/java/com/example/saipp/data/MariaDbConnection.kt index 83f9bf2..8f0ca60 100644 --- a/app/src/main/java/com/example/saipp/data/MariaDbConnection.kt +++ b/app/src/main/java/com/example/saipp/data/MariaDbConnection.kt @@ -1,6 +1,8 @@ package com.example.saipp.data import android.util.Log +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import java.sql.Connection import java.sql.DriverManager import java.sql.SQLException @@ -34,4 +36,18 @@ object MariaDbConnection { null } } + + suspend fun testConnection(): Result = withContext(Dispatchers.IO) { + val connection = getConnection() + if (connection != null) { + try { + connection.close() + Result.success(Unit) + } catch (e: SQLException) { + Result.failure(e) + } + } else { + Result.failure(Exception("connection_failed")) + } + } } diff --git a/app/src/main/java/com/example/saipp/data/PersonRepository.kt b/app/src/main/java/com/example/saipp/data/PersonRepository.kt new file mode 100644 index 0000000..3f704ad --- /dev/null +++ b/app/src/main/java/com/example/saipp/data/PersonRepository.kt @@ -0,0 +1,78 @@ +package com.example.saipp.data + +import com.example.saipp.data.model.Person +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.sql.SQLException + +class PersonRepository { + suspend fun getAll(): Result> = withContext(Dispatchers.IO) { + val connection = MariaDbConnection.getConnection() ?: return@withContext Result.failure(Exception("db_connection_error")) + try { + val sql = "SELECT id_persona, nombre, puesto FROM personas" + val statement = connection.prepareStatement(sql) + val resultSet = statement.executeQuery() + val items = mutableListOf() + while (resultSet.next()) { + items.add(Person( + id = resultSet.getInt("id_persona"), + name = resultSet.getString("nombre"), + position = resultSet.getString("puesto") + )) + } + Result.success(items) + } catch (e: SQLException) { + Result.failure(e) + } finally { + connection.close() + } + } + + suspend fun insert(item: Person): Result = withContext(Dispatchers.IO) { + val connection = MariaDbConnection.getConnection() ?: return@withContext Result.failure(Exception("db_connection_error")) + try { + val sql = "INSERT INTO personas (nombre, puesto) VALUES (?, ?)" + val statement = connection.prepareStatement(sql) + statement.setString(1, item.name) + statement.setString(2, item.position) + if (statement.executeUpdate() > 0) Result.success(Unit) + else Result.failure(Exception("no_rows_inserted")) + } catch (e: SQLException) { + Result.failure(e) + } finally { + connection.close() + } + } + + suspend fun update(item: Person): Result = withContext(Dispatchers.IO) { + val connection = MariaDbConnection.getConnection() ?: return@withContext Result.failure(Exception("db_connection_error")) + try { + val sql = "UPDATE personas SET nombre = ?, puesto = ? WHERE id_persona = ?" + val statement = connection.prepareStatement(sql) + statement.setString(1, item.name) + statement.setString(2, item.position) + statement.setInt(3, item.id) + if (statement.executeUpdate() > 0) Result.success(Unit) + else Result.failure(Exception("update_failed")) + } catch (e: SQLException) { + Result.failure(e) + } finally { + connection.close() + } + } + + suspend fun delete(id: Int): Result = withContext(Dispatchers.IO) { + val connection = MariaDbConnection.getConnection() ?: return@withContext Result.failure(Exception("db_connection_error")) + try { + val sql = "DELETE FROM personas WHERE id_persona = ?" + val statement = connection.prepareStatement(sql) + statement.setInt(1, id) + if (statement.executeUpdate() > 0) Result.success(Unit) + else Result.failure(Exception("delete_failed")) + } catch (e: SQLException) { + Result.failure(e) + } finally { + connection.close() + } + } +} diff --git a/app/src/main/java/com/example/saipp/data/SaiRepository.kt b/app/src/main/java/com/example/saipp/data/SaiRepository.kt new file mode 100644 index 0000000..e684cda --- /dev/null +++ b/app/src/main/java/com/example/saipp/data/SaiRepository.kt @@ -0,0 +1,77 @@ +package com.example.saipp.data + +import android.util.Log +import com.example.saipp.data.model.Sai +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.sql.SQLException + +class SaiRepository { + companion object { + private const val TAG = "SaiRepository" + } + + suspend fun getAll(): Result> = withContext(Dispatchers.IO) { + val connection = MariaDbConnection.getConnection() ?: return@withContext Result.failure(Exception("db_connection_error")) + try { + val sql = "SELECT id_sai, ns FROM SAIs" + val statement = connection.prepareStatement(sql) + val resultSet = statement.executeQuery() + val sais = mutableListOf() + while (resultSet.next()) { + sais.add(Sai(id = resultSet.getInt("id_sai"), ns = resultSet.getString("ns"))) + } + Result.success(sais) + } catch (e: SQLException) { + Result.failure(e) + } finally { + connection.close() + } + } + + suspend fun insert(ns: String): Result = withContext(Dispatchers.IO) { + val connection = MariaDbConnection.getConnection() ?: return@withContext Result.failure(Exception("db_connection_error")) + try { + val sql = "INSERT INTO SAIs (ns) VALUES (?)" + val statement = connection.prepareStatement(sql) + statement.setString(1, ns) + if (statement.executeUpdate() > 0) Result.success(Unit) + else Result.failure(Exception("no_rows_inserted")) + } catch (e: SQLException) { + Result.failure(e) + } finally { + connection.close() + } + } + + suspend fun update(sai: Sai): Result = withContext(Dispatchers.IO) { + val connection = MariaDbConnection.getConnection() ?: return@withContext Result.failure(Exception("db_connection_error")) + try { + val sql = "UPDATE SAIs SET ns = ? WHERE id_sai = ?" + val statement = connection.prepareStatement(sql) + statement.setString(1, sai.ns) + statement.setInt(2, sai.id) + if (statement.executeUpdate() > 0) Result.success(Unit) + else Result.failure(Exception("update_failed")) + } catch (e: SQLException) { + Result.failure(e) + } finally { + connection.close() + } + } + + suspend fun delete(id: Int): Result = withContext(Dispatchers.IO) { + val connection = MariaDbConnection.getConnection() ?: return@withContext Result.failure(Exception("db_connection_error")) + try { + val sql = "DELETE FROM SAIs WHERE id_sai = ?" + val statement = connection.prepareStatement(sql) + statement.setInt(1, id) + if (statement.executeUpdate() > 0) Result.success(Unit) + else Result.failure(Exception("delete_failed")) + } catch (e: SQLException) { + Result.failure(e) + } finally { + connection.close() + } + } +} diff --git a/app/src/main/java/com/example/saipp/data/ScannerRepository.kt b/app/src/main/java/com/example/saipp/data/ScannerRepository.kt deleted file mode 100644 index 0427e8a..0000000 --- a/app/src/main/java/com/example/saipp/data/ScannerRepository.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.example.saipp.data - -import android.util.Log -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import java.sql.SQLException - -class ScannerRepository { - companion object { - private const val TAG = "ScannerRepository" - } - - suspend fun saveBarcode(barcode: String): Result = withContext(Dispatchers.IO) { - val connection = MariaDbConnection.getConnection() - if (connection == null) { - return@withContext Result.failure(Exception("db_connection_error")) - } - - try { - // TODO: Customize table and column names - val sql = "INSERT INTO scans (barcode, timestamp) VALUES (?, CURRENT_TIMESTAMP)" - val statement = connection.prepareStatement(sql) - statement.setString(1, barcode) - - val rowsInserted = statement.executeUpdate() - if (rowsInserted > 0) { - Log.d(TAG, "Barcode saved successfully: $barcode") - Result.success(Unit) - } else { - Result.failure(Exception("no_rows_inserted")) - } - } catch (e: SQLException) { - Log.e(TAG, "Database error: ${e.message}", e) - Result.failure(e) - } finally { - try { - connection.close() - } catch (e: SQLException) { - Log.e(TAG, "Error closing connection: ${e.message}", e) - } - } - } - - suspend fun testConnection(): Result = withContext(Dispatchers.IO) { - val connection = MariaDbConnection.getConnection() - if (connection != null) { - try { - connection.close() - Result.success(Unit) - } catch (e: SQLException) { - Result.failure(e) - } - } else { - Result.failure(Exception("connection_failed")) - } - } -} diff --git a/app/src/main/java/com/example/saipp/data/model/Battery.kt b/app/src/main/java/com/example/saipp/data/model/Battery.kt new file mode 100644 index 0000000..9a0bc3c --- /dev/null +++ b/app/src/main/java/com/example/saipp/data/model/Battery.kt @@ -0,0 +1,7 @@ +package com.example.saipp.data.model + +data class Battery( + val id: Int = 0, + val model: String, + val status: String +) diff --git a/app/src/main/java/com/example/saipp/data/model/Person.kt b/app/src/main/java/com/example/saipp/data/model/Person.kt new file mode 100644 index 0000000..536a9a3 --- /dev/null +++ b/app/src/main/java/com/example/saipp/data/model/Person.kt @@ -0,0 +1,7 @@ +package com.example.saipp.data.model + +data class Person( + val id: Int = 0, + val name: String, + val position: String +) diff --git a/app/src/main/java/com/example/saipp/data/model/Sai.kt b/app/src/main/java/com/example/saipp/data/model/Sai.kt new file mode 100644 index 0000000..bdcf3fc --- /dev/null +++ b/app/src/main/java/com/example/saipp/data/model/Sai.kt @@ -0,0 +1,6 @@ +package com.example.saipp.data.model + +data class Sai( + val id: Int = 0, + val ns: String +) diff --git a/app/src/main/java/com/example/saipp/ui/AppNavigation.kt b/app/src/main/java/com/example/saipp/ui/AppNavigation.kt new file mode 100644 index 0000000..38b8011 --- /dev/null +++ b/app/src/main/java/com/example/saipp/ui/AppNavigation.kt @@ -0,0 +1,65 @@ +package com.example.saipp.ui + +import androidx.compose.runtime.Composable +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import com.example.saipp.ui.management.* + +sealed class Screen(val route: String) { + object Home : Screen("home") + object SaiList : Screen("sai_list") + object SaiScanner : Screen("sai_scanner") + object BatteryList : Screen("battery_list") + object AddBattery : Screen("add_battery") + object PersonList : Screen("person_list") + object AddPerson : Screen("add_person") +} + +@Composable +fun AppNavigation() { + val navController = rememberNavController() + + NavHost(navController = navController, startDestination = Screen.Home.route) { + composable(Screen.Home.route) { + HomeScreen( + onNavigateToSais = { navController.navigate(Screen.SaiList.route) }, + onNavigateToBatteries = { navController.navigate(Screen.BatteryList.route) }, + onNavigateToPeople = { navController.navigate(Screen.PersonList.route) } + ) + } + composable(Screen.SaiList.route) { + SaiListScreen( + onNavigateBack = { navController.popBackStack() }, + onNavigateToScan = { navController.navigate(Screen.SaiScanner.route) } + ) + } + composable(Screen.SaiScanner.route) { + SaiScannerScreen( + onNavigateBack = { navController.popBackStack() } + ) + } + composable(Screen.BatteryList.route) { + BatteryListScreen( + onNavigateBack = { navController.popBackStack() }, + onNavigateToAdd = { navController.navigate(Screen.AddBattery.route) } + ) + } + composable(Screen.AddBattery.route) { + AddBatteryScreen( + onNavigateBack = { navController.popBackStack() } + ) + } + composable(Screen.PersonList.route) { + PersonListScreen( + onNavigateBack = { navController.popBackStack() }, + onNavigateToAdd = { navController.navigate(Screen.AddPerson.route) } + ) + } + composable(Screen.AddPerson.route) { + AddPersonScreen( + onNavigateBack = { navController.popBackStack() } + ) + } + } +} diff --git a/app/src/main/java/com/example/saipp/ui/Components.kt b/app/src/main/java/com/example/saipp/ui/Components.kt new file mode 100644 index 0000000..c8f190c --- /dev/null +++ b/app/src/main/java/com/example/saipp/ui/Components.kt @@ -0,0 +1,82 @@ +package com.example.saipp.ui + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +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.res.stringResource +import androidx.compose.ui.unit.dp +import com.example.saipp.R + +@Composable +fun ResultDialog( + barcode: String, + onDismiss: () -> Unit, + onConfirm: () -> Unit +) { + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(text = stringResource(R.string.dialog_title)) }, + text = { Text(text = stringResource(R.string.dialog_message, barcode)) }, + confirmButton = { + Button(onClick = onConfirm) { + Text(stringResource(R.string.button_save)) + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text(stringResource(R.string.button_close)) + } + } + ) +} + +@Composable +fun ConnectionIndicator( + modifier: Modifier = Modifier, + isChecking: Boolean, + status: Result? +) { + Surface( + modifier = modifier, + color = Color.Black.copy(alpha = 0.6f), + shape = CircleShape + ) { + Row( + modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp), + verticalAlignment = Alignment.CenterVertically + ) { + val color = when { + isChecking -> Color.Gray + status?.isSuccess == true -> Color.Green + else -> Color.Red + } + + Surface( + modifier = Modifier.size(8.dp), + color = color, + shape = CircleShape + ) {} + + Spacer(modifier = Modifier.width(8.dp)) + + Text( + text = when { + isChecking -> stringResource(R.string.checking_server) + status?.isSuccess == true -> stringResource(R.string.connected_status) + else -> stringResource(R.string.disconnected_status) + }, + color = Color.White, + style = MaterialTheme.typography.bodySmall + ) + } + } +} diff --git a/app/src/main/java/com/example/saipp/ui/management/AddBatteryScreen.kt b/app/src/main/java/com/example/saipp/ui/management/AddBatteryScreen.kt new file mode 100644 index 0000000..ad8ec53 --- /dev/null +++ b/app/src/main/java/com/example/saipp/ui/management/AddBatteryScreen.kt @@ -0,0 +1,84 @@ +package com.example.saipp.ui.management + +import android.widget.Toast +import androidx.compose.foundation.layout.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import com.example.saipp.data.BatteryRepository +import com.example.saipp.data.model.Battery +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AddBatteryScreen( + onNavigateBack: () -> Unit +) { + val context = LocalContext.current + val scope = rememberCoroutineScope() + val repository = remember { BatteryRepository() } + + var model by remember { mutableStateOf("") } + var status by remember { mutableStateOf("") } + var isSaving by remember { mutableStateOf(false) } + + Scaffold( + topBar = { + TopAppBar( + title = { Text("Añadir Batería") }, + navigationIcon = { + IconButton(onClick = onNavigateBack) { + Icon(Icons.Default.ArrowBack, contentDescription = "Atrás") + } + } + ) + } + ) { padding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(padding) + .padding(16.dp) + ) { + OutlinedTextField( + value = model, + onValueChange = { model = it }, + label = { Text("Modelo") }, + modifier = Modifier.fillMaxWidth() + ) + Spacer(modifier = Modifier.height(16.dp)) + OutlinedTextField( + value = status, + onValueChange = { status = it }, + label = { Text("Estado") }, + modifier = Modifier.fillMaxWidth() + ) + Spacer(modifier = Modifier.height(32.dp)) + Button( + onClick = { + if (model.isNotBlank() && status.isNotBlank()) { + isSaving = true + scope.launch { + repository.insert(Battery(model = model, status = status)) + .onSuccess { + Toast.makeText(context, "Batería añadida", Toast.LENGTH_SHORT).show() + onNavigateBack() + } + .onFailure { Toast.makeText(context, "Error: ${it.message}", Toast.LENGTH_SHORT).show() } + isSaving = false + } + } + }, + modifier = Modifier.fillMaxWidth(), + enabled = !isSaving + ) { + if (isSaving) CircularProgressIndicator(modifier = Modifier.size(24.dp)) + else Text("Guardar") + } + } + } +} diff --git a/app/src/main/java/com/example/saipp/ui/management/AddPersonScreen.kt b/app/src/main/java/com/example/saipp/ui/management/AddPersonScreen.kt new file mode 100644 index 0000000..905223a --- /dev/null +++ b/app/src/main/java/com/example/saipp/ui/management/AddPersonScreen.kt @@ -0,0 +1,84 @@ +package com.example.saipp.ui.management + +import android.widget.Toast +import androidx.compose.foundation.layout.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import com.example.saipp.data.PersonRepository +import com.example.saipp.data.model.Person +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AddPersonScreen( + onNavigateBack: () -> Unit +) { + val context = LocalContext.current + val scope = rememberCoroutineScope() + val repository = remember { PersonRepository() } + + var name by remember { mutableStateOf("") } + var position by remember { mutableStateOf("") } + var isSaving by remember { mutableStateOf(false) } + + Scaffold( + topBar = { + TopAppBar( + title = { Text("Añadir Persona") }, + navigationIcon = { + IconButton(onClick = onNavigateBack) { + Icon(Icons.Default.ArrowBack, contentDescription = "Atrás") + } + } + ) + } + ) { padding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(padding) + .padding(16.dp) + ) { + OutlinedTextField( + value = name, + onValueChange = { name = it }, + label = { Text("Nombre") }, + modifier = Modifier.fillMaxWidth() + ) + Spacer(modifier = Modifier.height(16.dp)) + OutlinedTextField( + value = position, + onValueChange = { position = it }, + label = { Text("Puesto") }, + modifier = Modifier.fillMaxWidth() + ) + Spacer(modifier = Modifier.height(32.dp)) + Button( + onClick = { + if (name.isNotBlank() && position.isNotBlank()) { + isSaving = true + scope.launch { + repository.insert(Person(name = name, position = position)) + .onSuccess { + Toast.makeText(context, "Persona añadida", Toast.LENGTH_SHORT).show() + onNavigateBack() + } + .onFailure { Toast.makeText(context, "Error: ${it.message}", Toast.LENGTH_SHORT).show() } + isSaving = false + } + } + }, + modifier = Modifier.fillMaxWidth(), + enabled = !isSaving + ) { + if (isSaving) CircularProgressIndicator(modifier = Modifier.size(24.dp)) + else Text("Guardar") + } + } + } +} diff --git a/app/src/main/java/com/example/saipp/ui/management/BatteryListScreen.kt b/app/src/main/java/com/example/saipp/ui/management/BatteryListScreen.kt new file mode 100644 index 0000000..f962386 --- /dev/null +++ b/app/src/main/java/com/example/saipp/ui/management/BatteryListScreen.kt @@ -0,0 +1,93 @@ +package com.example.saipp.ui.management + +import android.widget.Toast +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import com.example.saipp.data.BatteryRepository +import com.example.saipp.data.model.Battery +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun BatteryListScreen( + onNavigateBack: () -> Unit, + onNavigateToAdd: () -> Unit +) { + val context = LocalContext.current + val scope = rememberCoroutineScope() + val repository = remember { BatteryRepository() } + var items by remember { mutableStateOf>(emptyList()) } + var isLoading by remember { mutableStateOf(true) } + + fun loadItems() { + scope.launch { + isLoading = true + repository.getAll() + .onSuccess { items = it } + .onFailure { Toast.makeText(context, "Error: ${it.message}", Toast.LENGTH_SHORT).show() } + isLoading = false + } + } + + LaunchedEffect(Unit) { + loadItems() + } + + Scaffold( + topBar = { + TopAppBar( + title = { Text("Listado de Baterías") }, + navigationIcon = { + IconButton(onClick = onNavigateBack) { + Icon(Icons.Default.ArrowBack, contentDescription = "Atrás") + } + } + ) + }, + floatingActionButton = { + FloatingActionButton(onClick = onNavigateToAdd) { + Icon(Icons.Default.Add, contentDescription = "Añadir Batería") + } + } + ) { padding -> + if (isLoading) { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + CircularProgressIndicator() + } + } else { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(padding) + ) { + items(items) { item -> + ListItem( + headlineContent = { Text("Modelo: ${item.model}") }, + supportingContent = { Text("Estado: ${item.status}") }, + trailingContent = { + IconButton(onClick = { + scope.launch { + repository.delete(item.id).onSuccess { loadItems() } + } + }) { + Icon(Icons.Default.Delete, contentDescription = "Eliminar") + } + } + ) + HorizontalDivider() + } + } + } + } +} diff --git a/app/src/main/java/com/example/saipp/ui/management/HomeScreen.kt b/app/src/main/java/com/example/saipp/ui/management/HomeScreen.kt new file mode 100644 index 0000000..dd1d008 --- /dev/null +++ b/app/src/main/java/com/example/saipp/ui/management/HomeScreen.kt @@ -0,0 +1,89 @@ +package com.example.saipp.ui.management + +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.example.saipp.ui.ConnectionIndicator +import com.example.saipp.R +import com.example.saipp.data.MariaDbConnection + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun HomeScreen( + onNavigateToSais: () -> Unit, + onNavigateToBatteries: () -> Unit, + onNavigateToPeople: () -> Unit +) { + var connectionStatus by remember { mutableStateOf?>(null) } + var isCheckingConnection by remember { mutableStateOf(true) } + + LaunchedEffect(Unit) { + isCheckingConnection = true + connectionStatus = MariaDbConnection.testConnection() + isCheckingConnection = false + } + + Scaffold( + topBar = { + TopAppBar(title = { Text(stringResource(R.string.app_name)) }) + } + ) { padding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(padding) + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + ConnectionIndicator( + isChecking = isCheckingConnection, + status = connectionStatus + ) + + Spacer(modifier = Modifier.height(32.dp)) + + MenuButton( + text = "Gestión de SAIs", + onClick = onNavigateToSais, + modifier = Modifier.fillMaxWidth() + ) + + Spacer(modifier = Modifier.height(16.dp)) + + MenuButton( + text = "Gestión de Baterías", + onClick = onNavigateToBatteries, + modifier = Modifier.fillMaxWidth() + ) + + Spacer(modifier = Modifier.height(16.dp)) + + MenuButton( + text = "Gestión de Personas", + onClick = onNavigateToPeople, + modifier = Modifier.fillMaxWidth() + ) + } + } +} + +@Composable +fun MenuButton( + text: String, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + Button( + onClick = onClick, + modifier = modifier.height(64.dp), + shape = MaterialTheme.shapes.medium + ) { + Text(text = text, style = MaterialTheme.typography.titleMedium) + } +} diff --git a/app/src/main/java/com/example/saipp/ui/management/PersonListScreen.kt b/app/src/main/java/com/example/saipp/ui/management/PersonListScreen.kt new file mode 100644 index 0000000..7b88372 --- /dev/null +++ b/app/src/main/java/com/example/saipp/ui/management/PersonListScreen.kt @@ -0,0 +1,93 @@ +package com.example.saipp.ui.management + +import android.widget.Toast +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import com.example.saipp.data.PersonRepository +import com.example.saipp.data.model.Person +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PersonListScreen( + onNavigateBack: () -> Unit, + onNavigateToAdd: () -> Unit +) { + val context = LocalContext.current + val scope = rememberCoroutineScope() + val repository = remember { PersonRepository() } + var items by remember { mutableStateOf>(emptyList()) } + var isLoading by remember { mutableStateOf(true) } + + fun loadItems() { + scope.launch { + isLoading = true + repository.getAll() + .onSuccess { items = it } + .onFailure { Toast.makeText(context, "Error: ${it.message}", Toast.LENGTH_SHORT).show() } + isLoading = false + } + } + + LaunchedEffect(Unit) { + loadItems() + } + + Scaffold( + topBar = { + TopAppBar( + title = { Text("Listado de Personas") }, + navigationIcon = { + IconButton(onClick = onNavigateBack) { + Icon(Icons.Default.ArrowBack, contentDescription = "Atrás") + } + } + ) + }, + floatingActionButton = { + FloatingActionButton(onClick = onNavigateToAdd) { + Icon(Icons.Default.Add, contentDescription = "Añadir Persona") + } + } + ) { padding -> + if (isLoading) { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + CircularProgressIndicator() + } + } else { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(padding) + ) { + items(items) { item -> + ListItem( + headlineContent = { Text(item.name) }, + supportingContent = { Text(item.position) }, + trailingContent = { + IconButton(onClick = { + scope.launch { + repository.delete(item.id).onSuccess { loadItems() } + } + }) { + Icon(Icons.Default.Delete, contentDescription = "Eliminar") + } + } + ) + HorizontalDivider() + } + } + } + } +} diff --git a/app/src/main/java/com/example/saipp/ui/management/SaiListScreen.kt b/app/src/main/java/com/example/saipp/ui/management/SaiListScreen.kt new file mode 100644 index 0000000..1aef99b --- /dev/null +++ b/app/src/main/java/com/example/saipp/ui/management/SaiListScreen.kt @@ -0,0 +1,92 @@ +package com.example.saipp.ui.management + +import android.widget.Toast +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import com.example.saipp.data.SaiRepository +import com.example.saipp.data.model.Sai +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SaiListScreen( + onNavigateBack: () -> Unit, + onNavigateToScan: () -> Unit +) { + val context = LocalContext.current + val scope = rememberCoroutineScope() + val repository = remember { SaiRepository() } + var sais by remember { mutableStateOf>(emptyList()) } + var isLoading by remember { mutableStateOf(true) } + + fun loadSais() { + scope.launch { + isLoading = true + repository.getAll() + .onSuccess { sais = it } + .onFailure { Toast.makeText(context, "Error: ${it.message}", Toast.LENGTH_SHORT).show() } + isLoading = false + } + } + + LaunchedEffect(Unit) { + loadSais() + } + + Scaffold( + topBar = { + TopAppBar( + title = { Text("Listado de SAIs") }, + navigationIcon = { + IconButton(onClick = onNavigateBack) { + Icon(Icons.Default.ArrowBack, contentDescription = "Atrás") + } + } + ) + }, + floatingActionButton = { + FloatingActionButton(onClick = onNavigateToScan) { + Icon(Icons.Default.Add, contentDescription = "Añadir SAI") + } + } + ) { padding -> + if (isLoading) { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + CircularProgressIndicator() + } + } else { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(padding) + ) { + items(sais) { sai -> + ListItem( + headlineContent = { Text("NS: ${sai.ns}") }, + trailingContent = { + IconButton(onClick = { + scope.launch { + repository.delete(sai.id).onSuccess { loadSais() } + } + }) { + Icon(Icons.Default.Delete, contentDescription = "Eliminar") + } + } + ) + HorizontalDivider() + } + } + } + } +} diff --git a/app/src/main/java/com/example/saipp/ui/management/SaiScannerScreen.kt b/app/src/main/java/com/example/saipp/ui/management/SaiScannerScreen.kt new file mode 100644 index 0000000..cd8852c --- /dev/null +++ b/app/src/main/java/com/example/saipp/ui/management/SaiScannerScreen.kt @@ -0,0 +1,114 @@ +package com.example.saipp.ui.management + +import android.Manifest +import android.content.pm.PackageManager +import android.util.Log +import android.widget.Toast +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import androidx.core.content.ContextCompat +import com.example.saipp.ui.ResultDialog +import com.example.saipp.data.SaiRepository +import com.example.saipp.ui.scanner.BarcodeScannerView +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SaiScannerScreen( + onNavigateBack: () -> Unit +) { + val context = LocalContext.current + val scope = rememberCoroutineScope() + val repository = remember { SaiRepository() } + + var hasCameraPermission by remember { + mutableStateOf( + ContextCompat.checkSelfPermission( + context, + Manifest.permission.CAMERA + ) == PackageManager.PERMISSION_GRANTED + ) + } + + val launcher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestPermission(), + onResult = { granted -> + hasCameraPermission = granted + } + ) + + LaunchedEffect(Unit) { + if (!hasCameraPermission) { + launcher.launch(Manifest.permission.CAMERA) + } + } + + var scannedBarcode by remember { mutableStateOf(null) } + var isSaving by remember { mutableStateOf(false) } + + Scaffold( + topBar = { + TopAppBar( + title = { Text("Escanear SAI") }, + navigationIcon = { + IconButton(onClick = onNavigateBack) { + Icon(Icons.Default.ArrowBack, contentDescription = "Atrás") + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surface.copy(alpha = 0.5f) + ) + ) + } + ) { padding -> + Box(modifier = Modifier.fillMaxSize().padding(padding)) { + if (hasCameraPermission) { + if (scannedBarcode == null) { + BarcodeScannerView(onBarcodeDetected = { barcode -> + scannedBarcode = barcode + }) + } + + if (isSaving) { + CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) + } + } else { + Text( + text = "Se requiere permiso de cámara.", + modifier = Modifier.align(Alignment.Center) + ) + } + + scannedBarcode?.let { barcode -> + ResultDialog( + barcode = barcode, + onDismiss = { scannedBarcode = null }, + onConfirm = { + scannedBarcode = null + isSaving = true + scope.launch { + repository.insert(barcode) + .onSuccess { + Toast.makeText(context, "SAI guardado", Toast.LENGTH_SHORT).show() + onNavigateBack() + } + .onFailure { Toast.makeText(context, "Error: ${it.message}", Toast.LENGTH_SHORT).show() } + isSaving = false + } + } + ) + } + } + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c0c4bf5..bea09ad 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,6 +11,7 @@ composeBom = "2026.02.01" cameraX = "1.6.1" mlkitBarcode = "17.3.0" mariadbDriver = "3.5.1" +navigationCompose = "2.9.8" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -27,6 +28,8 @@ androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "u 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" } +androidx-compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" } +androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" } androidx-camera-core = { group = "androidx.camera", name = "camera-core", version.ref = "cameraX" } androidx-camera-camera2 = { group = "androidx.camera", name = "camera-camera2", version.ref = "cameraX" } androidx-camera-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", version.ref = "cameraX" }