diff --git a/README.md b/README.md
index 0859092..cc0e191 100644
--- a/README.md
+++ b/README.md
@@ -4,10 +4,12 @@ Android application developed in Kotlin using Jetpack Compose that scans barcode
## 馃殌 Features
-- **Instant Scan**: The camera opens when adding a new SAI.
-- **Multi-Entity Management**: Dedicated sections for SAIs, Batteries, People, and SAI-Person Assignments.
-- **CRUD Operations**: Add, View, and Delete records for all managed entities.
-- **Connection Status**: Real-time indicator on the home screen.
+- **Dashboard**: High-level overview of inventory stats (Total SAIs, available, etc.) on the home screen.
+- **Instant Scan & Quick Info**: Scan any SAI to see its details and current assignee immediately.
+- **Search & Filter**: Robust search bars across all lists with optional barcode scanning for SAIs.
+- **Smart Status Management**: Automatic SAI status updates (Available/Assigned) based on assignment records.
+- **Confirmation Safety**: Built-in confirmation dialogs for all delete operations.
+- **Modern UI**: Clean, card-based Material 3 interface with intuitive icons and native date pickers.
- **Visual Overlay**: Centered scanning guide with a red line to help align barcodes.
- **ML Kit Integration**: High-performance barcode detection (HD 720p analysis).
- **Confirmation Flow**: Scanned results are shown in a popup dialog before saving.
@@ -40,7 +42,8 @@ Ensure your MariaDB database has the following tables:
CREATE TABLE SAIs (
id_sai INT AUTO_INCREMENT PRIMARY KEY,
ns VARCHAR(255) NOT NULL UNIQUE,
- modelo VARCHAR(255)
+ modelo VARCHAR(255),
+ estado ENUM('Disponible', 'Asignado', 'En reparaci贸n', 'Baja') NOT NULL DEFAULT 'Disponible'
);
-- Table for Batteries
@@ -48,14 +51,15 @@ CREATE TABLE Bateria (
id_bateria INT AUTO_INCREMENT PRIMARY KEY,
id_sai INT,
marca VARCHAR(255),
- fecha_instalacion DATETIME,
+ fecha_instalacion DATE,
FOREIGN KEY (id_sai) REFERENCES SAIs(id_sai)
);
-- Table for People
CREATE TABLE Persona (
id_persona INT AUTO_INCREMENT PRIMARY KEY,
- nombre VARCHAR(255) NOT NULL
+ nombre VARCHAR(255) NOT NULL,
+ activo TINYINT(1) DEFAULT 1
);
-- Table for SAI - Person Assignments
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 15b454a..599f51f 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -52,6 +52,7 @@ dependencies {
implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.core.splashscreen)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.7.0")
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index d556a51..4f72ada 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -27,6 +27,16 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/example/saipp/MainActivity.kt b/app/src/main/java/com/example/saipp/MainActivity.kt
index 34648f9..bb02072 100644
--- a/app/src/main/java/com/example/saipp/MainActivity.kt
+++ b/app/src/main/java/com/example/saipp/MainActivity.kt
@@ -4,11 +4,13 @@ import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
+import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import com.example.saipp.ui.AppNavigation
import com.example.saipp.ui.theme.SAIppTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
+ installSplashScreen()
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
diff --git a/app/src/main/java/com/example/saipp/data/AssignmentRepository.kt b/app/src/main/java/com/example/saipp/data/AssignmentRepository.kt
index ed8a66c..ea781fa 100644
--- a/app/src/main/java/com/example/saipp/data/AssignmentRepository.kt
+++ b/app/src/main/java/com/example/saipp/data/AssignmentRepository.kt
@@ -4,7 +4,6 @@ import com.example.saipp.data.model.Assignment
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.sql.SQLException
-import java.sql.Types
class AssignmentRepository {
suspend fun getAll(): Result> = withContext(Dispatchers.IO) {
@@ -12,7 +11,7 @@ class AssignmentRepository {
try {
val sql = """
SELECT a.id_asignacion, a.id_sai, a.id_persona, a.fecha_asignado, a.fecha_devolucion, a.observaciones,
- s.ns as sai_ns, p.nombre as person_name
+ s.ns as sai_ns, p.nombre as person_name, p.activo as person_active
FROM Asignado a
LEFT JOIN SAIs s ON a.id_sai = s.id_sai
LEFT JOIN Persona p ON a.id_persona = p.id_persona
@@ -21,6 +20,9 @@ class AssignmentRepository {
val resultSet = statement.executeQuery()
val items = mutableListOf()
while (resultSet.next()) {
+ // We'll use the 'personName' to filter if person is active in the list screen
+ // OR we can add a flag to the Assignment model. I'll add 'person_active' check.
+ val isActive = resultSet.getInt("person_active") == 1
items.add(Assignment(
id = resultSet.getInt("id_asignacion"),
idSai = resultSet.getInt("id_sai"),
@@ -29,7 +31,7 @@ class AssignmentRepository {
returnDate = resultSet.getString("fecha_devolucion"),
observations = resultSet.getString("observaciones"),
saiNs = resultSet.getString("sai_ns"),
- personName = resultSet.getString("person_name")
+ personName = if (isActive) resultSet.getString("person_name") else "${resultSet.getString("person_name")} (Baja)"
))
}
Result.success(items)
@@ -77,7 +79,15 @@ class AssignmentRepository {
statement.setString(4, item.returnDate)
statement.setString(5, item.observations)
- if (statement.executeUpdate() > 0) Result.success(Unit)
+ if (statement.executeUpdate() > 0) {
+ // Update SAI status to 'Asignado'
+ val updateSaiSql = "UPDATE SAIs SET estado = 'Asignado' WHERE id_sai = ?"
+ val updateSaiStmt = connection.prepareStatement(updateSaiSql)
+ updateSaiStmt.setInt(1, item.idSai)
+ updateSaiStmt.executeUpdate()
+
+ Result.success(Unit)
+ }
else Result.failure(Exception("no_rows_inserted"))
} catch (e: SQLException) {
Result.failure(e)
@@ -98,7 +108,17 @@ class AssignmentRepository {
statement.setString(5, item.observations)
statement.setInt(6, item.id)
- if (statement.executeUpdate() > 0) Result.success(Unit)
+ if (statement.executeUpdate() > 0) {
+ // If return date is set, make SAI 'Disponible' again?
+ // Let's assume manual status management is better or auto-toggle if return date exists.
+ if (!item.returnDate.isNullOrBlank()) {
+ val updateSaiSql = "UPDATE SAIs SET estado = 'Disponible' WHERE id_sai = ?"
+ val updateSaiStmt = connection.prepareStatement(updateSaiSql)
+ updateSaiStmt.setInt(1, item.idSai)
+ updateSaiStmt.executeUpdate()
+ }
+ Result.success(Unit)
+ }
else Result.failure(Exception("update_failed"))
} catch (e: SQLException) {
Result.failure(e)
@@ -110,10 +130,25 @@ class AssignmentRepository {
suspend fun delete(id: Int): Result = withContext(Dispatchers.IO) {
val connection = MariaDbConnection.getConnection() ?: return@withContext Result.failure(Exception("db_connection_error"))
try {
+ // First get SAI id to free it
+ val getSaiSql = "SELECT id_sai FROM Asignado WHERE id_asignacion = ?"
+ val getSaiStmt = connection.prepareStatement(getSaiSql)
+ getSaiStmt.setInt(1, id)
+ val rs = getSaiStmt.executeQuery()
+ val saiId = if (rs.next()) rs.getInt("id_sai") else null
+
val sql = "DELETE FROM Asignado WHERE id_asignacion = ?"
val statement = connection.prepareStatement(sql)
statement.setInt(1, id)
- if (statement.executeUpdate() > 0) Result.success(Unit)
+ if (statement.executeUpdate() > 0) {
+ if (saiId != null) {
+ val updateSaiSql = "UPDATE SAIs SET estado = 'Disponible' WHERE id_sai = ?"
+ val updateSaiStmt = connection.prepareStatement(updateSaiSql)
+ updateSaiStmt.setInt(1, saiId)
+ updateSaiStmt.executeUpdate()
+ }
+ Result.success(Unit)
+ }
else Result.failure(Exception("delete_failed"))
} catch (e: SQLException) {
Result.failure(e)
diff --git a/app/src/main/java/com/example/saipp/data/BatteryRepository.kt b/app/src/main/java/com/example/saipp/data/BatteryRepository.kt
index 3437186..f15c1d2 100644
--- a/app/src/main/java/com/example/saipp/data/BatteryRepository.kt
+++ b/app/src/main/java/com/example/saipp/data/BatteryRepository.kt
@@ -39,7 +39,16 @@ class BatteryRepository {
statement.setString(2, item.brand)
statement.setString(3, item.installDate)
- if (statement.executeUpdate() > 0) Result.success(Unit)
+ if (statement.executeUpdate() > 0) {
+ // AUTO-UPDATE SAI STATUS: If SAI was in repair, make it available now that it has a new battery
+ if (item.idSai != null) {
+ val updateSaiSql = "UPDATE SAIs SET estado = 'Disponible' WHERE id_sai = ? AND estado = 'En reparaci贸n'"
+ val updateSaiStmt = connection.prepareStatement(updateSaiSql)
+ updateSaiStmt.setInt(1, item.idSai)
+ updateSaiStmt.executeUpdate()
+ }
+ Result.success(Unit)
+ }
else Result.failure(Exception("no_rows_inserted"))
} catch (e: SQLException) {
Result.failure(e)
@@ -58,7 +67,16 @@ class BatteryRepository {
statement.setString(3, item.installDate)
statement.setInt(4, item.id)
- if (statement.executeUpdate() > 0) Result.success(Unit)
+ if (statement.executeUpdate() > 0) {
+ // AUTO-UPDATE SAI STATUS: Same logic as insert
+ if (item.idSai != null) {
+ val updateSaiSql = "UPDATE SAIs SET estado = 'Disponible' WHERE id_sai = ? AND estado = 'En reparaci贸n'"
+ val updateSaiStmt = connection.prepareStatement(updateSaiSql)
+ updateSaiStmt.setInt(1, item.idSai)
+ updateSaiStmt.executeUpdate()
+ }
+ Result.success(Unit)
+ }
else Result.failure(Exception("update_failed"))
} catch (e: SQLException) {
Result.failure(e)
@@ -70,10 +88,26 @@ class BatteryRepository {
suspend fun delete(id: Int): Result = withContext(Dispatchers.IO) {
val connection = MariaDbConnection.getConnection() ?: return@withContext Result.failure(Exception("db_connection_error"))
try {
+ // First get SAI id to mark it as in repair
+ val getSaiSql = "SELECT id_sai FROM Bateria WHERE id_bateria = ?"
+ val getSaiStmt = connection.prepareStatement(getSaiSql)
+ getSaiStmt.setInt(1, id)
+ val rs = getSaiStmt.executeQuery()
+ val saiId = if (rs.next()) rs.getInt("id_sai").takeIf { !rs.wasNull() } else null
+
val sql = "DELETE FROM Bateria WHERE id_bateria = ?"
val statement = connection.prepareStatement(sql)
statement.setInt(1, id)
- if (statement.executeUpdate() > 0) Result.success(Unit)
+ if (statement.executeUpdate() > 0) {
+ // AUTO-UPDATE SAI STATUS: If battery record is removed, assume SAI is back in repair
+ if (saiId != null) {
+ val updateSaiSql = "UPDATE SAIs SET estado = 'En reparaci贸n' WHERE id_sai = ?"
+ val updateSaiStmt = connection.prepareStatement(updateSaiSql)
+ updateSaiStmt.setInt(1, saiId)
+ updateSaiStmt.executeUpdate()
+ }
+ Result.success(Unit)
+ }
else Result.failure(Exception("delete_failed"))
} catch (e: SQLException) {
Result.failure(e)
@@ -105,4 +139,33 @@ class BatteryRepository {
connection.close()
}
}
+
+ suspend fun getCount(): Result = withContext(Dispatchers.IO) {
+ val connection = MariaDbConnection.getConnection() ?: return@withContext Result.failure(Exception("db_connection_error"))
+ try {
+ val sql = "SELECT COUNT(*) FROM Bateria"
+ val statement = connection.prepareStatement(sql)
+ val rs = statement.executeQuery()
+ if (rs.next()) Result.success(rs.getInt(1)) else Result.success(0)
+ } catch (e: SQLException) {
+ Result.failure(e)
+ } finally {
+ connection.close()
+ }
+ }
+
+ suspend fun getExpiredCount(): Result = withContext(Dispatchers.IO) {
+ val connection = MariaDbConnection.getConnection() ?: return@withContext Result.failure(Exception("db_connection_error"))
+ try {
+ // Count batteries older than 2 years (730 days)
+ val sql = "SELECT COUNT(*) FROM Bateria WHERE fecha_instalacion < DATE_SUB(CURDATE(), INTERVAL 2 YEAR)"
+ val statement = connection.prepareStatement(sql)
+ val rs = statement.executeQuery()
+ if (rs.next()) Result.success(rs.getInt(1)) else Result.success(0)
+ } catch (e: SQLException) {
+ Result.failure(e)
+ } finally {
+ connection.close()
+ }
+ }
}
diff --git a/app/src/main/java/com/example/saipp/data/PersonRepository.kt b/app/src/main/java/com/example/saipp/data/PersonRepository.kt
index ee0f157..ef9ee78 100644
--- a/app/src/main/java/com/example/saipp/data/PersonRepository.kt
+++ b/app/src/main/java/com/example/saipp/data/PersonRepository.kt
@@ -9,14 +9,37 @@ class PersonRepository {
suspend fun getAll(): Result> = withContext(Dispatchers.IO) {
val connection = MariaDbConnection.getConnection() ?: return@withContext Result.failure(Exception("db_connection_error"))
try {
- val sql = "SELECT id_persona, nombre FROM Persona"
+ val sql = "SELECT id_persona, nombre, activo FROM Persona"
val statement = connection.prepareStatement(sql)
val resultSet = statement.executeQuery()
val items = mutableListOf()
while (resultSet.next()) {
items.add(Person(
id = resultSet.getInt("id_persona"),
- name = resultSet.getString("nombre")
+ name = resultSet.getString("nombre"),
+ activo = resultSet.getInt("activo") == 1
+ ))
+ }
+ Result.success(items)
+ } catch (e: SQLException) {
+ Result.failure(e)
+ } finally {
+ connection.close()
+ }
+ }
+
+ suspend fun getActive(): Result> = withContext(Dispatchers.IO) {
+ val connection = MariaDbConnection.getConnection() ?: return@withContext Result.failure(Exception("db_connection_error"))
+ try {
+ val sql = "SELECT id_persona, nombre, activo FROM Persona WHERE activo = 1"
+ val statement = connection.prepareStatement(sql)
+ val resultSet = statement.executeQuery()
+ val items = mutableListOf()
+ while (resultSet.next()) {
+ items.add(Person(
+ id = resultSet.getInt("id_persona"),
+ name = resultSet.getString("nombre"),
+ activo = true
))
}
Result.success(items)
@@ -30,7 +53,7 @@ class PersonRepository {
suspend fun insert(item: Person): Result = withContext(Dispatchers.IO) {
val connection = MariaDbConnection.getConnection() ?: return@withContext Result.failure(Exception("db_connection_error"))
try {
- val sql = "INSERT INTO Persona (nombre) VALUES (?)"
+ val sql = "INSERT INTO Persona (nombre, activo) VALUES (?, 1)"
val statement = connection.prepareStatement(sql)
statement.setString(1, item.name)
if (statement.executeUpdate() > 0) Result.success(Unit)
@@ -45,10 +68,27 @@ class PersonRepository {
suspend fun update(item: Person): Result = withContext(Dispatchers.IO) {
val connection = MariaDbConnection.getConnection() ?: return@withContext Result.failure(Exception("db_connection_error"))
try {
- val sql = "UPDATE Persona SET nombre = ? WHERE id_persona = ?"
+ val sql = "UPDATE Persona SET nombre = ?, activo = ? WHERE id_persona = ?"
val statement = connection.prepareStatement(sql)
statement.setString(1, item.name)
- statement.setInt(2, item.id)
+ statement.setInt(2, if (item.activo) 1 else 0)
+ 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 toggleStatus(id: Int, isActive: Boolean): Result = withContext(Dispatchers.IO) {
+ val connection = MariaDbConnection.getConnection() ?: return@withContext Result.failure(Exception("db_connection_error"))
+ try {
+ val sql = "UPDATE Persona SET activo = ? WHERE id_persona = ?"
+ val statement = connection.prepareStatement(sql)
+ statement.setInt(1, if (isActive) 1 else 0)
+ statement.setInt(2, id)
if (statement.executeUpdate() > 0) Result.success(Unit)
else Result.failure(Exception("update_failed"))
} catch (e: SQLException) {
@@ -76,14 +116,15 @@ class PersonRepository {
suspend fun getById(id: Int): Result = withContext(Dispatchers.IO) {
val connection = MariaDbConnection.getConnection() ?: return@withContext Result.failure(Exception("db_connection_error"))
try {
- val sql = "SELECT id_persona, nombre FROM Persona WHERE id_persona = ?"
+ val sql = "SELECT id_persona, nombre, activo FROM Persona WHERE id_persona = ?"
val statement = connection.prepareStatement(sql)
statement.setInt(1, id)
val resultSet = statement.executeQuery()
if (resultSet.next()) {
Result.success(Person(
id = resultSet.getInt("id_persona"),
- name = resultSet.getString("nombre")
+ name = resultSet.getString("nombre"),
+ activo = resultSet.getInt("activo") == 1
))
} else {
Result.failure(Exception("not_found"))
@@ -94,4 +135,18 @@ class PersonRepository {
connection.close()
}
}
+
+ suspend fun getActiveCount(): Result = withContext(Dispatchers.IO) {
+ val connection = MariaDbConnection.getConnection() ?: return@withContext Result.failure(Exception("db_connection_error"))
+ try {
+ val sql = "SELECT COUNT(*) FROM Persona WHERE activo = 1"
+ val statement = connection.prepareStatement(sql)
+ val rs = statement.executeQuery()
+ if (rs.next()) Result.success(rs.getInt(1)) else Result.success(0)
+ } catch (e: SQLException) {
+ Result.failure(e)
+ } finally {
+ connection.close()
+ }
+ }
}
diff --git a/app/src/main/java/com/example/saipp/data/SaiRepository.kt b/app/src/main/java/com/example/saipp/data/SaiRepository.kt
index 8cd7b4f..41ebeac 100644
--- a/app/src/main/java/com/example/saipp/data/SaiRepository.kt
+++ b/app/src/main/java/com/example/saipp/data/SaiRepository.kt
@@ -1,6 +1,7 @@
package com.example.saipp.data
import com.example.saipp.data.model.Sai
+import com.example.saipp.data.model.HistoryItem
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.sql.SQLException
@@ -9,7 +10,12 @@ class SaiRepository {
suspend fun getAll(): Result> = withContext(Dispatchers.IO) {
val connection = MariaDbConnection.getConnection() ?: return@withContext Result.failure(Exception("db_connection_error"))
try {
- val sql = "SELECT id_sai, ns, modelo FROM SAIs"
+ val sql = """
+ SELECT s.id_sai, s.ns, s.modelo, s.estado, p.nombre as person_name
+ FROM SAIs s
+ LEFT JOIN Asignado a ON s.id_sai = a.id_sai AND a.fecha_devolucion IS NULL
+ LEFT JOIN Persona p ON a.id_persona = p.id_persona
+ """.trimIndent()
val statement = connection.prepareStatement(sql)
val resultSet = statement.executeQuery()
val sais = mutableListOf()
@@ -17,7 +23,32 @@ class SaiRepository {
sais.add(Sai(
id = resultSet.getInt("id_sai"),
ns = resultSet.getString("ns"),
- modelo = resultSet.getString("modelo")
+ modelo = resultSet.getString("modelo"),
+ estado = resultSet.getString("estado") ?: "Disponible",
+ asignadoA = resultSet.getString("person_name")
+ ))
+ }
+ Result.success(sais)
+ } catch (e: SQLException) {
+ Result.failure(e)
+ } finally {
+ connection.close()
+ }
+ }
+
+ suspend fun getAvailable(): Result> = withContext(Dispatchers.IO) {
+ val connection = MariaDbConnection.getConnection() ?: return@withContext Result.failure(Exception("db_connection_error"))
+ try {
+ val sql = "SELECT id_sai, ns, modelo, estado FROM SAIs WHERE estado = 'Disponible'"
+ val statement = connection.prepareStatement(sql)
+ val resultSet = statement.executeQuery()
+ val sais = mutableListOf()
+ while (resultSet.next()) {
+ sais.add(Sai(
+ id = resultSet.getInt("id_sai"),
+ ns = resultSet.getString("ns"),
+ modelo = resultSet.getString("modelo"),
+ estado = resultSet.getString("estado")
))
}
Result.success(sais)
@@ -50,7 +81,7 @@ class SaiRepository {
suspend fun insert(ns: String, modelo: String?): Result = 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 sql = "INSERT INTO SAIs (ns, modelo, estado) VALUES (?, ?, 'En reparaci贸n')"
val statement = connection.prepareStatement(sql)
statement.setString(1, ns)
statement.setString(2, modelo)
@@ -66,11 +97,12 @@ class SaiRepository {
suspend fun update(sai: Sai): Result = withContext(Dispatchers.IO) {
val connection = MariaDbConnection.getConnection() ?: return@withContext Result.failure(Exception("db_connection_error"))
try {
- val sql = "UPDATE SAIs SET ns = ?, modelo = ? WHERE id_sai = ?"
+ val sql = "UPDATE SAIs SET ns = ?, modelo = ?, estado = ? WHERE id_sai = ?"
val statement = connection.prepareStatement(sql)
statement.setString(1, sai.ns)
statement.setString(2, sai.modelo)
- statement.setInt(3, sai.id)
+ statement.setString(3, sai.estado)
+ statement.setInt(4, sai.id)
if (statement.executeUpdate() > 0) Result.success(Unit)
else Result.failure(Exception("update_failed"))
} catch (e: SQLException) {
@@ -98,7 +130,13 @@ class SaiRepository {
suspend fun getById(id: Int): Result = withContext(Dispatchers.IO) {
val connection = MariaDbConnection.getConnection() ?: return@withContext Result.failure(Exception("db_connection_error"))
try {
- val sql = "SELECT id_sai, ns, modelo FROM SAIs WHERE id_sai = ?"
+ val sql = """
+ SELECT s.id_sai, s.ns, s.modelo, s.estado, p.nombre as person_name
+ FROM SAIs s
+ LEFT JOIN Asignado a ON s.id_sai = a.id_sai AND a.fecha_devolucion IS NULL
+ LEFT JOIN Persona p ON a.id_persona = p.id_persona
+ WHERE s.id_sai = ?
+ """.trimIndent()
val statement = connection.prepareStatement(sql)
statement.setInt(1, id)
val resultSet = statement.executeQuery()
@@ -106,7 +144,9 @@ class SaiRepository {
Result.success(Sai(
id = resultSet.getInt("id_sai"),
ns = resultSet.getString("ns"),
- modelo = resultSet.getString("modelo")
+ modelo = resultSet.getString("modelo"),
+ estado = resultSet.getString("estado") ?: "Disponible",
+ asignadoA = resultSet.getString("person_name")
))
} else {
Result.failure(Exception("not_found"))
@@ -117,4 +157,106 @@ class SaiRepository {
connection.close()
}
}
+
+ suspend fun getStats(): Result> = withContext(Dispatchers.IO) {
+ val connection = MariaDbConnection.getConnection() ?: return@withContext Result.failure(Exception("db_connection_error"))
+ try {
+ val totalSql = "SELECT COUNT(*) FROM SAIs"
+ val availableSql = "SELECT COUNT(*) FROM SAIs WHERE estado = 'Disponible'"
+
+ val totalStmt = connection.prepareStatement(totalSql)
+ val availableStmt = connection.prepareStatement(availableSql)
+
+ val totalRs = totalStmt.executeQuery()
+ val availableRs = availableStmt.executeQuery()
+
+ var total = 0
+ var available = 0
+
+ if (totalRs.next()) total = totalRs.getInt(1)
+ if (availableRs.next()) available = availableRs.getInt(1)
+
+ Result.success(Pair(total, available))
+ } catch (e: SQLException) {
+ Result.failure(e)
+ } finally {
+ connection.close()
+ }
+ }
+
+ suspend fun getByNs(ns: String): Result = withContext(Dispatchers.IO) {
+ val connection = MariaDbConnection.getConnection() ?: return@withContext Result.failure(Exception("db_connection_error"))
+ try {
+ val sql = """
+ SELECT s.id_sai, s.ns, s.modelo, s.estado, p.nombre as person_name
+ FROM SAIs s
+ LEFT JOIN Asignado a ON s.id_sai = a.id_sai AND a.fecha_devolucion IS NULL
+ LEFT JOIN Persona p ON a.id_persona = p.id_persona
+ WHERE s.ns = ?
+ """.trimIndent()
+ val statement = connection.prepareStatement(sql)
+ statement.setString(1, ns)
+ val resultSet = statement.executeQuery()
+ if (resultSet.next()) {
+ Result.success(Sai(
+ id = resultSet.getInt("id_sai"),
+ ns = resultSet.getString("ns"),
+ modelo = resultSet.getString("modelo"),
+ estado = resultSet.getString("estado") ?: "Disponible",
+ asignadoA = resultSet.getString("person_name")
+ ))
+ } else {
+ Result.failure(Exception("not_found"))
+ }
+ } catch (e: SQLException) {
+ Result.failure(e)
+ } finally {
+ connection.close()
+ }
+ }
+
+ suspend fun getHistory(id: Int): Result> = withContext(Dispatchers.IO) {
+ val connection = MariaDbConnection.getConnection() ?: return@withContext Result.failure(Exception("db_connection_error"))
+ try {
+ val items = mutableListOf()
+
+ // 1. Get assignments and returns
+ val assignSql = """
+ SELECT p.nombre, a.fecha_asignado, a.fecha_devolucion
+ FROM Asignado a
+ JOIN Persona p ON a.id_persona = p.id_persona
+ WHERE a.id_sai = ?
+ """.trimIndent()
+ val assignStmt = connection.prepareStatement(assignSql)
+ assignStmt.setInt(1, id)
+ val assignRs = assignStmt.executeQuery()
+ while (assignRs.next()) {
+ val name = assignRs.getString(1)
+ val start = assignRs.getString(2)
+ val end = assignRs.getString(3)
+
+ items.add(HistoryItem("Asignaci贸n", "Entregado a $name", start))
+ if (end != null) {
+ items.add(HistoryItem("Devoluci贸n", "Devuelto por $name", end))
+ }
+ }
+
+ // 2. Get battery changes
+ val batterySql = "SELECT marca, fecha_instalacion FROM Bateria WHERE id_sai = ?"
+ val batteryStmt = connection.prepareStatement(batterySql)
+ batteryStmt.setInt(1, id)
+ val batteryRs = batteryStmt.executeQuery()
+ while (batteryRs.next()) {
+ val brand = batteryRs.getString(1)
+ val date = batteryRs.getString(2)
+ items.add(HistoryItem("Mantenimiento", "Bater铆a $brand instalada", date))
+ }
+
+ Result.success(items.sortedByDescending { it.date ?: "" })
+ } catch (e: SQLException) {
+ Result.failure(e)
+ } finally {
+ connection.close()
+ }
+ }
}
diff --git a/app/src/main/java/com/example/saipp/data/model/HistoryItem.kt b/app/src/main/java/com/example/saipp/data/model/HistoryItem.kt
new file mode 100644
index 0000000..0416888
--- /dev/null
+++ b/app/src/main/java/com/example/saipp/data/model/HistoryItem.kt
@@ -0,0 +1,7 @@
+package com.example.saipp.data.model
+
+data class HistoryItem(
+ val type: String, // 'Alta', 'Asignaci贸n', 'Devoluci贸n', 'Mantenimiento'
+ val description: String,
+ val date: String?
+)
diff --git a/app/src/main/java/com/example/saipp/data/model/Person.kt b/app/src/main/java/com/example/saipp/data/model/Person.kt
index 3ff1cca..041a878 100644
--- a/app/src/main/java/com/example/saipp/data/model/Person.kt
+++ b/app/src/main/java/com/example/saipp/data/model/Person.kt
@@ -2,5 +2,6 @@ package com.example.saipp.data.model
data class Person(
val id: Int = 0,
- val name: String
+ val name: String,
+ val activo: Boolean = true
)
diff --git a/app/src/main/java/com/example/saipp/data/model/Sai.kt b/app/src/main/java/com/example/saipp/data/model/Sai.kt
index 786c977..f28e277 100644
--- a/app/src/main/java/com/example/saipp/data/model/Sai.kt
+++ b/app/src/main/java/com/example/saipp/data/model/Sai.kt
@@ -3,5 +3,7 @@ package com.example.saipp.data.model
data class Sai(
val id: Int = 0,
val ns: String,
- val modelo: String? = null
+ val modelo: String? = null,
+ val estado: String = "Disponible",
+ val asignadoA: String? = null
)
diff --git a/app/src/main/java/com/example/saipp/ui/AppNavigation.kt b/app/src/main/java/com/example/saipp/ui/AppNavigation.kt
index 8ab7697..6fa3d45 100644
--- a/app/src/main/java/com/example/saipp/ui/AppNavigation.kt
+++ b/app/src/main/java/com/example/saipp/ui/AppNavigation.kt
@@ -15,6 +15,9 @@ sealed class Screen(val route: String) {
object EditSai : Screen("edit_sai/{saiId}") {
fun createRoute(id: Int) = "edit_sai/$id"
}
+ object SaiDetail : Screen("sai_detail/{saiId}") {
+ fun createRoute(id: Int) = "sai_detail/$id"
+ }
object BatteryList : Screen("battery_list")
object AddBattery : Screen("add_battery")
object EditBattery : Screen("edit_battery/{batteryId}") {
@@ -30,6 +33,8 @@ sealed class Screen(val route: String) {
object EditAssignment : Screen("edit_assignment/{assignmentId}") {
fun createRoute(id: Int) = "edit_assignment/$id"
}
+ object AvailableSaiList : Screen("available_sai_list")
+ object QuickInfo : Screen("quick_info")
}
@Composable
@@ -42,7 +47,9 @@ fun AppNavigation() {
onNavigateToSais = { navController.navigate(Screen.SaiList.route) },
onNavigateToBatteries = { navController.navigate(Screen.BatteryList.route) },
onNavigateToPeople = { navController.navigate(Screen.PersonList.route) },
- onNavigateToAssignments = { navController.navigate(Screen.AssignmentList.route) }
+ onNavigateToAssignments = { navController.navigate(Screen.AssignmentList.route) },
+ onNavigateToAvailableSais = { navController.navigate(Screen.AvailableSaiList.route) },
+ onNavigateToQuickInfo = { navController.navigate(Screen.QuickInfo.route) }
)
}
@@ -51,13 +58,12 @@ fun AppNavigation() {
SaiListScreen(
onNavigateBack = { navController.popBackStack() },
onNavigateToScan = { navController.navigate(Screen.SaiScanner.route) },
- onNavigateToEdit = { id -> navController.navigate(Screen.EditSai.createRoute(id)) }
+ onNavigateToEdit = { id -> navController.navigate(Screen.EditSai.createRoute(id)) },
+ onNavigateToDetail = { id -> navController.navigate(Screen.SaiDetail.createRoute(id)) }
)
}
composable(Screen.SaiScanner.route) {
- SaiScannerScreen(
- onNavigateBack = { navController.popBackStack() }
- )
+ SaiScannerScreen(onNavigateBack = { navController.popBackStack() })
}
composable(
route = Screen.EditSai.route,
@@ -66,6 +72,13 @@ fun AppNavigation() {
val id = backStackEntry.arguments?.getInt("saiId") ?: 0
EditSaiScreen(saiId = id, onNavigateBack = { navController.popBackStack() })
}
+ composable(
+ route = Screen.SaiDetail.route,
+ arguments = listOf(navArgument("saiId") { type = NavType.IntType })
+ ) { backStackEntry ->
+ val id = backStackEntry.arguments?.getInt("saiId") ?: 0
+ SaiDetailScreen(saiId = id, onNavigateBack = { navController.popBackStack() })
+ }
// Batteries
composable(Screen.BatteryList.route) {
@@ -123,5 +136,11 @@ fun AppNavigation() {
val id = backStackEntry.arguments?.getInt("assignmentId") ?: 0
EditAssignmentScreen(assignmentId = id, onNavigateBack = { navController.popBackStack() })
}
+ composable(Screen.AvailableSaiList.route) {
+ AvailableSaiListScreen(onNavigateBack = { navController.popBackStack() })
+ }
+ composable(Screen.QuickInfo.route) {
+ QuickInfoScreen(onNavigateBack = { navController.popBackStack() })
+ }
}
}
diff --git a/app/src/main/java/com/example/saipp/ui/Components.kt b/app/src/main/java/com/example/saipp/ui/Components.kt
index f503717..8c44a52 100644
--- a/app/src/main/java/com/example/saipp/ui/Components.kt
+++ b/app/src/main/java/com/example/saipp/ui/Components.kt
@@ -100,6 +100,31 @@ fun ResultDialog(
)
}
+@Composable
+fun DeleteConfirmDialog(
+ onDismiss: () -> Unit,
+ onConfirm: () -> Unit
+) {
+ AlertDialog(
+ onDismissRequest = onDismiss,
+ title = { Text(stringResource(R.string.confirm_delete_title)) },
+ text = { Text(stringResource(R.string.confirm_delete_message)) },
+ confirmButton = {
+ Button(
+ onClick = onConfirm,
+ colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error)
+ ) {
+ Text(stringResource(R.string.button_delete))
+ }
+ },
+ dismissButton = {
+ TextButton(onClick = onDismiss) {
+ Text(stringResource(R.string.button_cancel))
+ }
+ }
+ )
+}
+
@Composable
fun ConnectionIndicator(
modifier: Modifier = Modifier,
diff --git a/app/src/main/java/com/example/saipp/ui/management/AssignmentListScreen.kt b/app/src/main/java/com/example/saipp/ui/management/AssignmentListScreen.kt
index c5387b9..00392b0 100644
--- a/app/src/main/java/com/example/saipp/ui/management/AssignmentListScreen.kt
+++ b/app/src/main/java/com/example/saipp/ui/management/AssignmentListScreen.kt
@@ -9,16 +9,22 @@ import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit
+import androidx.compose.material.icons.filled.Share
import androidx.compose.material3.*
import androidx.compose.runtime.*
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 com.example.saipp.R
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 com.example.saipp.ui.DeleteConfirmDialog
+import com.example.saipp.util.PdfGenerator
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.*
@@ -37,6 +43,9 @@ fun AssignmentListScreen(
var isLoading by remember { mutableStateOf(true) }
var searchQuery by remember { mutableStateOf("") }
var showQuickScanner by remember { mutableStateOf(false) }
+ var hideInactive by remember { mutableStateOf(true) }
+
+ var itemToDelete by remember { mutableStateOf(null) }
fun loadItems() {
scope.launch {
@@ -52,11 +61,15 @@ fun AssignmentListScreen(
loadItems()
}
- val filteredItems = remember(items, searchQuery) {
+ val filteredItems = remember(items, searchQuery, hideInactive) {
items.filter {
- it.personName?.contains(searchQuery, ignoreCase = true) ?: false ||
- it.saiNs?.contains(searchQuery, ignoreCase = true) ?: false ||
- it.observations?.contains(searchQuery, ignoreCase = true) ?: false
+ val matchesSearch = it.personName?.contains(searchQuery, ignoreCase = true) ?: false ||
+ it.saiNs?.contains(searchQuery, ignoreCase = true) ?: false ||
+ it.observations?.contains(searchQuery, ignoreCase = true) ?: false
+
+ val matchesActive = if (hideInactive) !it.personName!!.contains("(Baja)") else true
+
+ matchesSearch && matchesActive
}
}
@@ -85,7 +98,7 @@ fun AssignmentListScreen(
},
floatingActionButton = {
FloatingActionButton(onClick = onNavigateToAdd) {
- Icon(Icons.Default.Add, contentDescription = "A帽adir SAI - Persona")
+ Icon(Icons.Default.Add, contentDescription = "A帽adir Asignaci贸n")
}
}
) { padding ->
@@ -94,9 +107,38 @@ fun AssignmentListScreen(
query = searchQuery,
onQueryChange = { searchQuery = it },
onScanClick = { showQuickScanner = true },
- modifier = Modifier.padding(16.dp)
+ modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
)
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Checkbox(
+ checked = hideInactive,
+ onCheckedChange = { hideInactive = it }
+ )
+ Text(
+ text = stringResource(R.string.filter_hide_inactive),
+ style = MaterialTheme.typography.bodySmall
+ )
+ }
+
+ if (itemToDelete != null) {
+ DeleteConfirmDialog(
+ onDismiss = { itemToDelete = null },
+ onConfirm = {
+ val id = itemToDelete!!.id
+ itemToDelete = null
+ scope.launch {
+ repository.delete(id).onSuccess { loadItems() }
+ }
+ }
+ )
+ }
+
if (showQuickScanner) {
ScannerDialog(
onDismiss = { showQuickScanner = false },
@@ -112,38 +154,51 @@ fun AssignmentListScreen(
CircularProgressIndicator()
}
} else {
- LazyColumn(modifier = Modifier.weight(1f).fillMaxWidth()) {
+ LazyColumn(
+ modifier = Modifier.weight(1f).fillMaxWidth(),
+ contentPadding = PaddingValues(16.dp),
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
items(filteredItems) { item ->
- ListItem(
- headlineContent = { Text(item.personName ?: "Persona desconocida") },
- supportingContent = {
- Column {
- Text("SAI: ${item.saiNs ?: "N/A"}")
- Text("Asignado: ${formatDate(item.assignedDate)}")
- if (item.returnDate != null) {
- Text("Devoluci贸n: ${formatDate(item.returnDate)}")
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
+ ) {
+ ListItem(
+ headlineContent = {
+ Text(
+ text = item.personName ?: "Persona desconocida",
+ fontWeight = androidx.compose.ui.text.font.FontWeight.Bold,
+ color = if (item.personName?.contains("(Baja)") == true) Color.Gray else Color.Unspecified
+ )
+ },
+ supportingContent = {
+ Column {
+ Text("SAI: ${item.saiNs ?: "N/A"}")
+ Text("Asignado: ${formatDate(item.assignedDate)}")
+ if (item.returnDate != null) {
+ Text("Devoluci贸n: ${formatDate(item.returnDate)}")
+ }
+ if (!item.observations.isNullOrBlank()) {
+ Text("Obs: ${item.observations}", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.secondary)
+ }
}
- if (!item.observations.isNullOrBlank()) {
- Text("Obs: ${item.observations}", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.secondary)
- }
- }
- },
- trailingContent = {
- Row {
- IconButton(onClick = { onNavigateToEdit(item.id) }) {
- Icon(Icons.Default.Edit, contentDescription = "Editar")
- }
- IconButton(onClick = {
- scope.launch {
- repository.delete(item.id).onSuccess { loadItems() }
+ },
+ trailingContent = {
+ Row {
+ IconButton(onClick = { PdfGenerator.generateReceipt(context, item) }) {
+ Icon(imageVector = Icons.Default.Share, contentDescription = "Generar Recibo", tint = MaterialTheme.colorScheme.tertiary)
+ }
+ IconButton(onClick = { onNavigateToEdit(item.id) }) {
+ Icon(Icons.Default.Edit, contentDescription = "Editar", tint = MaterialTheme.colorScheme.primary)
+ }
+ IconButton(onClick = { itemToDelete = item }) {
+ Icon(Icons.Default.Delete, contentDescription = "Eliminar", tint = MaterialTheme.colorScheme.error)
}
- }) {
- Icon(Icons.Default.Delete, contentDescription = "Eliminar")
}
}
- }
- )
- HorizontalDivider()
+ )
+ }
}
}
}
diff --git a/app/src/main/java/com/example/saipp/ui/management/AvailableSaiListScreen.kt b/app/src/main/java/com/example/saipp/ui/management/AvailableSaiListScreen.kt
new file mode 100644
index 0000000..878f57d
--- /dev/null
+++ b/app/src/main/java/com/example/saipp/ui/management/AvailableSaiListScreen.kt
@@ -0,0 +1,97 @@
+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.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.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 com.example.saipp.ui.SearchBar
+import kotlinx.coroutines.launch
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun AvailableSaiListScreen(
+ onNavigateBack: () -> Unit
+) {
+ val context = LocalContext.current
+ val scope = rememberCoroutineScope()
+ val repository = remember { SaiRepository() }
+ var sais by remember { mutableStateOf>(emptyList()) }
+ var isLoading by remember { mutableStateOf(true) }
+ var searchQuery by remember { mutableStateOf("") }
+
+ fun loadSais() {
+ scope.launch {
+ isLoading = true
+ repository.getAvailable()
+ .onSuccess { sais = it }
+ .onFailure { Toast.makeText(context, "Error: ${it.message}", Toast.LENGTH_SHORT).show() }
+ isLoading = false
+ }
+ }
+
+ LaunchedEffect(Unit) {
+ loadSais()
+ }
+
+ val filteredSais = remember(sais, searchQuery) {
+ sais.filter {
+ it.ns.contains(searchQuery, ignoreCase = true) ||
+ (it.modelo?.contains(searchQuery, ignoreCase = true) ?: false)
+ }
+ }
+
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ title = { Text(stringResource(R.string.menu_available_sais)) },
+ navigationIcon = {
+ IconButton(onClick = onNavigateBack) {
+ Icon(Icons.Default.ArrowBack, contentDescription = "Atr谩s")
+ }
+ }
+ )
+ }
+ ) { padding ->
+ Column(modifier = Modifier.fillMaxSize().padding(padding)) {
+ SearchBar(
+ query = searchQuery,
+ onQueryChange = { searchQuery = it },
+ modifier = Modifier.padding(16.dp)
+ )
+
+ if (isLoading) {
+ Box(modifier = Modifier.weight(1f).fillMaxWidth(), contentAlignment = Alignment.Center) {
+ CircularProgressIndicator()
+ }
+ } else {
+ if (filteredSais.isEmpty()) {
+ Box(modifier = Modifier.weight(1f).fillMaxWidth(), contentAlignment = Alignment.Center) {
+ Text("No hay SAIs disponibles")
+ }
+ } else {
+ LazyColumn(modifier = Modifier.weight(1f).fillMaxWidth()) {
+ items(filteredSais) { sai ->
+ ListItem(
+ headlineContent = { Text("NS: ${sai.ns}") },
+ supportingContent = { Text("Modelo: ${sai.modelo ?: "N/A"}") }
+ )
+ HorizontalDivider()
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/example/saipp/ui/management/BatteryListScreen.kt b/app/src/main/java/com/example/saipp/ui/management/BatteryListScreen.kt
index 83a9f7b..3e82f82 100644
--- a/app/src/main/java/com/example/saipp/ui/management/BatteryListScreen.kt
+++ b/app/src/main/java/com/example/saipp/ui/management/BatteryListScreen.kt
@@ -18,6 +18,7 @@ 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 com.example.saipp.ui.DeleteConfirmDialog
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.*
@@ -35,6 +36,8 @@ fun BatteryListScreen(
var items by remember { mutableStateOf>(emptyList()) }
var isLoading by remember { mutableStateOf(true) }
var searchQuery by remember { mutableStateOf("") }
+
+ var batteryToDelete by remember { mutableStateOf(null) }
fun loadItems() {
scope.launch {
@@ -92,38 +95,55 @@ fun BatteryListScreen(
onQueryChange = { searchQuery = it },
modifier = Modifier.padding(16.dp)
)
+
+ if (batteryToDelete != null) {
+ DeleteConfirmDialog(
+ onDismiss = { batteryToDelete = null },
+ onConfirm = {
+ val id = batteryToDelete!!.id
+ batteryToDelete = null
+ scope.launch {
+ repository.delete(id).onSuccess { loadItems() }
+ }
+ }
+ )
+ }
if (isLoading) {
Box(modifier = Modifier.weight(1f).fillMaxWidth(), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
} else {
- LazyColumn(modifier = Modifier.weight(1f).fillMaxWidth()) {
+ LazyColumn(
+ modifier = Modifier.weight(1f).fillMaxWidth(),
+ contentPadding = PaddingValues(16.dp),
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
items(filteredItems) { item ->
- ListItem(
- headlineContent = { Text("Marca: ${item.brand ?: "Desconocida"}") },
- supportingContent = {
- Column {
- Text("Instalaci贸n: ${formatDate(item.installDate)}")
- item.idSai?.let { Text("SAI ID: $it") }
- }
- },
- trailingContent = {
- Row {
- IconButton(onClick = { onNavigateToEdit(item.id) }) {
- Icon(Icons.Default.Edit, contentDescription = "Editar")
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
+ ) {
+ ListItem(
+ headlineContent = { Text("Marca: ${item.brand ?: "Desconocida"}", fontWeight = androidx.compose.ui.text.font.FontWeight.Bold) },
+ supportingContent = {
+ Column {
+ Text("Instalaci贸n: ${formatDate(item.installDate)}")
+ item.idSai?.let { Text("SAI ID: $it") }
}
- IconButton(onClick = {
- scope.launch {
- repository.delete(item.id).onSuccess { loadItems() }
+ },
+ trailingContent = {
+ Row {
+ IconButton(onClick = { onNavigateToEdit(item.id) }) {
+ Icon(Icons.Default.Edit, contentDescription = "Editar", tint = MaterialTheme.colorScheme.primary)
+ }
+ IconButton(onClick = { batteryToDelete = item }) {
+ Icon(Icons.Default.Delete, contentDescription = "Eliminar", tint = MaterialTheme.colorScheme.error)
}
- }) {
- Icon(Icons.Default.Delete, contentDescription = "Eliminar")
}
}
- }
- )
- HorizontalDivider()
+ )
+ }
}
}
}
diff --git a/app/src/main/java/com/example/saipp/ui/management/EditSaiScreen.kt b/app/src/main/java/com/example/saipp/ui/management/EditSaiScreen.kt
index 2f680d6..fa71dc8 100644
--- a/app/src/main/java/com/example/saipp/ui/management/EditSaiScreen.kt
+++ b/app/src/main/java/com/example/saipp/ui/management/EditSaiScreen.kt
@@ -27,13 +27,18 @@ fun EditSaiScreen(
var ns by remember { mutableStateOf("") }
var modelo by remember { mutableStateOf("") }
+ var estado by remember { mutableStateOf("Disponible") }
var isLoading by remember { mutableStateOf(true) }
var isSaving by remember { mutableStateOf(false) }
+ var expanded by remember { mutableStateOf(false) }
+
+ val statusOptions = listOf("Disponible", "Asignado", "En reparaci贸n", "Baja")
LaunchedEffect(saiId) {
repository.getById(saiId).onSuccess {
ns = it.ns
modelo = it.modelo ?: ""
+ estado = it.estado
isLoading = false
}.onFailure {
Toast.makeText(context, "Error al cargar SAI", Toast.LENGTH_SHORT).show()
@@ -77,13 +82,44 @@ fun EditSaiScreen(
label = { Text(stringResource(R.string.label_modelo)) },
modifier = Modifier.fillMaxWidth()
)
+ Spacer(modifier = Modifier.height(16.dp))
+
+ // Status Dropdown
+ ExposedDropdownMenuBox(
+ expanded = expanded,
+ onExpandedChange = { expanded = !expanded }
+ ) {
+ OutlinedTextField(
+ value = estado,
+ onValueChange = {},
+ readOnly = true,
+ label = { Text(stringResource(R.string.label_status)) },
+ trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
+ modifier = Modifier.menuAnchor().fillMaxWidth()
+ )
+ ExposedDropdownMenu(
+ expanded = expanded,
+ onDismissRequest = { expanded = false }
+ ) {
+ statusOptions.forEach { selectionOption ->
+ DropdownMenuItem(
+ text = { Text(selectionOption) },
+ onClick = {
+ estado = selectionOption
+ expanded = false
+ }
+ )
+ }
+ }
+ }
+
Spacer(modifier = Modifier.height(32.dp))
Button(
onClick = {
if (ns.isNotBlank()) {
isSaving = true
scope.launch {
- repository.update(Sai(id = saiId, ns = ns, modelo = modelo))
+ repository.update(Sai(id = saiId, ns = ns, modelo = modelo, estado = estado))
.onSuccess {
Toast.makeText(context, "SAI actualizado", Toast.LENGTH_SHORT).show()
onNavigateBack()
diff --git a/app/src/main/java/com/example/saipp/ui/management/HomeScreen.kt b/app/src/main/java/com/example/saipp/ui/management/HomeScreen.kt
index 9aaef0e..a562a6c 100644
--- a/app/src/main/java/com/example/saipp/ui/management/HomeScreen.kt
+++ b/app/src/main/java/com/example/saipp/ui/management/HomeScreen.kt
@@ -1,15 +1,20 @@
package com.example.saipp.ui.management
import androidx.compose.foundation.layout.*
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.*
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.text.font.FontWeight
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
import com.example.saipp.ui.ConnectionIndicator
import com.example.saipp.R
-import com.example.saipp.data.MariaDbConnection
+import com.example.saipp.data.*
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -17,14 +22,31 @@ fun HomeScreen(
onNavigateToSais: () -> Unit,
onNavigateToBatteries: () -> Unit,
onNavigateToPeople: () -> Unit,
- onNavigateToAssignments: () -> Unit
+ onNavigateToAssignments: () -> Unit,
+ onNavigateToAvailableSais: () -> Unit,
+ onNavigateToQuickInfo: () -> Unit
) {
var connectionStatus by remember { mutableStateOf?>(null) }
var isCheckingConnection by remember { mutableStateOf(true) }
+
+ val saiRepo = remember { SaiRepository() }
+ val batteryRepo = remember { BatteryRepository() }
+ val personRepo = remember { PersonRepository() }
+
+ var saiStats by remember { mutableStateOf(Pair(0, 0)) }
+ var batteryCount by remember { mutableStateOf(0) }
+ var expiredBatteries by remember { mutableStateOf(0) }
+ var peopleCount by remember { mutableStateOf(0) }
LaunchedEffect(Unit) {
isCheckingConnection = true
connectionStatus = MariaDbConnection.testConnection()
+ if (connectionStatus?.isSuccess == true) {
+ saiStats = saiRepo.getStats().getOrDefault(Pair(0, 0))
+ batteryCount = batteryRepo.getCount().getOrDefault(0)
+ expiredBatteries = batteryRepo.getExpiredCount().getOrDefault(0)
+ peopleCount = personRepo.getActiveCount().getOrDefault(0)
+ }
isCheckingConnection = false
}
@@ -37,61 +59,163 @@ fun HomeScreen(
modifier = Modifier
.fillMaxSize()
.padding(padding)
- .padding(16.dp),
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.Center
+ .padding(horizontal = 16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
) {
ConnectionIndicator(
isChecking = isCheckingConnection,
status = connectionStatus
)
- Spacer(modifier = Modifier.height(32.dp))
+ Spacer(modifier = Modifier.height(24.dp))
- MenuButton(
- text = stringResource(R.string.menu_sais),
- onClick = onNavigateToSais,
- modifier = Modifier.fillMaxWidth()
- )
-
- Spacer(modifier = Modifier.height(16.dp))
-
- MenuButton(
- text = stringResource(R.string.menu_batteries),
- onClick = onNavigateToBatteries,
- modifier = Modifier.fillMaxWidth()
- )
-
- Spacer(modifier = Modifier.height(16.dp))
-
- MenuButton(
- text = stringResource(R.string.menu_people),
- onClick = onNavigateToPeople,
- modifier = Modifier.fillMaxWidth()
- )
+ // Dashboard Stats
+ StatsDashboard(saiStats, batteryCount, peopleCount)
+
+ if (expiredBatteries > 0) {
+ Spacer(modifier = Modifier.height(16.dp))
+ MaintenanceAlert(expiredBatteries)
+ }
+
+ Spacer(modifier = Modifier.height(24.dp))
+
+ // Quick Scan Button
+ Button(
+ onClick = onNavigateToQuickInfo,
+ modifier = Modifier.fillMaxWidth().height(56.dp),
+ colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.tertiary)
+ ) {
+ Icon(Icons.Default.QrCodeScanner, contentDescription = null)
+ Spacer(modifier = Modifier.width(8.dp))
+ Text(stringResource(R.string.menu_quick_info))
+ }
Spacer(modifier = Modifier.height(16.dp))
- MenuButton(
- text = stringResource(R.string.menu_assignments),
- onClick = onNavigateToAssignments,
- modifier = Modifier.fillMaxWidth()
- )
+ // Main Menu Grid
+ Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
+ Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
+ MenuCard(
+ text = stringResource(R.string.menu_sais),
+ icon = Icons.Default.Computer,
+ onClick = onNavigateToSais,
+ modifier = Modifier.weight(1f)
+ )
+ MenuCard(
+ text = stringResource(R.string.menu_batteries),
+ icon = Icons.Default.BatteryChargingFull,
+ onClick = onNavigateToBatteries,
+ modifier = Modifier.weight(1f)
+ )
+ }
+ Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
+ MenuCard(
+ text = stringResource(R.string.menu_people),
+ icon = Icons.Default.People,
+ onClick = onNavigateToPeople,
+ modifier = Modifier.weight(1f)
+ )
+ MenuCard(
+ text = stringResource(R.string.menu_assignments),
+ icon = Icons.Default.AssignmentInd,
+ onClick = onNavigateToAssignments,
+ modifier = Modifier.weight(1f)
+ )
+ }
+ }
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ Button(
+ onClick = onNavigateToAvailableSais,
+ modifier = Modifier.fillMaxWidth().height(56.dp),
+ shape = MaterialTheme.shapes.medium,
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.secondaryContainer,
+ contentColor = MaterialTheme.colorScheme.onSecondaryContainer
+ )
+ ) {
+ Icon(Icons.Default.CheckCircle, contentDescription = null)
+ Spacer(modifier = Modifier.width(8.dp))
+ Text(text = stringResource(R.string.menu_available_sais), style = MaterialTheme.typography.titleMedium)
+ }
+
+ Spacer(modifier = Modifier.height(16.dp))
}
}
}
@Composable
-fun MenuButton(
- text: String,
- onClick: () -> Unit,
- modifier: Modifier = Modifier
-) {
- Button(
- onClick = onClick,
- modifier = modifier.height(64.dp),
- shape = MaterialTheme.shapes.medium
+fun MaintenanceAlert(count: Int) {
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.errorContainer,
+ contentColor = MaterialTheme.colorScheme.onErrorContainer
+ )
) {
- Text(text = text, style = MaterialTheme.typography.titleMedium)
+ Row(
+ modifier = Modifier.padding(16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(Icons.Default.Warning, contentDescription = null, tint = MaterialTheme.colorScheme.error)
+ Spacer(modifier = Modifier.width(16.dp))
+ Column {
+ Text(stringResource(R.string.maintenance_alert_title), fontWeight = FontWeight.Bold)
+ Text(stringResource(R.string.maintenance_alert_message, count), style = MaterialTheme.typography.bodySmall)
+ }
+ }
+ }
+}
+
+@Composable
+fun StatsDashboard(saiStats: Pair, batteryCount: Int, peopleCount: Int) {
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)
+ ) {
+ Column(modifier = Modifier.padding(16.dp)) {
+ Text(
+ stringResource(R.string.dashboard_title),
+ style = MaterialTheme.typography.titleSmall,
+ fontWeight = FontWeight.Bold
+ )
+ Spacer(modifier = Modifier.height(12.dp))
+ Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceAround) {
+ StatItem(count = saiStats.first.toString(), label = stringResource(R.string.stats_sais), subLabel = "${saiStats.second} ${stringResource(R.string.stats_available)}")
+ StatItem(count = batteryCount.toString(), label = stringResource(R.string.stats_batteries))
+ StatItem(count = peopleCount.toString(), label = stringResource(R.string.stats_people))
+ }
+ }
+ }
+}
+
+@Composable
+fun StatItem(count: String, label: String, subLabel: String? = null) {
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
+ Text(count, fontSize = 24.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.primary)
+ Text(label, style = MaterialTheme.typography.labelMedium)
+ if (subLabel != null) {
+ Text(subLabel, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.secondary)
+ }
+ }
+}
+
+@Composable
+fun MenuCard(text: String, icon: ImageVector, onClick: () -> Unit, modifier: Modifier = Modifier) {
+ Card(
+ onClick = onClick,
+ modifier = modifier.height(100.dp),
+ elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
+ ) {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center
+ ) {
+ Icon(icon, contentDescription = null, modifier = Modifier.size(32.dp), tint = MaterialTheme.colorScheme.primary)
+ Spacer(modifier = Modifier.height(8.dp))
+ Text(text, style = MaterialTheme.typography.labelLarge, textAlign = androidx.compose.ui.text.style.TextAlign.Center)
+ }
}
}
diff --git a/app/src/main/java/com/example/saipp/ui/management/PersonListScreen.kt b/app/src/main/java/com/example/saipp/ui/management/PersonListScreen.kt
index 1c2eca4..c0f7b7f 100644
--- a/app/src/main/java/com/example/saipp/ui/management/PersonListScreen.kt
+++ b/app/src/main/java/com/example/saipp/ui/management/PersonListScreen.kt
@@ -9,15 +9,19 @@ import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit
+import androidx.compose.material.icons.filled.PersonOff
+import androidx.compose.material.icons.filled.PersonAdd
import androidx.compose.material3.*
import androidx.compose.runtime.*
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.unit.dp
import com.example.saipp.data.PersonRepository
import com.example.saipp.data.model.Person
import com.example.saipp.ui.SearchBar
+import com.example.saipp.ui.DeleteConfirmDialog
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@@ -33,6 +37,8 @@ fun PersonListScreen(
var items by remember { mutableStateOf>(emptyList()) }
var isLoading by remember { mutableStateOf(true) }
var searchQuery by remember { mutableStateOf("") }
+
+ var personToDelete by remember { mutableStateOf(null) }
fun loadItems() {
scope.launch {
@@ -75,32 +81,72 @@ fun PersonListScreen(
onQueryChange = { searchQuery = it },
modifier = Modifier.padding(16.dp)
)
+
+ if (personToDelete != null) {
+ DeleteConfirmDialog(
+ onDismiss = { personToDelete = null },
+ onConfirm = {
+ val id = personToDelete!!.id
+ personToDelete = null
+ scope.launch {
+ repository.delete(id).onSuccess { loadItems() }
+ }
+ }
+ )
+ }
if (isLoading) {
Box(modifier = Modifier.weight(1f).fillMaxWidth(), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
} else {
- LazyColumn(modifier = Modifier.weight(1f).fillMaxWidth()) {
+ LazyColumn(
+ modifier = Modifier.weight(1f).fillMaxWidth(),
+ contentPadding = PaddingValues(16.dp),
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
items(filteredItems) { item ->
- ListItem(
- headlineContent = { Text(item.name) },
- trailingContent = {
- Row {
- IconButton(onClick = { onNavigateToEdit(item.id) }) {
- Icon(Icons.Default.Edit, contentDescription = "Editar")
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
+ ) {
+ ListItem(
+ headlineContent = {
+ Text(
+ text = item.name,
+ fontWeight = androidx.compose.ui.text.font.FontWeight.Bold,
+ color = if (item.activo) Color.Unspecified else Color.Gray
+ )
+ },
+ supportingContent = {
+ if (!item.activo) {
+ Text("Baja", color = Color.Red, style = MaterialTheme.typography.labelSmall)
}
- IconButton(onClick = {
- scope.launch {
- repository.delete(item.id).onSuccess { loadItems() }
+ },
+ trailingContent = {
+ Row {
+ IconButton(onClick = {
+ scope.launch {
+ repository.toggleStatus(item.id, !item.activo)
+ .onSuccess { loadItems() }
+ }
+ }) {
+ Icon(
+ imageVector = if (item.activo) Icons.Default.PersonOff else Icons.Default.PersonAdd,
+ contentDescription = if (item.activo) "Dar de baja" else "Reactivar",
+ tint = if (item.activo) Color.Red else Color.Green
+ )
+ }
+ IconButton(onClick = { onNavigateToEdit(item.id) }) {
+ Icon(Icons.Default.Edit, contentDescription = "Editar", tint = MaterialTheme.colorScheme.primary)
+ }
+ IconButton(onClick = { personToDelete = item }) {
+ Icon(Icons.Default.Delete, contentDescription = "Eliminar", tint = MaterialTheme.colorScheme.error)
}
- }) {
- Icon(Icons.Default.Delete, contentDescription = "Eliminar")
}
}
- }
- )
- HorizontalDivider()
+ )
+ }
}
}
}
diff --git a/app/src/main/java/com/example/saipp/ui/management/QuickInfoScreen.kt b/app/src/main/java/com/example/saipp/ui/management/QuickInfoScreen.kt
new file mode 100644
index 0000000..3b228ab
--- /dev/null
+++ b/app/src/main/java/com/example/saipp/ui/management/QuickInfoScreen.kt
@@ -0,0 +1,139 @@
+package com.example.saipp.ui.management
+
+import android.Manifest
+import android.content.pm.PackageManager
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+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.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.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.core.content.ContextCompat
+import com.example.saipp.R
+import com.example.saipp.data.SaiRepository
+import com.example.saipp.data.model.Sai
+import com.example.saipp.ui.scanner.BarcodeScannerView
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun QuickInfoScreen(
+ onNavigateBack: () -> Unit
+) {
+ val context = LocalContext.current
+ 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 scannedSai by remember { mutableStateOf(null) }
+ var isSearching by remember { mutableStateOf(false) }
+ var lastScannedCode by remember { mutableStateOf("") }
+
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ title = { Text(stringResource(R.string.quick_info_title)) },
+ navigationIcon = {
+ IconButton(onClick = onNavigateBack) {
+ Icon(Icons.Default.ArrowBack, contentDescription = "Atr谩s")
+ }
+ }
+ )
+ }
+ ) { padding ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(padding)
+ ) {
+ if (hasCameraPermission) {
+ Box(modifier = Modifier.weight(1f)) {
+ BarcodeScannerView(onBarcodeDetected = { code ->
+ if (code != lastScannedCode) {
+ lastScannedCode = code
+ isSearching = true
+ // In a real app we would use a proper CoroutineScope,
+ // but LaunchedEffect(code) is cleaner here.
+ }
+ })
+ }
+ } else {
+ Box(modifier = Modifier.weight(1f), contentAlignment = Alignment.Center) {
+ Text("Permiso de c谩mara necesario")
+ }
+ }
+
+ LaunchedEffect(lastScannedCode) {
+ if (lastScannedCode.isNotEmpty()) {
+ repository.getByNs(lastScannedCode).onSuccess {
+ scannedSai = it
+ }.onFailure {
+ scannedSai = null
+ }
+ isSearching = false
+ }
+ }
+
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)
+ ) {
+ Column(modifier = Modifier.padding(16.dp)) {
+ if (isSearching) {
+ CircularProgressIndicator(modifier = Modifier.align(Alignment.CenterHorizontally))
+ } else if (scannedSai != null) {
+ Text("Informaci贸n Encontrada", fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.primary)
+ Spacer(modifier = Modifier.height(8.dp))
+ InfoRow("NS:", scannedSai!!.ns)
+ InfoRow("Modelo:", scannedSai!!.modelo ?: "Desconocido")
+ InfoRow("Estado:", scannedSai!!.estado)
+ if (scannedSai!!.asignadoA != null) {
+ InfoRow("Asignado a:", scannedSai!!.asignadoA!!)
+ }
+ } else if (lastScannedCode.isNotEmpty()) {
+ Text("No se encontr贸 ning煤n SAI con el c贸digo:", color = MaterialTheme.colorScheme.error)
+ Text(lastScannedCode, fontWeight = FontWeight.Bold)
+ } else {
+ Text(stringResource(R.string.quick_info_scan_hint), textAlign = androidx.compose.ui.text.style.TextAlign.Center)
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun InfoRow(label: String, value: String) {
+ Row(modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp)) {
+ Text(label, modifier = Modifier.width(100.dp), fontWeight = FontWeight.Medium)
+ Text(value, fontWeight = FontWeight.Bold)
+ }
+}
diff --git a/app/src/main/java/com/example/saipp/ui/management/SaiDetailScreen.kt b/app/src/main/java/com/example/saipp/ui/management/SaiDetailScreen.kt
new file mode 100644
index 0000000..18d34d3
--- /dev/null
+++ b/app/src/main/java/com/example/saipp/ui/management/SaiDetailScreen.kt
@@ -0,0 +1,158 @@
+package com.example.saipp.ui.management
+
+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.*
+import androidx.compose.material3.*
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import com.example.saipp.data.SaiRepository
+import com.example.saipp.data.model.HistoryItem
+import com.example.saipp.data.model.Sai
+import java.text.SimpleDateFormat
+import java.util.*
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun SaiDetailScreen(
+ saiId: Int,
+ onNavigateBack: () -> Unit
+) {
+ val repository = remember { SaiRepository() }
+ var sai by remember { mutableStateOf(null) }
+ var history by remember { mutableStateOf>(emptyList()) }
+ var isLoading by remember { mutableStateOf(true) }
+
+ LaunchedEffect(saiId) {
+ isLoading = true
+ val saiRes = repository.getById(saiId)
+ val historyRes = repository.getHistory(saiId)
+
+ sai = saiRes.getOrNull()
+ history = historyRes.getOrDefault(emptyList())
+ isLoading = false
+ }
+
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ title = { Text("Detalle del SAI") },
+ navigationIcon = {
+ IconButton(onClick = onNavigateBack) {
+ Icon(Icons.Default.ArrowBack, contentDescription = "Atr谩s")
+ }
+ }
+ )
+ }
+ ) { padding ->
+ if (isLoading) {
+ Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ CircularProgressIndicator()
+ }
+ } else {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(padding)
+ .padding(16.dp)
+ ) {
+ sai?.let { SaiInfoCard(it) }
+
+ Spacer(modifier = Modifier.height(24.dp))
+
+ Text(
+ text = "Historial de Actividad",
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.Bold
+ )
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ if (history.isEmpty()) {
+ Text("No hay registros previos para este equipo.", color = Color.Gray)
+ } else {
+ LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp)) {
+ items(history) { item ->
+ HistoryCard(item)
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun SaiInfoCard(sai: Sai) {
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
+ ) {
+ Column(modifier = Modifier.padding(16.dp)) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Icon(Icons.Default.Computer, contentDescription = null, tint = MaterialTheme.colorScheme.primary)
+ Spacer(modifier = Modifier.width(8.dp))
+ Text(text = "NS: ${sai.ns}", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
+ }
+ Spacer(modifier = Modifier.height(8.dp))
+ Text(text = "Modelo: ${sai.modelo ?: "N/A"}")
+ Text(text = "Estado actual: ${sai.estado}")
+ if (sai.asignadoA != null) {
+ Text(text = "En posesi贸n de: ${sai.asignadoA}", color = MaterialTheme.colorScheme.primary)
+ }
+ }
+ }
+}
+
+@Composable
+fun HistoryCard(item: HistoryItem) {
+ val icon = when(item.type) {
+ "Asignaci贸n" -> Icons.Default.Login
+ "Devoluci贸n" -> Icons.Default.Logout
+ "Mantenimiento" -> Icons.Default.Build
+ else -> Icons.Default.Info
+ }
+
+ val color = when(item.type) {
+ "Asignaci贸n" -> Color(0xFFC62828) // Red
+ "Devoluci贸n" -> Color(0xFF2E7D32) // Green
+ "Mantenimiento" -> Color(0xFFEF6C00) // Orange
+ else -> Color.Gray
+ }
+
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
+ ) {
+ Row(
+ modifier = Modifier.padding(12.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(icon, contentDescription = null, tint = color, modifier = Modifier.size(20.dp))
+ Spacer(modifier = Modifier.width(12.dp))
+ Column {
+ Text(text = item.description, style = MaterialTheme.typography.bodyMedium)
+ item.date?.let {
+ Text(text = formatDate(it), style = MaterialTheme.typography.labelSmall, color = Color.Gray)
+ }
+ }
+ }
+ }
+}
+
+fun formatDate(dbDate: String): String {
+ return try {
+ val dbFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
+ val displayFormat = SimpleDateFormat("dd MMMM yyyy", Locale.getDefault())
+ val date = dbFormat.parse(dbDate)
+ if (date != null) displayFormat.format(date) else dbDate
+ } catch (e: Exception) {
+ dbDate
+ }
+}
diff --git a/app/src/main/java/com/example/saipp/ui/management/SaiListScreen.kt b/app/src/main/java/com/example/saipp/ui/management/SaiListScreen.kt
index 7cdb6c4..02242da 100644
--- a/app/src/main/java/com/example/saipp/ui/management/SaiListScreen.kt
+++ b/app/src/main/java/com/example/saipp/ui/management/SaiListScreen.kt
@@ -5,20 +5,19 @@ 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.material.icons.filled.Edit
+import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
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.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 com.example.saipp.ui.ScannerDialog
+import com.example.saipp.ui.DeleteConfirmDialog
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@@ -26,7 +25,8 @@ import kotlinx.coroutines.launch
fun SaiListScreen(
onNavigateBack: () -> Unit,
onNavigateToScan: () -> Unit,
- onNavigateToEdit: (Int) -> Unit
+ onNavigateToEdit: (Int) -> Unit,
+ onNavigateToDetail: (Int) -> Unit
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
@@ -35,6 +35,8 @@ fun SaiListScreen(
var isLoading by remember { mutableStateOf(true) }
var searchQuery by remember { mutableStateOf("") }
var showQuickScanner by remember { mutableStateOf(false) }
+
+ var saiToDelete by remember { mutableStateOf(null) }
fun loadSais() {
scope.launch {
@@ -91,33 +93,72 @@ fun SaiListScreen(
}
)
}
+
+ if (saiToDelete != null) {
+ DeleteConfirmDialog(
+ onDismiss = { saiToDelete = null },
+ onConfirm = {
+ val id = saiToDelete!!.id
+ saiToDelete = null
+ scope.launch {
+ repository.delete(id).onSuccess { loadSais() }
+ }
+ }
+ )
+ }
if (isLoading) {
Box(modifier = Modifier.weight(1f).fillMaxWidth(), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
} else {
- LazyColumn(modifier = Modifier.weight(1f).fillMaxWidth()) {
+ LazyColumn(
+ modifier = Modifier.weight(1f).fillMaxWidth(),
+ contentPadding = PaddingValues(16.dp),
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
items(filteredSais) { sai ->
- ListItem(
- headlineContent = { Text("NS: ${sai.ns}") },
- supportingContent = { Text("Modelo: ${sai.modelo ?: "N/A"}") },
- trailingContent = {
- Row {
- IconButton(onClick = { onNavigateToEdit(sai.id) }) {
- Icon(Icons.Default.Edit, contentDescription = "Editar")
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ onClick = { onNavigateToDetail(sai.id) },
+ elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
+ ) {
+ ListItem(
+ headlineContent = { Text("NS: ${sai.ns}", fontWeight = androidx.compose.ui.text.font.FontWeight.Bold) },
+ supportingContent = {
+ Column {
+ Text("Modelo: ${sai.modelo ?: "N/A"}")
+ Text(
+ text = sai.estado,
+ color = when(sai.estado) {
+ "Disponible" -> Color(0xFF2E7D32)
+ "Asignado" -> Color(0xFFC62828)
+ "En reparaci贸n" -> Color(0xFFEF6C00)
+ else -> Color.Gray
+ },
+ style = MaterialTheme.typography.labelSmall
+ )
+ if (sai.estado == "Asignado" && sai.asignadoA != null) {
+ Text(
+ text = "Asignado a: ${sai.asignadoA}",
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.primary
+ )
+ }
}
- IconButton(onClick = {
- scope.launch {
- repository.delete(sai.id).onSuccess { loadSais() }
+ },
+ trailingContent = {
+ Row {
+ IconButton(onClick = { onNavigateToEdit(sai.id) }) {
+ Icon(Icons.Default.Edit, contentDescription = "Editar", tint = MaterialTheme.colorScheme.primary)
+ }
+ IconButton(onClick = { saiToDelete = sai }) {
+ Icon(Icons.Default.Delete, contentDescription = "Eliminar", tint = MaterialTheme.colorScheme.error)
}
- }) {
- Icon(Icons.Default.Delete, contentDescription = "Eliminar")
}
}
- }
- )
- HorizontalDivider()
+ )
+ }
}
}
}
diff --git a/app/src/main/java/com/example/saipp/util/PdfGenerator.kt b/app/src/main/java/com/example/saipp/util/PdfGenerator.kt
new file mode 100644
index 0000000..af19cdf
--- /dev/null
+++ b/app/src/main/java/com/example/saipp/util/PdfGenerator.kt
@@ -0,0 +1,73 @@
+package com.example.saipp.util
+
+import android.content.Context
+import android.content.Intent
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.graphics.pdf.PdfDocument
+import android.net.Uri
+import androidx.core.content.FileProvider
+import com.example.saipp.data.model.Assignment
+import java.io.File
+import java.io.FileOutputStream
+
+object PdfGenerator {
+
+ fun generateReceipt(context: Context, assignment: Assignment) {
+ val pdfDocument = PdfDocument()
+ val pageInfo = PdfDocument.PageInfo.Builder(595, 842, 1).create() // A4 size
+ val page = pdfDocument.startPage(pageInfo)
+ val canvas: Canvas = page.canvas
+ val paint = Paint()
+
+ // Title
+ paint.textSize = 24f
+ paint.isFakeBoldText = true
+ canvas.drawText("RECIBO DE ENTREGA DE EQUIPO", 50f, 50f, paint)
+
+ // Content
+ paint.textSize = 14f
+ paint.isFakeBoldText = false
+ var y = 100f
+
+ val content = listOf(
+ "Persona: ${assignment.personName ?: "N/A"}",
+ "Equipo (SAI) NS: ${assignment.saiNs ?: "N/A"}",
+ "Fecha Asignaci贸n: ${assignment.assignedDate ?: "N/A"}",
+ "Observaciones: ${assignment.observations ?: "Ninguna"}",
+ "",
+ "",
+ "__________________________",
+ "Firma del Receptor"
+ )
+
+ for (line in content) {
+ canvas.drawText(line, 50f, y, paint)
+ y += 30f
+ }
+
+ pdfDocument.finishPage(page)
+
+ // Save file
+ val file = File(context.cacheDir, "recibo_${assignment.id}.pdf")
+ try {
+ pdfDocument.writeTo(FileOutputStream(file))
+ sharePdf(context, file)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ } finally {
+ pdfDocument.close()
+ }
+ }
+
+ private fun sharePdf(context: Context, file: File) {
+ val uri = FileProvider.getUriForFile(context, "${context.packageName}.provider", file)
+ val intent = Intent(Intent.ACTION_SEND).apply {
+ type = "application/pdf"
+ putExtra(Intent.EXTRA_STREAM, uri)
+ addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ }
+ context.startActivity(Intent.createChooser(intent, "Compartir Recibo"))
+ }
+}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 04efb2e..d01f184 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -22,11 +22,38 @@
Buscar...
Este SAI ya est谩 registrado en el sistema.
Escanear para buscar
+ Disponible
+ Asignado
+ En reparaci贸n
+ Baja
+ Estado
+ Dar de Baja
+ Reactivar
+ Ocultar personal de baja
+
+ Estado General
+ SAIs
+ Bater铆as
+ Personal
+ disponibles
+
+ Alerta de Mantenimiento
+ Hay %1$d bater铆as con m谩s de 2 a帽os de antig眉edad. Se recomienda revisarlas.
+
+ 驴Eliminar registro?
+ Esta acci贸n no se puede deshacer. 驴Est谩s seguro de que deseas eliminar este elemento?
+ Eliminar
+ Cancelar
+
+ Consulta R谩pida
+ Informaci贸n del SAI
+ Escanea el c贸digo de barras del SAI para ver su estado actual.
Gesti贸n de SAIs
Gesti贸n de Bater铆as
Gesti贸n de Personas
Gestion SAI - Persona
+ SAIs Disponibles
Persona
Fecha Asignaci贸n (DD-MM-AAAA)
diff --git a/app/src/main/res/xml/file_paths.xml b/app/src/main/res/xml/file_paths.xml
new file mode 100644
index 0000000..d2e2c8d
--- /dev/null
+++ b/app/src/main/res/xml/file_paths.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index bea09ad..c576dc9 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -12,6 +12,7 @@ cameraX = "1.6.1"
mlkitBarcode = "17.3.0"
mariadbDriver = "3.5.1"
navigationCompose = "2.9.8"
+coreSplashscreen = "1.0.1"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -30,6 +31,7 @@ androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-te
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-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "coreSplashscreen" }
androidx-camera-core = { group = "androidx.camera", name = "camera-core", version.ref = "cameraX" }
androidx-camera-camera2 = { group = "androidx.camera", name = "camera-camera2", version.ref = "cameraX" }
androidx-camera-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", version.ref = "cameraX" }