Añadir Menu Gestion de SAIS Gestion de baterias y gestion de personas

This commit is contained in:
2026-07-31 09:27:31 +02:00
parent 3cd4fc4b53
commit 2fc7ad3f52
21 changed files with 1095 additions and 278 deletions
+23 -7
View File
@@ -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
);
```
+2
View File
@@ -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)
@@ -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<Result<Unit>?>(null) }
var isCheckingConnection by remember { mutableStateOf(true) }
LaunchedEffect(key1 = true) {
isCheckingConnection = true
connectionStatus = repository.testConnection()
isCheckingConnection = false
}
var scannedBarcode by remember { mutableStateOf<String?>(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<Unit>?
) {
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
)
}
}
}
@@ -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<List<Battery>> = 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<Battery>()
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<Unit> = 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<Unit> = 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<Unit> = 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()
}
}
}
@@ -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<Unit> = 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"))
}
}
}
@@ -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<List<Person>> = 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<Person>()
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<Unit> = 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<Unit> = 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<Unit> = 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()
}
}
}
@@ -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<List<Sai>> = 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<Sai>()
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<Unit> = 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<Unit> = 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<Unit> = 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()
}
}
}
@@ -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<Unit> = 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<Unit> = 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"))
}
}
}
@@ -0,0 +1,7 @@
package com.example.saipp.data.model
data class Battery(
val id: Int = 0,
val model: String,
val status: String
)
@@ -0,0 +1,7 @@
package com.example.saipp.data.model
data class Person(
val id: Int = 0,
val name: String,
val position: String
)
@@ -0,0 +1,6 @@
package com.example.saipp.data.model
data class Sai(
val id: Int = 0,
val ns: String
)
@@ -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() }
)
}
}
}
@@ -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<Unit>?
) {
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
)
}
}
}
@@ -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")
}
}
}
}
@@ -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")
}
}
}
}
@@ -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<List<Battery>>(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()
}
}
}
}
}
@@ -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<Result<Unit>?>(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)
}
}
@@ -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<List<Person>>(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()
}
}
}
}
}
@@ -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<List<Sai>>(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()
}
}
}
}
}
@@ -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<String?>(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
}
}
)
}
}
}
}
+3
View File
@@ -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" }