Añadir Menu Gestion de SAIS Gestion de baterias y gestion de personas
This commit is contained in:
@@ -4,8 +4,10 @@ Android application developed in Kotlin using Jetpack Compose that scans barcode
|
|||||||
|
|
||||||
## 🚀 Features
|
## 🚀 Features
|
||||||
|
|
||||||
- **Instant Scan**: The camera opens automatically upon application startup.
|
- **Instant Scan**: The camera opens when adding a new SAI.
|
||||||
- **Connection Status**: Real-time indicator at the top showing if the app can reach the MariaDB server.
|
- **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.
|
- **Visual Overlay**: Centered scanning guide with a red line to help align barcodes.
|
||||||
- **ML Kit Integration**: High-performance barcode detection (HD 720p analysis).
|
- **ML Kit Integration**: High-performance barcode detection (HD 720p analysis).
|
||||||
- **Confirmation Flow**: Scanned results are shown in a popup dialog before saving.
|
- **Confirmation Flow**: Scanned results are shown in a popup dialog before saving.
|
||||||
@@ -31,13 +33,27 @@ The app features a professional scanning interface:
|
|||||||
## ⚙️ Configuration
|
## ⚙️ Configuration
|
||||||
|
|
||||||
### 1. Database Setup
|
### 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
|
```sql
|
||||||
CREATE TABLE scans (
|
-- Table for SAIs
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
CREATE TABLE SAIs (
|
||||||
barcode VARCHAR(255) NOT NULL,
|
id_sai INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
|
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
|
||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ dependencies {
|
|||||||
implementation(platform(libs.androidx.compose.bom))
|
implementation(platform(libs.androidx.compose.bom))
|
||||||
implementation(libs.androidx.activity.compose)
|
implementation(libs.androidx.activity.compose)
|
||||||
implementation(libs.androidx.compose.material3)
|
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)
|
||||||
implementation(libs.androidx.compose.ui.graphics)
|
implementation(libs.androidx.compose.ui.graphics)
|
||||||
implementation(libs.androidx.compose.ui.tooling.preview)
|
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||||
|
|||||||
@@ -1,50 +1,11 @@
|
|||||||
package com.example.saipp
|
package com.example.saipp
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import com.example.saipp.ui.AppNavigation
|
||||||
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.theme.SAIppTheme
|
import com.example.saipp.ui.theme.SAIppTheme
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@@ -52,181 +13,8 @@ class MainActivity : ComponentActivity() {
|
|||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
setContent {
|
setContent {
|
||||||
SAIppTheme {
|
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
|
package com.example.saipp.data
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import java.sql.Connection
|
import java.sql.Connection
|
||||||
import java.sql.DriverManager
|
import java.sql.DriverManager
|
||||||
import java.sql.SQLException
|
import java.sql.SQLException
|
||||||
@@ -34,4 +36,18 @@ object MariaDbConnection {
|
|||||||
null
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ composeBom = "2026.02.01"
|
|||||||
cameraX = "1.6.1"
|
cameraX = "1.6.1"
|
||||||
mlkitBarcode = "17.3.0"
|
mlkitBarcode = "17.3.0"
|
||||||
mariadbDriver = "3.5.1"
|
mariadbDriver = "3.5.1"
|
||||||
|
navigationCompose = "2.9.8"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
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-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
|
||||||
androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
||||||
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
|
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-core = { group = "androidx.camera", name = "camera-core", version.ref = "cameraX" }
|
||||||
androidx-camera-camera2 = { group = "androidx.camera", name = "camera-camera2", 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" }
|
androidx-camera-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", version.ref = "cameraX" }
|
||||||
|
|||||||
Reference in New Issue
Block a user