Aplicacion terminada

This commit is contained in:
2026-07-31 13:58:56 +02:00
parent 2d185416c6
commit eb8cc806c1
16 changed files with 782 additions and 192 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="temurin-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
+2 -1
View File
@@ -39,7 +39,8 @@ Ensure your MariaDB database has the following tables:
-- Table for SAIs
CREATE TABLE SAIs (
id_sai INT AUTO_INCREMENT PRIMARY KEY,
ns VARCHAR(255) NOT NULL
ns VARCHAR(255) NOT NULL UNIQUE,
modelo VARCHAR(255)
);
-- Table for Batteries
@@ -1,25 +1,24 @@
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 sql = "SELECT id_sai, ns, modelo 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")))
sais.add(Sai(
id = resultSet.getInt("id_sai"),
ns = resultSet.getString("ns"),
modelo = resultSet.getString("modelo")
))
}
Result.success(sais)
} catch (e: SQLException) {
@@ -29,12 +28,32 @@ class SaiRepository {
}
}
suspend fun insert(ns: String): Result<Unit> = withContext(Dispatchers.IO) {
suspend fun isNsRegistered(ns: String): Result<Boolean> = withContext(Dispatchers.IO) {
val connection = MariaDbConnection.getConnection() ?: return@withContext Result.failure(Exception("db_connection_error"))
try {
val sql = "INSERT INTO SAIs (ns) VALUES (?)"
val sql = "SELECT COUNT(*) FROM SAIs WHERE ns = ?"
val statement = connection.prepareStatement(sql)
statement.setString(1, ns)
val resultSet = statement.executeQuery()
if (resultSet.next()) {
Result.success(resultSet.getInt(1) > 0)
} else {
Result.success(false)
}
} catch (e: SQLException) {
Result.failure(e)
} finally {
connection.close()
}
}
suspend fun insert(ns: String, modelo: 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, modelo) VALUES (?, ?)"
val statement = connection.prepareStatement(sql)
statement.setString(1, ns)
statement.setString(2, modelo)
if (statement.executeUpdate() > 0) Result.success(Unit)
else Result.failure(Exception("no_rows_inserted"))
} catch (e: SQLException) {
@@ -47,10 +66,11 @@ class SaiRepository {
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 sql = "UPDATE SAIs SET ns = ?, modelo = ? WHERE id_sai = ?"
val statement = connection.prepareStatement(sql)
statement.setString(1, sai.ns)
statement.setInt(2, sai.id)
statement.setString(2, sai.modelo)
statement.setInt(3, sai.id)
if (statement.executeUpdate() > 0) Result.success(Unit)
else Result.failure(Exception("update_failed"))
} catch (e: SQLException) {
@@ -78,12 +98,16 @@ class SaiRepository {
suspend fun getById(id: Int): Result<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 WHERE id_sai = ?"
val sql = "SELECT id_sai, ns, modelo FROM SAIs WHERE id_sai = ?"
val statement = connection.prepareStatement(sql)
statement.setInt(1, id)
val resultSet = statement.executeQuery()
if (resultSet.next()) {
Result.success(Sai(id = resultSet.getInt("id_sai"), ns = resultSet.getString("ns")))
Result.success(Sai(
id = resultSet.getInt("id_sai"),
ns = resultSet.getString("ns"),
modelo = resultSet.getString("modelo")
))
} else {
Result.failure(Exception("not_found"))
}
@@ -2,5 +2,6 @@ package com.example.saipp.data.model
data class Sai(
val id: Int = 0,
val ns: String
val ns: String,
val modelo: String? = null
)
@@ -2,19 +2,80 @@ 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.material.icons.Icons
import androidx.compose.material.icons.filled.QrCodeScanner
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.*
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 androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.example.saipp.R
import com.example.saipp.ui.scanner.BarcodeScannerView
@Composable
fun SearchBar(
query: String,
onQueryChange: (String) -> Unit,
modifier: Modifier = Modifier,
onScanClick: (() -> Unit)? = null
) {
OutlinedTextField(
value = query,
onValueChange = onQueryChange,
modifier = modifier.fillMaxWidth(),
placeholder = { Text(stringResource(R.string.placeholder_search)) },
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
trailingIcon = {
if (onScanClick != null) {
IconButton(onClick = onScanClick) {
Icon(
imageVector = Icons.Default.QrCodeScanner,
contentDescription = stringResource(R.string.scan_to_search)
)
}
}
},
singleLine = true,
shape = MaterialTheme.shapes.medium
)
}
@Composable
fun ScannerDialog(
onDismiss: () -> Unit,
onCodeScanned: (String) -> Unit
) {
Dialog(
onDismissRequest = onDismiss,
properties = DialogProperties(usePlatformDefaultWidth = false)
) {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Box(modifier = Modifier.fillMaxSize()) {
BarcodeScannerView(onBarcodeDetected = { code ->
onCodeScanned(code)
onDismiss()
})
TextButton(
onClick = onDismiss,
modifier = Modifier
.align(Alignment.TopStart)
.padding(16.dp)
) {
Text(stringResource(R.string.button_close), color = Color.White)
}
}
}
}
}
@Composable
fun ResultDialog(
@@ -1,9 +1,12 @@
package com.example.saipp.ui.management
import android.widget.Toast
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.CalendarMonth
import androidx.compose.material.icons.filled.QrCodeScanner
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
@@ -17,7 +20,10 @@ import com.example.saipp.data.SaiRepository
import com.example.saipp.data.model.Assignment
import com.example.saipp.data.model.Person
import com.example.saipp.data.model.Sai
import com.example.saipp.ui.ScannerDialog
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.*
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -32,8 +38,11 @@ fun AddAssignmentScreen(
var selectedSaiId by remember { mutableStateOf<Int?>(null) }
var selectedPersonId by remember { mutableStateOf<Int?>(null) }
var assignedDate by remember { mutableStateOf("") }
var returnDate by remember { mutableStateOf("") }
var assignedDateDisplay by remember { mutableStateOf("") }
var assignedDateDb by remember { mutableStateOf("") }
var returnDateDisplay by remember { mutableStateOf("") }
var returnDateDb by remember { mutableStateOf("") }
var sais by remember { mutableStateOf<List<Sai>>(emptyList()) }
var people by remember { mutableStateOf<List<Person>>(emptyList()) }
@@ -41,11 +50,71 @@ fun AddAssignmentScreen(
var saiExpanded by remember { mutableStateOf(false) }
var personExpanded by remember { mutableStateOf(false) }
var showAssignedDatePicker by remember { mutableStateOf(false) }
var showReturnDatePicker by remember { mutableStateOf(false) }
var showQuickScanner by remember { mutableStateOf(false) }
val assignedDatePickerState = rememberDatePickerState()
val returnDatePickerState = rememberDatePickerState()
LaunchedEffect(Unit) {
saiRepository.getAll().onSuccess { sais = it }
personRepository.getAll().onSuccess { people = it }
}
if (showAssignedDatePicker) {
DatePickerDialog(
onDismissRequest = { showAssignedDatePicker = false },
confirmButton = {
TextButton(onClick = {
assignedDatePickerState.selectedDateMillis?.let { millis ->
val date = Date(millis)
assignedDateDisplay = SimpleDateFormat("dd-MM-yyyy", Locale.getDefault()).format(date)
assignedDateDb = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(date)
}
showAssignedDatePicker = false
}) { Text("Aceptar") }
},
dismissButton = {
TextButton(onClick = { showAssignedDatePicker = false }) { Text("Cancelar") }
}
) { DatePicker(state = assignedDatePickerState) }
}
if (showReturnDatePicker) {
DatePickerDialog(
onDismissRequest = { showReturnDatePicker = false },
confirmButton = {
TextButton(onClick = {
returnDatePickerState.selectedDateMillis?.let { millis ->
val date = Date(millis)
returnDateDisplay = SimpleDateFormat("dd-MM-yyyy", Locale.getDefault()).format(date)
returnDateDb = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(date)
}
showReturnDatePicker = false
}) { Text("Aceptar") }
},
dismissButton = {
TextButton(onClick = { showReturnDatePicker = false }) { Text("Cancelar") }
}
) { DatePicker(state = returnDatePickerState) }
}
if (showQuickScanner) {
ScannerDialog(
onDismiss = { showQuickScanner = false },
onCodeScanned = { code ->
val matchingSai = sais.find { it.ns.equals(code, ignoreCase = true) }
if (matchingSai != null) {
selectedSaiId = matchingSai.id
} else {
Toast.makeText(context, "SAI no encontrado: $code", Toast.LENGTH_SHORT).show()
}
showQuickScanner = false
}
)
}
Scaffold(
topBar = {
TopAppBar(
@@ -75,6 +144,14 @@ fun AddAssignmentScreen(
onValueChange = {},
readOnly = true,
label = { Text(stringResource(R.string.label_sai)) },
leadingIcon = {
IconButton(onClick = { showQuickScanner = true }) {
Icon(
imageVector = Icons.Default.QrCodeScanner,
contentDescription = "Escanear SAI"
)
}
},
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = saiExpanded) },
modifier = Modifier.menuAnchor().fillMaxWidth()
)
@@ -129,19 +206,31 @@ fun AddAssignmentScreen(
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
value = assignedDate,
onValueChange = { assignedDate = it },
value = assignedDateDisplay,
onValueChange = {},
readOnly = true,
label = { Text(stringResource(R.string.label_assigned_date)) },
modifier = Modifier.fillMaxWidth()
trailingIcon = {
IconButton(onClick = { showAssignedDatePicker = true }) {
Icon(Icons.Default.CalendarMonth, contentDescription = "Seleccionar fecha")
}
},
modifier = Modifier.fillMaxWidth().clickable { showAssignedDatePicker = true }
)
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
value = returnDate,
onValueChange = { returnDate = it },
value = returnDateDisplay,
onValueChange = {},
readOnly = true,
label = { Text(stringResource(R.string.label_return_date)) },
modifier = Modifier.fillMaxWidth()
trailingIcon = {
IconButton(onClick = { showReturnDatePicker = true }) {
Icon(Icons.Default.CalendarMonth, contentDescription = "Seleccionar fecha")
}
},
modifier = Modifier.fillMaxWidth().clickable { showReturnDatePicker = true }
)
Spacer(modifier = Modifier.height(32.dp))
@@ -155,8 +244,8 @@ fun AddAssignmentScreen(
Assignment(
idSai = selectedSaiId!!,
idPersona = selectedPersonId!!,
assignedDate = assignedDate.takeIf { it.isNotBlank() },
returnDate = returnDate.takeIf { it.isNotBlank() }
assignedDate = assignedDateDb.takeIf { it.isNotBlank() },
returnDate = returnDateDb.takeIf { it.isNotBlank() }
)
).onSuccess {
Toast.makeText(context, "Asignación guardada", Toast.LENGTH_SHORT).show()
@@ -1,9 +1,12 @@
package com.example.saipp.ui.management
import android.widget.Toast
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.CalendarMonth
import androidx.compose.material.icons.filled.QrCodeScanner
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
@@ -15,7 +18,10 @@ import com.example.saipp.data.BatteryRepository
import com.example.saipp.data.SaiRepository
import com.example.saipp.data.model.Battery
import com.example.saipp.data.model.Sai
import com.example.saipp.ui.ScannerDialog
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.*
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -28,17 +34,64 @@ fun AddBatteryScreen(
val saiRepository = remember { SaiRepository() }
var brand by remember { mutableStateOf("") }
var installDate by remember { mutableStateOf("") }
var installDateDisplay by remember { mutableStateOf("") } // DD-MM-AAAA
var installDateDb by remember { mutableStateOf("") } // YYYY-MM-DD
var selectedSaiId by remember { mutableStateOf<Int?>(null) }
var sais by remember { mutableStateOf<List<Sai>>(emptyList()) }
var isSaving by remember { mutableStateOf(false) }
var expanded by remember { mutableStateOf(false) }
var showDatePicker by remember { mutableStateOf(false) }
var showQuickScanner by remember { mutableStateOf(false) }
val datePickerState = rememberDatePickerState()
LaunchedEffect(Unit) {
saiRepository.getAll().onSuccess { sais = it }
}
if (showDatePicker) {
DatePickerDialog(
onDismissRequest = { showDatePicker = false },
confirmButton = {
TextButton(onClick = {
datePickerState.selectedDateMillis?.let { millis ->
val date = Date(millis)
val displayFormat = SimpleDateFormat("dd-MM-yyyy", Locale.getDefault())
val dbFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
installDateDisplay = displayFormat.format(date)
installDateDb = dbFormat.format(date)
}
showDatePicker = false
}) {
Text("Aceptar")
}
},
dismissButton = {
TextButton(onClick = { showDatePicker = false }) {
Text("Cancelar")
}
}
) {
DatePicker(state = datePickerState)
}
}
if (showQuickScanner) {
ScannerDialog(
onDismiss = { showQuickScanner = false },
onCodeScanned = { code ->
val matchingSai = sais.find { it.ns.equals(code, ignoreCase = true) }
if (matchingSai != null) {
selectedSaiId = matchingSai.id
} else {
Toast.makeText(context, "SAI no encontrado: $code", Toast.LENGTH_SHORT).show()
}
showQuickScanner = false
}
)
}
Scaffold(
topBar = {
TopAppBar(
@@ -68,6 +121,14 @@ fun AddBatteryScreen(
onValueChange = {},
readOnly = true,
label = { Text(stringResource(R.string.label_sai)) },
leadingIcon = {
IconButton(onClick = { showQuickScanner = true }) {
Icon(
imageVector = Icons.Default.QrCodeScanner,
contentDescription = "Escanear SAI"
)
}
},
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
modifier = Modifier.menuAnchor().fillMaxWidth()
)
@@ -106,10 +167,18 @@ fun AddBatteryScreen(
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
value = installDate,
onValueChange = { installDate = it },
value = installDateDisplay,
onValueChange = {},
readOnly = true,
label = { Text(stringResource(R.string.label_install_date)) },
modifier = Modifier.fillMaxWidth()
trailingIcon = {
IconButton(onClick = { showDatePicker = true }) {
Icon(Icons.Default.CalendarMonth, contentDescription = "Seleccionar fecha")
}
},
modifier = Modifier
.fillMaxWidth()
.clickable { showDatePicker = true }
)
Spacer(modifier = Modifier.height(32.dp))
@@ -122,7 +191,7 @@ fun AddBatteryScreen(
Battery(
idSai = selectedSaiId,
brand = brand,
installDate = installDate.takeIf { it.isNotBlank() }
installDate = installDateDb.takeIf { it.isNotBlank() }
)
).onSuccess {
Toast.makeText(context, "Batería añadida", Toast.LENGTH_SHORT).show()
@@ -17,7 +17,11 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import com.example.saipp.data.AssignmentRepository
import com.example.saipp.data.model.Assignment
import com.example.saipp.ui.ScannerDialog
import com.example.saipp.ui.SearchBar
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.*
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -31,6 +35,8 @@ fun AssignmentListScreen(
val repository = remember { AssignmentRepository() }
var items by remember { mutableStateOf<List<Assignment>>(emptyList()) }
var isLoading by remember { mutableStateOf(true) }
var searchQuery by remember { mutableStateOf("") }
var showQuickScanner by remember { mutableStateOf(false) }
fun loadItems() {
scope.launch {
@@ -46,6 +52,25 @@ fun AssignmentListScreen(
loadItems()
}
val filteredItems = remember(items, searchQuery) {
items.filter {
it.personName?.contains(searchQuery, ignoreCase = true) ?: false ||
it.saiNs?.contains(searchQuery, ignoreCase = true) ?: false
}
}
fun formatDate(dbDate: String?): String {
if (dbDate == null) return "N/A"
return try {
val dbFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
val displayFormat = SimpleDateFormat("dd-MM-yyyy", Locale.getDefault())
val date = dbFormat.parse(dbDate)
if (date != null) displayFormat.format(date) else dbDate
} catch (e: Exception) {
dbDate
}
}
Scaffold(
topBar = {
TopAppBar(
@@ -63,25 +88,39 @@ fun AssignmentListScreen(
}
}
) { padding ->
Column(modifier = Modifier.fillMaxSize().padding(padding)) {
SearchBar(
query = searchQuery,
onQueryChange = { searchQuery = it },
onScanClick = { showQuickScanner = true },
modifier = Modifier.padding(16.dp)
)
if (showQuickScanner) {
ScannerDialog(
onDismiss = { showQuickScanner = false },
onCodeScanned = { code ->
searchQuery = code
showQuickScanner = false
}
)
}
if (isLoading) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Box(modifier = Modifier.weight(1f).fillMaxWidth(), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
} else {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(padding)
) {
items(items) { item ->
LazyColumn(modifier = Modifier.weight(1f).fillMaxWidth()) {
items(filteredItems) { item ->
ListItem(
headlineContent = { Text("${item.personName ?: "Persona desconocida"}") },
headlineContent = { Text(item.personName ?: "Persona desconocida") },
supportingContent = {
Column {
Text("SAI: ${item.saiNs ?: "N/A"}")
Text("Asignado: ${item.assignedDate ?: "N/A"}")
Text("Asignado: ${formatDate(item.assignedDate)}")
if (item.returnDate != null) {
Text("Devolución: ${item.returnDate}")
Text("Devolución: ${formatDate(item.returnDate)}")
}
}
},
@@ -106,3 +145,4 @@ fun AssignmentListScreen(
}
}
}
}
@@ -17,7 +17,10 @@ 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 com.example.saipp.ui.SearchBar
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.*
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -31,6 +34,7 @@ fun BatteryListScreen(
val repository = remember { BatteryRepository() }
var items by remember { mutableStateOf<List<Battery>>(emptyList()) }
var isLoading by remember { mutableStateOf(true) }
var searchQuery by remember { mutableStateOf("") }
fun loadItems() {
scope.launch {
@@ -46,6 +50,25 @@ fun BatteryListScreen(
loadItems()
}
val filteredItems = remember(items, searchQuery) {
items.filter {
it.brand?.contains(searchQuery, ignoreCase = true) ?: false ||
it.idSai?.toString()?.contains(searchQuery) ?: false
}
}
fun formatDate(dbDate: String?): String {
if (dbDate == null) return "N/A"
return try {
val dbFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
val displayFormat = SimpleDateFormat("dd-MM-yyyy", Locale.getDefault())
val date = dbFormat.parse(dbDate)
if (date != null) displayFormat.format(date) else dbDate
} catch (e: Exception) {
dbDate
}
}
Scaffold(
topBar = {
TopAppBar(
@@ -63,22 +86,25 @@ fun BatteryListScreen(
}
}
) { padding ->
Column(modifier = Modifier.fillMaxSize().padding(padding)) {
SearchBar(
query = searchQuery,
onQueryChange = { searchQuery = it },
modifier = Modifier.padding(16.dp)
)
if (isLoading) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Box(modifier = Modifier.weight(1f).fillMaxWidth(), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
} else {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(padding)
) {
items(items) { item ->
LazyColumn(modifier = Modifier.weight(1f).fillMaxWidth()) {
items(filteredItems) { item ->
ListItem(
headlineContent = { Text("Marca: ${item.brand ?: "Desconocida"}") },
supportingContent = {
Column {
Text("Instalación: ${item.installDate ?: "N/A"}")
Text("Instalación: ${formatDate(item.installDate)}")
item.idSai?.let { Text("SAI ID: $it") }
}
},
@@ -103,3 +129,4 @@ fun BatteryListScreen(
}
}
}
}
@@ -1,9 +1,12 @@
package com.example.saipp.ui.management
import android.widget.Toast
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.CalendarMonth
import androidx.compose.material.icons.filled.QrCodeScanner
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
@@ -17,7 +20,10 @@ import com.example.saipp.data.SaiRepository
import com.example.saipp.data.model.Assignment
import com.example.saipp.data.model.Person
import com.example.saipp.data.model.Sai
import com.example.saipp.ui.ScannerDialog
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.*
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -33,8 +39,11 @@ fun EditAssignmentScreen(
var selectedSaiId by remember { mutableStateOf<Int?>(null) }
var selectedPersonId by remember { mutableStateOf<Int?>(null) }
var assignedDate by remember { mutableStateOf("") }
var returnDate by remember { mutableStateOf("") }
var assignedDateDisplay by remember { mutableStateOf("") }
var assignedDateDb by remember { mutableStateOf("") }
var returnDateDisplay by remember { mutableStateOf("") }
var returnDateDb by remember { mutableStateOf("") }
var sais by remember { mutableStateOf<List<Sai>>(emptyList()) }
var people by remember { mutableStateOf<List<Person>>(emptyList()) }
@@ -43,6 +52,13 @@ fun EditAssignmentScreen(
var saiExpanded by remember { mutableStateOf(false) }
var personExpanded by remember { mutableStateOf(false) }
var showAssignedDatePicker by remember { mutableStateOf(false) }
var showReturnDatePicker by remember { mutableStateOf(false) }
var showQuickScanner by remember { mutableStateOf(false) }
val assignedDatePickerState = rememberDatePickerState()
val returnDatePickerState = rememberDatePickerState()
LaunchedEffect(assignmentId) {
val assignmentResult = repository.getById(assignmentId)
val saisResult = saiRepository.getAll()
@@ -52,8 +68,32 @@ fun EditAssignmentScreen(
val item = assignmentResult.getOrNull()!!
selectedSaiId = item.idSai
selectedPersonId = item.idPersona
assignedDate = item.assignedDate ?: ""
returnDate = item.returnDate ?: ""
val dbFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
val displayFormat = SimpleDateFormat("dd-MM-yyyy", Locale.getDefault())
item.assignedDate?.let { dateStr ->
try {
val date = dbFormat.parse(dateStr)
if (date != null) {
assignedDateDisplay = displayFormat.format(date)
assignedDateDb = dateStr
assignedDatePickerState.selectedDateMillis = date.time
}
} catch (e: Exception) {}
}
item.returnDate?.let { dateStr ->
try {
val date = dbFormat.parse(dateStr)
if (date != null) {
returnDateDisplay = displayFormat.format(date)
returnDateDb = dateStr
returnDatePickerState.selectedDateMillis = date.time
}
} catch (e: Exception) {}
}
sais = saisResult.getOrDefault(emptyList())
people = peopleResult.getOrDefault(emptyList())
isLoading = false
@@ -63,6 +103,59 @@ fun EditAssignmentScreen(
}
}
if (showAssignedDatePicker) {
DatePickerDialog(
onDismissRequest = { showAssignedDatePicker = false },
confirmButton = {
TextButton(onClick = {
assignedDatePickerState.selectedDateMillis?.let { millis ->
val date = Date(millis)
assignedDateDisplay = SimpleDateFormat("dd-MM-yyyy", Locale.getDefault()).format(date)
assignedDateDb = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(date)
}
showAssignedDatePicker = false
}) { Text("Aceptar") }
},
dismissButton = {
TextButton(onClick = { showAssignedDatePicker = false }) { Text("Cancelar") }
}
) { DatePicker(state = assignedDatePickerState) }
}
if (showReturnDatePicker) {
DatePickerDialog(
onDismissRequest = { showReturnDatePicker = false },
confirmButton = {
TextButton(onClick = {
returnDatePickerState.selectedDateMillis?.let { millis ->
val date = Date(millis)
returnDateDisplay = SimpleDateFormat("dd-MM-yyyy", Locale.getDefault()).format(date)
returnDateDb = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(date)
}
showReturnDatePicker = false
}) { Text("Aceptar") }
},
dismissButton = {
TextButton(onClick = { showReturnDatePicker = false }) { Text("Cancelar") }
}
) { DatePicker(state = returnDatePickerState) }
}
if (showQuickScanner) {
ScannerDialog(
onDismiss = { showQuickScanner = false },
onCodeScanned = { code ->
val matchingSai = sais.find { it.ns.equals(code, ignoreCase = true) }
if (matchingSai != null) {
selectedSaiId = matchingSai.id
} else {
Toast.makeText(context, "SAI no encontrado: $code", Toast.LENGTH_SHORT).show()
}
showQuickScanner = false
}
)
}
Scaffold(
topBar = {
TopAppBar(
@@ -97,6 +190,14 @@ fun EditAssignmentScreen(
onValueChange = {},
readOnly = true,
label = { Text(stringResource(R.string.label_sai)) },
leadingIcon = {
IconButton(onClick = { showQuickScanner = true }) {
Icon(
imageVector = Icons.Default.QrCodeScanner,
contentDescription = "Escanear SAI"
)
}
},
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = saiExpanded) },
modifier = Modifier.menuAnchor().fillMaxWidth()
)
@@ -151,19 +252,31 @@ fun EditAssignmentScreen(
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
value = assignedDate,
onValueChange = { assignedDate = it },
value = assignedDateDisplay,
onValueChange = {},
readOnly = true,
label = { Text(stringResource(R.string.label_assigned_date)) },
modifier = Modifier.fillMaxWidth()
trailingIcon = {
IconButton(onClick = { showAssignedDatePicker = true }) {
Icon(Icons.Default.CalendarMonth, contentDescription = "Seleccionar fecha")
}
},
modifier = Modifier.fillMaxWidth().clickable { showAssignedDatePicker = true }
)
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
value = returnDate,
onValueChange = { returnDate = it },
value = returnDateDisplay,
onValueChange = {},
readOnly = true,
label = { Text(stringResource(R.string.label_return_date)) },
modifier = Modifier.fillMaxWidth()
trailingIcon = {
IconButton(onClick = { showReturnDatePicker = true }) {
Icon(Icons.Default.CalendarMonth, contentDescription = "Seleccionar fecha")
}
},
modifier = Modifier.fillMaxWidth().clickable { showReturnDatePicker = true }
)
Spacer(modifier = Modifier.height(32.dp))
@@ -178,8 +291,8 @@ fun EditAssignmentScreen(
id = assignmentId,
idSai = selectedSaiId!!,
idPersona = selectedPersonId!!,
assignedDate = assignedDate.takeIf { it.isNotBlank() },
returnDate = returnDate.takeIf { it.isNotBlank() }
assignedDate = assignedDateDb.takeIf { it.isNotBlank() },
returnDate = returnDateDb.takeIf { it.isNotBlank() }
)
).onSuccess {
Toast.makeText(context, "Asignación actualizada", Toast.LENGTH_SHORT).show()
@@ -1,9 +1,12 @@
package com.example.saipp.ui.management
import android.widget.Toast
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.CalendarMonth
import androidx.compose.material.icons.filled.QrCodeScanner
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
@@ -15,7 +18,10 @@ import com.example.saipp.data.BatteryRepository
import com.example.saipp.data.SaiRepository
import com.example.saipp.data.model.Battery
import com.example.saipp.data.model.Sai
import com.example.saipp.ui.ScannerDialog
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.*
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -29,13 +35,18 @@ fun EditBatteryScreen(
val saiRepository = remember { SaiRepository() }
var brand by remember { mutableStateOf("") }
var installDate by remember { mutableStateOf("") }
var installDateDisplay by remember { mutableStateOf("") }
var installDateDb by remember { mutableStateOf("") }
var selectedSaiId by remember { mutableStateOf<Int?>(null) }
var sais by remember { mutableStateOf<List<Sai>>(emptyList()) }
var isLoading by remember { mutableStateOf(true) }
var isSaving by remember { mutableStateOf(false) }
var expanded by remember { mutableStateOf(false) }
var showDatePicker by remember { mutableStateOf(false) }
var showQuickScanner by remember { mutableStateOf(false) }
val datePickerState = rememberDatePickerState()
LaunchedEffect(batteryId) {
val batteryResult = batteryRepository.getById(batteryId)
@@ -44,7 +55,24 @@ fun EditBatteryScreen(
if (batteryResult.isSuccess) {
val battery = batteryResult.getOrNull()!!
brand = battery.brand ?: ""
installDate = battery.installDate ?: ""
// Format existing date for display
battery.installDate?.let { dbDate ->
try {
val dbFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
val displayFormat = SimpleDateFormat("dd-MM-yyyy", Locale.getDefault())
val date = dbFormat.parse(dbDate)
if (date != null) {
installDateDisplay = displayFormat.format(date)
installDateDb = dbDate
datePickerState.selectedDateMillis = date.time
}
} catch (e: Exception) {
installDateDisplay = dbDate
installDateDb = dbDate
}
}
selectedSaiId = battery.idSai
sais = saisResult.getOrDefault(emptyList())
isLoading = false
@@ -54,6 +82,48 @@ fun EditBatteryScreen(
}
}
if (showDatePicker) {
DatePickerDialog(
onDismissRequest = { showDatePicker = false },
confirmButton = {
TextButton(onClick = {
datePickerState.selectedDateMillis?.let { millis ->
val date = Date(millis)
val displayFormat = SimpleDateFormat("dd-MM-yyyy", Locale.getDefault())
val dbFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
installDateDisplay = displayFormat.format(date)
installDateDb = dbFormat.format(date)
}
showDatePicker = false
}) {
Text("Aceptar")
}
},
dismissButton = {
TextButton(onClick = { showDatePicker = false }) {
Text("Cancelar")
}
}
) {
DatePicker(state = datePickerState)
}
}
if (showQuickScanner) {
ScannerDialog(
onDismiss = { showQuickScanner = false },
onCodeScanned = { code ->
val matchingSai = sais.find { it.ns.equals(code, ignoreCase = true) }
if (matchingSai != null) {
selectedSaiId = matchingSai.id
} else {
Toast.makeText(context, "SAI no encontrado: $code", Toast.LENGTH_SHORT).show()
}
showQuickScanner = false
}
)
}
Scaffold(
topBar = {
TopAppBar(
@@ -88,6 +158,14 @@ fun EditBatteryScreen(
onValueChange = {},
readOnly = true,
label = { Text(stringResource(R.string.label_sai)) },
leadingIcon = {
IconButton(onClick = { showQuickScanner = true }) {
Icon(
imageVector = Icons.Default.QrCodeScanner,
contentDescription = "Escanear SAI"
)
}
},
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
modifier = Modifier.menuAnchor().fillMaxWidth()
)
@@ -126,10 +204,18 @@ fun EditBatteryScreen(
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
value = installDate,
onValueChange = { installDate = it },
value = installDateDisplay,
onValueChange = {},
readOnly = true,
label = { Text(stringResource(R.string.label_install_date)) },
modifier = Modifier.fillMaxWidth()
trailingIcon = {
IconButton(onClick = { showDatePicker = true }) {
Icon(Icons.Default.CalendarMonth, contentDescription = "Seleccionar fecha")
}
},
modifier = Modifier
.fillMaxWidth()
.clickable { showDatePicker = true }
)
Spacer(modifier = Modifier.height(32.dp))
@@ -143,7 +229,7 @@ fun EditBatteryScreen(
id = batteryId,
idSai = selectedSaiId,
brand = brand,
installDate = installDate.takeIf { it.isNotBlank() }
installDate = installDateDb.takeIf { it.isNotBlank() }
)
).onSuccess {
Toast.makeText(context, "Batería actualizada", Toast.LENGTH_SHORT).show()
@@ -8,7 +8,9 @@ import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.example.saipp.R
import com.example.saipp.data.SaiRepository
import com.example.saipp.data.model.Sai
import kotlinx.coroutines.launch
@@ -24,12 +26,14 @@ fun EditSaiScreen(
val repository = remember { SaiRepository() }
var ns by remember { mutableStateOf("") }
var modelo by remember { mutableStateOf("") }
var isLoading by remember { mutableStateOf(true) }
var isSaving by remember { mutableStateOf(false) }
LaunchedEffect(saiId) {
repository.getById(saiId).onSuccess {
ns = it.ns
modelo = it.modelo ?: ""
isLoading = false
}.onFailure {
Toast.makeText(context, "Error al cargar SAI", Toast.LENGTH_SHORT).show()
@@ -66,13 +70,20 @@ fun EditSaiScreen(
label = { Text("Número de Serie") },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
value = modelo,
onValueChange = { modelo = it },
label = { Text(stringResource(R.string.label_modelo)) },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(32.dp))
Button(
onClick = {
if (ns.isNotBlank()) {
isSaving = true
scope.launch {
repository.update(Sai(id = saiId, ns = ns))
repository.update(Sai(id = saiId, ns = ns, modelo = modelo))
.onSuccess {
Toast.makeText(context, "SAI actualizado", Toast.LENGTH_SHORT).show()
onNavigateBack()
@@ -17,6 +17,7 @@ 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 com.example.saipp.ui.SearchBar
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@@ -31,6 +32,7 @@ fun PersonListScreen(
val repository = remember { PersonRepository() }
var items by remember { mutableStateOf<List<Person>>(emptyList()) }
var isLoading by remember { mutableStateOf(true) }
var searchQuery by remember { mutableStateOf("") }
fun loadItems() {
scope.launch {
@@ -46,6 +48,10 @@ fun PersonListScreen(
loadItems()
}
val filteredItems = remember(items, searchQuery) {
items.filter { it.name.contains(searchQuery, ignoreCase = true) }
}
Scaffold(
topBar = {
TopAppBar(
@@ -63,17 +69,20 @@ fun PersonListScreen(
}
}
) { padding ->
Column(modifier = Modifier.fillMaxSize().padding(padding)) {
SearchBar(
query = searchQuery,
onQueryChange = { searchQuery = it },
modifier = Modifier.padding(16.dp)
)
if (isLoading) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Box(modifier = Modifier.weight(1f).fillMaxWidth(), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
} else {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(padding)
) {
items(items) { item ->
LazyColumn(modifier = Modifier.weight(1f).fillMaxWidth()) {
items(filteredItems) { item ->
ListItem(
headlineContent = { Text(item.name) },
trailingContent = {
@@ -97,3 +106,4 @@ fun PersonListScreen(
}
}
}
}
@@ -17,6 +17,8 @@ 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 com.example.saipp.ui.ScannerDialog
import com.example.saipp.ui.SearchBar
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@@ -31,6 +33,8 @@ fun SaiListScreen(
val repository = remember { SaiRepository() }
var sais by remember { mutableStateOf<List<Sai>>(emptyList()) }
var isLoading by remember { mutableStateOf(true) }
var searchQuery by remember { mutableStateOf("") }
var showQuickScanner by remember { mutableStateOf(false) }
fun loadSais() {
scope.launch {
@@ -46,6 +50,13 @@ fun SaiListScreen(
loadSais()
}
val filteredSais = remember(sais, searchQuery) {
sais.filter {
it.ns.contains(searchQuery, ignoreCase = true) ||
(it.modelo?.contains(searchQuery, ignoreCase = true) ?: false)
}
}
Scaffold(
topBar = {
TopAppBar(
@@ -63,19 +74,34 @@ fun SaiListScreen(
}
}
) { padding ->
Column(modifier = Modifier.fillMaxSize().padding(padding)) {
SearchBar(
query = searchQuery,
onQueryChange = { searchQuery = it },
modifier = Modifier.padding(16.dp),
onScanClick = { showQuickScanner = true }
)
if (showQuickScanner) {
ScannerDialog(
onDismiss = { showQuickScanner = false },
onCodeScanned = { code ->
searchQuery = code
showQuickScanner = false
}
)
}
if (isLoading) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Box(modifier = Modifier.weight(1f).fillMaxWidth(), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
} else {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(padding)
) {
items(sais) { sai ->
LazyColumn(modifier = Modifier.weight(1f).fillMaxWidth()) {
items(filteredSais) { sai ->
ListItem(
headlineContent = { Text("NS: ${sai.ns}") },
supportingContent = { Text("Modelo: ${sai.modelo ?: "N/A"}") },
trailingContent = {
Row {
IconButton(onClick = { onNavigateToEdit(sai.id) }) {
@@ -97,3 +123,4 @@ fun SaiListScreen(
}
}
}
}
@@ -2,13 +2,10 @@ 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.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.*
@@ -16,9 +13,10 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.ui.ResultDialog
import com.example.saipp.R
import com.example.saipp.data.SaiRepository
import com.example.saipp.ui.scanner.BarcodeScannerView
import kotlinx.coroutines.launch
@@ -55,6 +53,7 @@ fun SaiScannerScreen(
}
var scannedBarcode by remember { mutableStateOf<String?>(null) }
var modelo by remember { mutableStateOf("") }
var isSaving by remember { mutableStateOf(false) }
Scaffold(
@@ -91,21 +90,49 @@ fun SaiScannerScreen(
}
scannedBarcode?.let { barcode ->
ResultDialog(
barcode = barcode,
onDismiss = { scannedBarcode = null },
onConfirm = {
AlertDialog(
onDismissRequest = { scannedBarcode = null },
title = { Text(text = stringResource(R.string.dialog_title)) },
text = {
Column {
Text(text = "Código: $barcode")
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
value = modelo,
onValueChange = { modelo = it },
label = { Text(stringResource(R.string.label_modelo)) },
modifier = Modifier.fillMaxWidth()
)
}
},
confirmButton = {
Button(onClick = {
val codeToSave = scannedBarcode!!
scannedBarcode = null
isSaving = true
scope.launch {
repository.insert(barcode)
// Check for duplicates
val isRegisteredResult = repository.isNsRegistered(codeToSave)
if (isRegisteredResult.getOrDefault(false)) {
Toast.makeText(context, R.string.error_duplicate_sai, Toast.LENGTH_LONG).show()
} else {
repository.insert(codeToSave, modelo)
.onSuccess {
Toast.makeText(context, "SAI guardado", Toast.LENGTH_SHORT).show()
onNavigateBack()
}
.onFailure { Toast.makeText(context, "Error: ${it.message}", Toast.LENGTH_SHORT).show() }
}
isSaving = false
}
}) {
Text(stringResource(R.string.button_save))
}
},
dismissButton = {
TextButton(onClick = { scannedBarcode = null }) {
Text(stringResource(R.string.button_close))
}
}
)
}
+7 -3
View File
@@ -15,9 +15,13 @@
<string name="no_rows_inserted">No se insertaron filas en la base de datos</string>
<string name="label_sai">SAI</string>
<string name="label_brand">Marca</string>
<string name="label_install_date">Fecha Instalación (AAAA-MM-DD HH:MM:SS)</string>
<string name="label_modelo">Modelo</string>
<string name="label_install_date">Fecha Instalación (DD-MM-AAAA)</string>
<string name="select_sai">Seleccionar SAI</string>
<string name="no_sai">Ninguno</string>
<string name="placeholder_search">Buscar...</string>
<string name="error_duplicate_sai">Este SAI ya está registrado en el sistema.</string>
<string name="scan_to_search">Escanear para buscar</string>
<string name="menu_sais">Gestión de SAIs</string>
<string name="menu_batteries">Gestión de Baterías</string>
@@ -25,7 +29,7 @@
<string name="menu_assignments">Gestión de Asignaciones</string>
<string name="label_person">Persona</string>
<string name="label_assigned_date">Fecha Asignación (AAAA-MM-DD)</string>
<string name="label_return_date">Fecha Devolución (AAAA-MM-DD)</string>
<string name="label_assigned_date">Fecha Asignación (DD-MM-AAAA)</string>
<string name="label_return_date">Fecha Devolución (DD-MM-AAAA)</string>
<string name="select_person">Seleccionar Persona</string>
</resources>