Mejoras para la aplicacion
This commit is contained in:
@@ -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")
|
||||
|
||||
|
||||
@@ -27,6 +27,16 @@
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<List<Assignment>> = 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<Assignment>()
|
||||
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<Unit> = 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)
|
||||
|
||||
@@ -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<Unit> = 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<Int> = 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<Int> = 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,14 +9,37 @@ class PersonRepository {
|
||||
suspend fun getAll(): Result<List<Person>> = withContext(Dispatchers.IO) {
|
||||
val connection = MariaDbConnection.getConnection() ?: return@withContext Result.failure(Exception("db_connection_error"))
|
||||
try {
|
||||
val sql = "SELECT id_persona, nombre FROM Persona"
|
||||
val sql = "SELECT id_persona, nombre, activo FROM Persona"
|
||||
val statement = connection.prepareStatement(sql)
|
||||
val resultSet = statement.executeQuery()
|
||||
val items = mutableListOf<Person>()
|
||||
while (resultSet.next()) {
|
||||
items.add(Person(
|
||||
id = resultSet.getInt("id_persona"),
|
||||
name = resultSet.getString("nombre")
|
||||
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<List<Person>> = 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<Person>()
|
||||
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<Unit> = 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<Unit> = 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<Unit> = 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<Person> = 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<Int> = 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<List<Sai>> = 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<Sai>()
|
||||
@@ -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<List<Sai>> = 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<Sai>()
|
||||
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<Unit> = withContext(Dispatchers.IO) {
|
||||
val connection = MariaDbConnection.getConnection() ?: return@withContext Result.failure(Exception("db_connection_error"))
|
||||
try {
|
||||
val sql = "INSERT INTO SAIs (ns, modelo) VALUES (?, ?)"
|
||||
val 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<Unit> = 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<Sai> = 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<Pair<Int, Int>> = 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<Sai> = 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<List<HistoryItem>> = withContext(Dispatchers.IO) {
|
||||
val connection = MariaDbConnection.getConnection() ?: return@withContext Result.failure(Exception("db_connection_error"))
|
||||
try {
|
||||
val items = mutableListOf<HistoryItem>()
|
||||
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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?
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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() })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<Assignment?>(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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<List<Sai>>(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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<List<Battery>>(emptyList()) }
|
||||
var isLoading by remember { mutableStateOf(true) }
|
||||
var searchQuery by remember { mutableStateOf("") }
|
||||
|
||||
var batteryToDelete by remember { mutableStateOf<Battery?>(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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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<Result<Unit>?>(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<Int, Int>, 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<List<Person>>(emptyList()) }
|
||||
var isLoading by remember { mutableStateOf(true) }
|
||||
var searchQuery by remember { mutableStateOf("") }
|
||||
|
||||
var personToDelete by remember { mutableStateOf<Person?>(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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Sai?>(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)
|
||||
}
|
||||
}
|
||||
@@ -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<Sai?>(null) }
|
||||
var history by remember { mutableStateOf<List<HistoryItem>>(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
|
||||
}
|
||||
}
|
||||
@@ -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<Sai?>(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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
}
|
||||
@@ -22,11 +22,38 @@
|
||||
<string name="placeholder_search">Buscar...</string>
|
||||
<string name="error_duplicate_sai">Este SAI ya está registrado en el sistema.</string>
|
||||
<string name="scan_to_search">Escanear para buscar</string>
|
||||
<string name="status_available">Disponible</string>
|
||||
<string name="status_assigned">Asignado</string>
|
||||
<string name="status_repair">En reparación</string>
|
||||
<string name="status_retired">Baja</string>
|
||||
<string name="label_status">Estado</string>
|
||||
<string name="action_deactivate">Dar de Baja</string>
|
||||
<string name="action_reactivate">Reactivar</string>
|
||||
<string name="filter_hide_inactive">Ocultar personal de baja</string>
|
||||
|
||||
<string name="dashboard_title">Estado General</string>
|
||||
<string name="stats_sais">SAIs</string>
|
||||
<string name="stats_batteries">Baterías</string>
|
||||
<string name="stats_people">Personal</string>
|
||||
<string name="stats_available">disponibles</string>
|
||||
|
||||
<string name="maintenance_alert_title">Alerta de Mantenimiento</string>
|
||||
<string name="maintenance_alert_message">Hay %1$d baterías con más de 2 años de antigüedad. Se recomienda revisarlas.</string>
|
||||
|
||||
<string name="confirm_delete_title">¿Eliminar registro?</string>
|
||||
<string name="confirm_delete_message">Esta acción no se puede deshacer. ¿Estás seguro de que deseas eliminar este elemento?</string>
|
||||
<string name="button_delete">Eliminar</string>
|
||||
<string name="button_cancel">Cancelar</string>
|
||||
|
||||
<string name="menu_quick_info">Consulta Rápida</string>
|
||||
<string name="quick_info_title">Información del SAI</string>
|
||||
<string name="quick_info_scan_hint">Escanea el código de barras del SAI para ver su estado actual.</string>
|
||||
|
||||
<string name="menu_sais">Gestión de SAIs</string>
|
||||
<string name="menu_batteries">Gestión de Baterías</string>
|
||||
<string name="menu_people">Gestión de Personas</string>
|
||||
<string name="menu_assignments">Gestion SAI - Persona</string>
|
||||
<string name="menu_available_sais">SAIs Disponibles</string>
|
||||
|
||||
<string name="label_person">Persona</string>
|
||||
<string name="label_assigned_date">Fecha Asignación (DD-MM-AAAA)</string>
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths>
|
||||
<cache-path name="cache" path="." />
|
||||
</paths>
|
||||
Reference in New Issue
Block a user