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" }