Mejoras para la aplicacion
This commit is contained in:
@@ -4,10 +4,12 @@ Android application developed in Kotlin using Jetpack Compose that scans barcode
|
|||||||
|
|
||||||
## 🚀 Features
|
## 🚀 Features
|
||||||
|
|
||||||
- **Instant Scan**: The camera opens when adding a new SAI.
|
- **Dashboard**: High-level overview of inventory stats (Total SAIs, available, etc.) on the home screen.
|
||||||
- **Multi-Entity Management**: Dedicated sections for SAIs, Batteries, People, and SAI-Person Assignments.
|
- **Instant Scan & Quick Info**: Scan any SAI to see its details and current assignee immediately.
|
||||||
- **CRUD Operations**: Add, View, and Delete records for all managed entities.
|
- **Search & Filter**: Robust search bars across all lists with optional barcode scanning for SAIs.
|
||||||
- **Connection Status**: Real-time indicator on the home screen.
|
- **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.
|
- **Visual Overlay**: Centered scanning guide with a red line to help align barcodes.
|
||||||
- **ML Kit Integration**: High-performance barcode detection (HD 720p analysis).
|
- **ML Kit Integration**: High-performance barcode detection (HD 720p analysis).
|
||||||
- **Confirmation Flow**: Scanned results are shown in a popup dialog before saving.
|
- **Confirmation Flow**: Scanned results are shown in a popup dialog before saving.
|
||||||
@@ -40,7 +42,8 @@ Ensure your MariaDB database has the following tables:
|
|||||||
CREATE TABLE SAIs (
|
CREATE TABLE SAIs (
|
||||||
id_sai INT AUTO_INCREMENT PRIMARY KEY,
|
id_sai INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
ns VARCHAR(255) NOT NULL UNIQUE,
|
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
|
-- Table for Batteries
|
||||||
@@ -48,14 +51,15 @@ CREATE TABLE Bateria (
|
|||||||
id_bateria INT AUTO_INCREMENT PRIMARY KEY,
|
id_bateria INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
id_sai INT,
|
id_sai INT,
|
||||||
marca VARCHAR(255),
|
marca VARCHAR(255),
|
||||||
fecha_instalacion DATETIME,
|
fecha_instalacion DATE,
|
||||||
FOREIGN KEY (id_sai) REFERENCES SAIs(id_sai)
|
FOREIGN KEY (id_sai) REFERENCES SAIs(id_sai)
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Table for People
|
-- Table for People
|
||||||
CREATE TABLE Persona (
|
CREATE TABLE Persona (
|
||||||
id_persona INT AUTO_INCREMENT PRIMARY KEY,
|
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
|
-- Table for SAI - Person Assignments
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ dependencies {
|
|||||||
implementation(libs.androidx.compose.ui.graphics)
|
implementation(libs.androidx.compose.ui.graphics)
|
||||||
implementation(libs.androidx.compose.ui.tooling.preview)
|
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||||
implementation(libs.androidx.core.ktx)
|
implementation(libs.androidx.core.ktx)
|
||||||
|
implementation(libs.androidx.core.splashscreen)
|
||||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.7.0")
|
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.7.0")
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,16 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</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>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -4,11 +4,13 @@ import android.os.Bundle
|
|||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
import com.example.saipp.ui.AppNavigation
|
import com.example.saipp.ui.AppNavigation
|
||||||
import com.example.saipp.ui.theme.SAIppTheme
|
import com.example.saipp.ui.theme.SAIppTheme
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
installSplashScreen()
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
setContent {
|
setContent {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import com.example.saipp.data.model.Assignment
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.sql.SQLException
|
import java.sql.SQLException
|
||||||
import java.sql.Types
|
|
||||||
|
|
||||||
class AssignmentRepository {
|
class AssignmentRepository {
|
||||||
suspend fun getAll(): Result<List<Assignment>> = withContext(Dispatchers.IO) {
|
suspend fun getAll(): Result<List<Assignment>> = withContext(Dispatchers.IO) {
|
||||||
@@ -12,7 +11,7 @@ class AssignmentRepository {
|
|||||||
try {
|
try {
|
||||||
val sql = """
|
val sql = """
|
||||||
SELECT a.id_asignacion, a.id_sai, a.id_persona, a.fecha_asignado, a.fecha_devolucion, a.observaciones,
|
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
|
FROM Asignado a
|
||||||
LEFT JOIN SAIs s ON a.id_sai = s.id_sai
|
LEFT JOIN SAIs s ON a.id_sai = s.id_sai
|
||||||
LEFT JOIN Persona p ON a.id_persona = p.id_persona
|
LEFT JOIN Persona p ON a.id_persona = p.id_persona
|
||||||
@@ -21,6 +20,9 @@ class AssignmentRepository {
|
|||||||
val resultSet = statement.executeQuery()
|
val resultSet = statement.executeQuery()
|
||||||
val items = mutableListOf<Assignment>()
|
val items = mutableListOf<Assignment>()
|
||||||
while (resultSet.next()) {
|
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(
|
items.add(Assignment(
|
||||||
id = resultSet.getInt("id_asignacion"),
|
id = resultSet.getInt("id_asignacion"),
|
||||||
idSai = resultSet.getInt("id_sai"),
|
idSai = resultSet.getInt("id_sai"),
|
||||||
@@ -29,7 +31,7 @@ class AssignmentRepository {
|
|||||||
returnDate = resultSet.getString("fecha_devolucion"),
|
returnDate = resultSet.getString("fecha_devolucion"),
|
||||||
observations = resultSet.getString("observaciones"),
|
observations = resultSet.getString("observaciones"),
|
||||||
saiNs = resultSet.getString("sai_ns"),
|
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)
|
Result.success(items)
|
||||||
@@ -77,7 +79,15 @@ class AssignmentRepository {
|
|||||||
statement.setString(4, item.returnDate)
|
statement.setString(4, item.returnDate)
|
||||||
statement.setString(5, item.observations)
|
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"))
|
else Result.failure(Exception("no_rows_inserted"))
|
||||||
} catch (e: SQLException) {
|
} catch (e: SQLException) {
|
||||||
Result.failure(e)
|
Result.failure(e)
|
||||||
@@ -98,7 +108,17 @@ class AssignmentRepository {
|
|||||||
statement.setString(5, item.observations)
|
statement.setString(5, item.observations)
|
||||||
statement.setInt(6, item.id)
|
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"))
|
else Result.failure(Exception("update_failed"))
|
||||||
} catch (e: SQLException) {
|
} catch (e: SQLException) {
|
||||||
Result.failure(e)
|
Result.failure(e)
|
||||||
@@ -110,10 +130,25 @@ class AssignmentRepository {
|
|||||||
suspend fun delete(id: Int): Result<Unit> = withContext(Dispatchers.IO) {
|
suspend fun delete(id: Int): Result<Unit> = withContext(Dispatchers.IO) {
|
||||||
val connection = MariaDbConnection.getConnection() ?: return@withContext Result.failure(Exception("db_connection_error"))
|
val connection = MariaDbConnection.getConnection() ?: return@withContext Result.failure(Exception("db_connection_error"))
|
||||||
try {
|
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 sql = "DELETE FROM Asignado WHERE id_asignacion = ?"
|
||||||
val statement = connection.prepareStatement(sql)
|
val statement = connection.prepareStatement(sql)
|
||||||
statement.setInt(1, id)
|
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"))
|
else Result.failure(Exception("delete_failed"))
|
||||||
} catch (e: SQLException) {
|
} catch (e: SQLException) {
|
||||||
Result.failure(e)
|
Result.failure(e)
|
||||||
|
|||||||
@@ -39,7 +39,16 @@ class BatteryRepository {
|
|||||||
statement.setString(2, item.brand)
|
statement.setString(2, item.brand)
|
||||||
statement.setString(3, item.installDate)
|
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"))
|
else Result.failure(Exception("no_rows_inserted"))
|
||||||
} catch (e: SQLException) {
|
} catch (e: SQLException) {
|
||||||
Result.failure(e)
|
Result.failure(e)
|
||||||
@@ -58,7 +67,16 @@ class BatteryRepository {
|
|||||||
statement.setString(3, item.installDate)
|
statement.setString(3, item.installDate)
|
||||||
statement.setInt(4, item.id)
|
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"))
|
else Result.failure(Exception("update_failed"))
|
||||||
} catch (e: SQLException) {
|
} catch (e: SQLException) {
|
||||||
Result.failure(e)
|
Result.failure(e)
|
||||||
@@ -70,10 +88,26 @@ class BatteryRepository {
|
|||||||
suspend fun delete(id: Int): Result<Unit> = withContext(Dispatchers.IO) {
|
suspend fun delete(id: Int): Result<Unit> = withContext(Dispatchers.IO) {
|
||||||
val connection = MariaDbConnection.getConnection() ?: return@withContext Result.failure(Exception("db_connection_error"))
|
val connection = MariaDbConnection.getConnection() ?: return@withContext Result.failure(Exception("db_connection_error"))
|
||||||
try {
|
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 sql = "DELETE FROM Bateria WHERE id_bateria = ?"
|
||||||
val statement = connection.prepareStatement(sql)
|
val statement = connection.prepareStatement(sql)
|
||||||
statement.setInt(1, id)
|
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"))
|
else Result.failure(Exception("delete_failed"))
|
||||||
} catch (e: SQLException) {
|
} catch (e: SQLException) {
|
||||||
Result.failure(e)
|
Result.failure(e)
|
||||||
@@ -105,4 +139,33 @@ class BatteryRepository {
|
|||||||
connection.close()
|
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) {
|
suspend fun getAll(): Result<List<Person>> = withContext(Dispatchers.IO) {
|
||||||
val connection = MariaDbConnection.getConnection() ?: return@withContext Result.failure(Exception("db_connection_error"))
|
val connection = MariaDbConnection.getConnection() ?: return@withContext Result.failure(Exception("db_connection_error"))
|
||||||
try {
|
try {
|
||||||
val sql = "SELECT id_persona, nombre FROM Persona"
|
val sql = "SELECT id_persona, nombre, activo FROM Persona"
|
||||||
val statement = connection.prepareStatement(sql)
|
val statement = connection.prepareStatement(sql)
|
||||||
val resultSet = statement.executeQuery()
|
val resultSet = statement.executeQuery()
|
||||||
val items = mutableListOf<Person>()
|
val items = mutableListOf<Person>()
|
||||||
while (resultSet.next()) {
|
while (resultSet.next()) {
|
||||||
items.add(Person(
|
items.add(Person(
|
||||||
id = resultSet.getInt("id_persona"),
|
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)
|
Result.success(items)
|
||||||
@@ -30,7 +53,7 @@ class PersonRepository {
|
|||||||
suspend fun insert(item: Person): Result<Unit> = withContext(Dispatchers.IO) {
|
suspend fun insert(item: Person): Result<Unit> = withContext(Dispatchers.IO) {
|
||||||
val connection = MariaDbConnection.getConnection() ?: return@withContext Result.failure(Exception("db_connection_error"))
|
val connection = MariaDbConnection.getConnection() ?: return@withContext Result.failure(Exception("db_connection_error"))
|
||||||
try {
|
try {
|
||||||
val sql = "INSERT INTO Persona (nombre) VALUES (?)"
|
val sql = "INSERT INTO Persona (nombre, activo) VALUES (?, 1)"
|
||||||
val statement = connection.prepareStatement(sql)
|
val statement = connection.prepareStatement(sql)
|
||||||
statement.setString(1, item.name)
|
statement.setString(1, item.name)
|
||||||
if (statement.executeUpdate() > 0) Result.success(Unit)
|
if (statement.executeUpdate() > 0) Result.success(Unit)
|
||||||
@@ -45,10 +68,27 @@ class PersonRepository {
|
|||||||
suspend fun update(item: Person): Result<Unit> = withContext(Dispatchers.IO) {
|
suspend fun update(item: Person): Result<Unit> = withContext(Dispatchers.IO) {
|
||||||
val connection = MariaDbConnection.getConnection() ?: return@withContext Result.failure(Exception("db_connection_error"))
|
val connection = MariaDbConnection.getConnection() ?: return@withContext Result.failure(Exception("db_connection_error"))
|
||||||
try {
|
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)
|
val statement = connection.prepareStatement(sql)
|
||||||
statement.setString(1, item.name)
|
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)
|
if (statement.executeUpdate() > 0) Result.success(Unit)
|
||||||
else Result.failure(Exception("update_failed"))
|
else Result.failure(Exception("update_failed"))
|
||||||
} catch (e: SQLException) {
|
} catch (e: SQLException) {
|
||||||
@@ -76,14 +116,15 @@ class PersonRepository {
|
|||||||
suspend fun getById(id: Int): Result<Person> = withContext(Dispatchers.IO) {
|
suspend fun getById(id: Int): Result<Person> = withContext(Dispatchers.IO) {
|
||||||
val connection = MariaDbConnection.getConnection() ?: return@withContext Result.failure(Exception("db_connection_error"))
|
val connection = MariaDbConnection.getConnection() ?: return@withContext Result.failure(Exception("db_connection_error"))
|
||||||
try {
|
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)
|
val statement = connection.prepareStatement(sql)
|
||||||
statement.setInt(1, id)
|
statement.setInt(1, id)
|
||||||
val resultSet = statement.executeQuery()
|
val resultSet = statement.executeQuery()
|
||||||
if (resultSet.next()) {
|
if (resultSet.next()) {
|
||||||
Result.success(Person(
|
Result.success(Person(
|
||||||
id = resultSet.getInt("id_persona"),
|
id = resultSet.getInt("id_persona"),
|
||||||
name = resultSet.getString("nombre")
|
name = resultSet.getString("nombre"),
|
||||||
|
activo = resultSet.getInt("activo") == 1
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
Result.failure(Exception("not_found"))
|
Result.failure(Exception("not_found"))
|
||||||
@@ -94,4 +135,18 @@ class PersonRepository {
|
|||||||
connection.close()
|
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
|
package com.example.saipp.data
|
||||||
|
|
||||||
import com.example.saipp.data.model.Sai
|
import com.example.saipp.data.model.Sai
|
||||||
|
import com.example.saipp.data.model.HistoryItem
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.sql.SQLException
|
import java.sql.SQLException
|
||||||
@@ -9,7 +10,12 @@ class SaiRepository {
|
|||||||
suspend fun getAll(): Result<List<Sai>> = withContext(Dispatchers.IO) {
|
suspend fun getAll(): Result<List<Sai>> = withContext(Dispatchers.IO) {
|
||||||
val connection = MariaDbConnection.getConnection() ?: return@withContext Result.failure(Exception("db_connection_error"))
|
val connection = MariaDbConnection.getConnection() ?: return@withContext Result.failure(Exception("db_connection_error"))
|
||||||
try {
|
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 statement = connection.prepareStatement(sql)
|
||||||
val resultSet = statement.executeQuery()
|
val resultSet = statement.executeQuery()
|
||||||
val sais = mutableListOf<Sai>()
|
val sais = mutableListOf<Sai>()
|
||||||
@@ -17,7 +23,32 @@ class SaiRepository {
|
|||||||
sais.add(Sai(
|
sais.add(Sai(
|
||||||
id = resultSet.getInt("id_sai"),
|
id = resultSet.getInt("id_sai"),
|
||||||
ns = resultSet.getString("ns"),
|
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)
|
Result.success(sais)
|
||||||
@@ -50,7 +81,7 @@ class SaiRepository {
|
|||||||
suspend fun insert(ns: String, modelo: String?): Result<Unit> = withContext(Dispatchers.IO) {
|
suspend fun insert(ns: String, modelo: String?): Result<Unit> = withContext(Dispatchers.IO) {
|
||||||
val connection = MariaDbConnection.getConnection() ?: return@withContext Result.failure(Exception("db_connection_error"))
|
val connection = MariaDbConnection.getConnection() ?: return@withContext Result.failure(Exception("db_connection_error"))
|
||||||
try {
|
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)
|
val statement = connection.prepareStatement(sql)
|
||||||
statement.setString(1, ns)
|
statement.setString(1, ns)
|
||||||
statement.setString(2, modelo)
|
statement.setString(2, modelo)
|
||||||
@@ -66,11 +97,12 @@ class SaiRepository {
|
|||||||
suspend fun update(sai: Sai): Result<Unit> = withContext(Dispatchers.IO) {
|
suspend fun update(sai: Sai): Result<Unit> = withContext(Dispatchers.IO) {
|
||||||
val connection = MariaDbConnection.getConnection() ?: return@withContext Result.failure(Exception("db_connection_error"))
|
val connection = MariaDbConnection.getConnection() ?: return@withContext Result.failure(Exception("db_connection_error"))
|
||||||
try {
|
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)
|
val statement = connection.prepareStatement(sql)
|
||||||
statement.setString(1, sai.ns)
|
statement.setString(1, sai.ns)
|
||||||
statement.setString(2, sai.modelo)
|
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)
|
if (statement.executeUpdate() > 0) Result.success(Unit)
|
||||||
else Result.failure(Exception("update_failed"))
|
else Result.failure(Exception("update_failed"))
|
||||||
} catch (e: SQLException) {
|
} catch (e: SQLException) {
|
||||||
@@ -98,7 +130,13 @@ class SaiRepository {
|
|||||||
suspend fun getById(id: Int): Result<Sai> = withContext(Dispatchers.IO) {
|
suspend fun getById(id: Int): Result<Sai> = withContext(Dispatchers.IO) {
|
||||||
val connection = MariaDbConnection.getConnection() ?: return@withContext Result.failure(Exception("db_connection_error"))
|
val connection = MariaDbConnection.getConnection() ?: return@withContext Result.failure(Exception("db_connection_error"))
|
||||||
try {
|
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)
|
val statement = connection.prepareStatement(sql)
|
||||||
statement.setInt(1, id)
|
statement.setInt(1, id)
|
||||||
val resultSet = statement.executeQuery()
|
val resultSet = statement.executeQuery()
|
||||||
@@ -106,7 +144,9 @@ class SaiRepository {
|
|||||||
Result.success(Sai(
|
Result.success(Sai(
|
||||||
id = resultSet.getInt("id_sai"),
|
id = resultSet.getInt("id_sai"),
|
||||||
ns = resultSet.getString("ns"),
|
ns = resultSet.getString("ns"),
|
||||||
modelo = resultSet.getString("modelo")
|
modelo = resultSet.getString("modelo"),
|
||||||
|
estado = resultSet.getString("estado") ?: "Disponible",
|
||||||
|
asignadoA = resultSet.getString("person_name")
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
Result.failure(Exception("not_found"))
|
Result.failure(Exception("not_found"))
|
||||||
@@ -117,4 +157,106 @@ class SaiRepository {
|
|||||||
connection.close()
|
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(
|
data class Person(
|
||||||
val id: Int = 0,
|
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(
|
data class Sai(
|
||||||
val id: Int = 0,
|
val id: Int = 0,
|
||||||
val ns: String,
|
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}") {
|
object EditSai : Screen("edit_sai/{saiId}") {
|
||||||
fun createRoute(id: Int) = "edit_sai/$id"
|
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 BatteryList : Screen("battery_list")
|
||||||
object AddBattery : Screen("add_battery")
|
object AddBattery : Screen("add_battery")
|
||||||
object EditBattery : Screen("edit_battery/{batteryId}") {
|
object EditBattery : Screen("edit_battery/{batteryId}") {
|
||||||
@@ -30,6 +33,8 @@ sealed class Screen(val route: String) {
|
|||||||
object EditAssignment : Screen("edit_assignment/{assignmentId}") {
|
object EditAssignment : Screen("edit_assignment/{assignmentId}") {
|
||||||
fun createRoute(id: Int) = "edit_assignment/$id"
|
fun createRoute(id: Int) = "edit_assignment/$id"
|
||||||
}
|
}
|
||||||
|
object AvailableSaiList : Screen("available_sai_list")
|
||||||
|
object QuickInfo : Screen("quick_info")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -42,7 +47,9 @@ fun AppNavigation() {
|
|||||||
onNavigateToSais = { navController.navigate(Screen.SaiList.route) },
|
onNavigateToSais = { navController.navigate(Screen.SaiList.route) },
|
||||||
onNavigateToBatteries = { navController.navigate(Screen.BatteryList.route) },
|
onNavigateToBatteries = { navController.navigate(Screen.BatteryList.route) },
|
||||||
onNavigateToPeople = { navController.navigate(Screen.PersonList.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(
|
SaiListScreen(
|
||||||
onNavigateBack = { navController.popBackStack() },
|
onNavigateBack = { navController.popBackStack() },
|
||||||
onNavigateToScan = { navController.navigate(Screen.SaiScanner.route) },
|
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) {
|
composable(Screen.SaiScanner.route) {
|
||||||
SaiScannerScreen(
|
SaiScannerScreen(onNavigateBack = { navController.popBackStack() })
|
||||||
onNavigateBack = { navController.popBackStack() }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
composable(
|
composable(
|
||||||
route = Screen.EditSai.route,
|
route = Screen.EditSai.route,
|
||||||
@@ -66,6 +72,13 @@ fun AppNavigation() {
|
|||||||
val id = backStackEntry.arguments?.getInt("saiId") ?: 0
|
val id = backStackEntry.arguments?.getInt("saiId") ?: 0
|
||||||
EditSaiScreen(saiId = id, onNavigateBack = { navController.popBackStack() })
|
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
|
// Batteries
|
||||||
composable(Screen.BatteryList.route) {
|
composable(Screen.BatteryList.route) {
|
||||||
@@ -123,5 +136,11 @@ fun AppNavigation() {
|
|||||||
val id = backStackEntry.arguments?.getInt("assignmentId") ?: 0
|
val id = backStackEntry.arguments?.getInt("assignmentId") ?: 0
|
||||||
EditAssignmentScreen(assignmentId = id, onNavigateBack = { navController.popBackStack() })
|
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
|
@Composable
|
||||||
fun ConnectionIndicator(
|
fun ConnectionIndicator(
|
||||||
modifier: Modifier = Modifier,
|
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.ArrowBack
|
||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
import androidx.compose.material.icons.filled.Edit
|
import androidx.compose.material.icons.filled.Edit
|
||||||
|
import androidx.compose.material.icons.filled.Share
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.example.saipp.R
|
||||||
import com.example.saipp.data.AssignmentRepository
|
import com.example.saipp.data.AssignmentRepository
|
||||||
import com.example.saipp.data.model.Assignment
|
import com.example.saipp.data.model.Assignment
|
||||||
import com.example.saipp.ui.ScannerDialog
|
import com.example.saipp.ui.ScannerDialog
|
||||||
import com.example.saipp.ui.SearchBar
|
import com.example.saipp.ui.SearchBar
|
||||||
|
import com.example.saipp.ui.DeleteConfirmDialog
|
||||||
|
import com.example.saipp.util.PdfGenerator
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@@ -37,6 +43,9 @@ fun AssignmentListScreen(
|
|||||||
var isLoading by remember { mutableStateOf(true) }
|
var isLoading by remember { mutableStateOf(true) }
|
||||||
var searchQuery by remember { mutableStateOf("") }
|
var searchQuery by remember { mutableStateOf("") }
|
||||||
var showQuickScanner by remember { mutableStateOf(false) }
|
var showQuickScanner by remember { mutableStateOf(false) }
|
||||||
|
var hideInactive by remember { mutableStateOf(true) }
|
||||||
|
|
||||||
|
var itemToDelete by remember { mutableStateOf<Assignment?>(null) }
|
||||||
|
|
||||||
fun loadItems() {
|
fun loadItems() {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
@@ -52,11 +61,15 @@ fun AssignmentListScreen(
|
|||||||
loadItems()
|
loadItems()
|
||||||
}
|
}
|
||||||
|
|
||||||
val filteredItems = remember(items, searchQuery) {
|
val filteredItems = remember(items, searchQuery, hideInactive) {
|
||||||
items.filter {
|
items.filter {
|
||||||
it.personName?.contains(searchQuery, ignoreCase = true) ?: false ||
|
val matchesSearch = it.personName?.contains(searchQuery, ignoreCase = true) ?: false ||
|
||||||
it.saiNs?.contains(searchQuery, ignoreCase = true) ?: false ||
|
it.saiNs?.contains(searchQuery, ignoreCase = true) ?: false ||
|
||||||
it.observations?.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 = {
|
||||||
FloatingActionButton(onClick = onNavigateToAdd) {
|
FloatingActionButton(onClick = onNavigateToAdd) {
|
||||||
Icon(Icons.Default.Add, contentDescription = "Añadir SAI - Persona")
|
Icon(Icons.Default.Add, contentDescription = "Añadir Asignación")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
) { padding ->
|
) { padding ->
|
||||||
@@ -94,9 +107,38 @@ fun AssignmentListScreen(
|
|||||||
query = searchQuery,
|
query = searchQuery,
|
||||||
onQueryChange = { searchQuery = it },
|
onQueryChange = { searchQuery = it },
|
||||||
onScanClick = { showQuickScanner = true },
|
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) {
|
if (showQuickScanner) {
|
||||||
ScannerDialog(
|
ScannerDialog(
|
||||||
onDismiss = { showQuickScanner = false },
|
onDismiss = { showQuickScanner = false },
|
||||||
@@ -112,38 +154,51 @@ fun AssignmentListScreen(
|
|||||||
CircularProgressIndicator()
|
CircularProgressIndicator()
|
||||||
}
|
}
|
||||||
} else {
|
} 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 ->
|
items(filteredItems) { item ->
|
||||||
ListItem(
|
Card(
|
||||||
headlineContent = { Text(item.personName ?: "Persona desconocida") },
|
modifier = Modifier.fillMaxWidth(),
|
||||||
supportingContent = {
|
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
||||||
Column {
|
) {
|
||||||
Text("SAI: ${item.saiNs ?: "N/A"}")
|
ListItem(
|
||||||
Text("Asignado: ${formatDate(item.assignedDate)}")
|
headlineContent = {
|
||||||
if (item.returnDate != null) {
|
Text(
|
||||||
Text("Devolución: ${formatDate(item.returnDate)}")
|
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 = { PdfGenerator.generateReceipt(context, item) }) {
|
||||||
},
|
Icon(imageVector = Icons.Default.Share, contentDescription = "Generar Recibo", tint = MaterialTheme.colorScheme.tertiary)
|
||||||
trailingContent = {
|
}
|
||||||
Row {
|
IconButton(onClick = { onNavigateToEdit(item.id) }) {
|
||||||
IconButton(onClick = { onNavigateToEdit(item.id) }) {
|
Icon(Icons.Default.Edit, contentDescription = "Editar", tint = MaterialTheme.colorScheme.primary)
|
||||||
Icon(Icons.Default.Edit, contentDescription = "Editar")
|
}
|
||||||
}
|
IconButton(onClick = { itemToDelete = item }) {
|
||||||
IconButton(onClick = {
|
Icon(Icons.Default.Delete, contentDescription = "Eliminar", tint = MaterialTheme.colorScheme.error)
|
||||||
scope.launch {
|
|
||||||
repository.delete(item.id).onSuccess { loadItems() }
|
|
||||||
}
|
}
|
||||||
}) {
|
|
||||||
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.BatteryRepository
|
||||||
import com.example.saipp.data.model.Battery
|
import com.example.saipp.data.model.Battery
|
||||||
import com.example.saipp.ui.SearchBar
|
import com.example.saipp.ui.SearchBar
|
||||||
|
import com.example.saipp.ui.DeleteConfirmDialog
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@@ -36,6 +37,8 @@ fun BatteryListScreen(
|
|||||||
var isLoading by remember { mutableStateOf(true) }
|
var isLoading by remember { mutableStateOf(true) }
|
||||||
var searchQuery by remember { mutableStateOf("") }
|
var searchQuery by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
var batteryToDelete by remember { mutableStateOf<Battery?>(null) }
|
||||||
|
|
||||||
fun loadItems() {
|
fun loadItems() {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
@@ -93,37 +96,54 @@ fun BatteryListScreen(
|
|||||||
modifier = Modifier.padding(16.dp)
|
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) {
|
if (isLoading) {
|
||||||
Box(modifier = Modifier.weight(1f).fillMaxWidth(), contentAlignment = Alignment.Center) {
|
Box(modifier = Modifier.weight(1f).fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||||
CircularProgressIndicator()
|
CircularProgressIndicator()
|
||||||
}
|
}
|
||||||
} else {
|
} 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 ->
|
items(filteredItems) { item ->
|
||||||
ListItem(
|
Card(
|
||||||
headlineContent = { Text("Marca: ${item.brand ?: "Desconocida"}") },
|
modifier = Modifier.fillMaxWidth(),
|
||||||
supportingContent = {
|
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
||||||
Column {
|
) {
|
||||||
Text("Instalación: ${formatDate(item.installDate)}")
|
ListItem(
|
||||||
item.idSai?.let { Text("SAI ID: $it") }
|
headlineContent = { Text("Marca: ${item.brand ?: "Desconocida"}", fontWeight = androidx.compose.ui.text.font.FontWeight.Bold) },
|
||||||
}
|
supportingContent = {
|
||||||
},
|
Column {
|
||||||
trailingContent = {
|
Text("Instalación: ${formatDate(item.installDate)}")
|
||||||
Row {
|
item.idSai?.let { Text("SAI ID: $it") }
|
||||||
IconButton(onClick = { onNavigateToEdit(item.id) }) {
|
|
||||||
Icon(Icons.Default.Edit, contentDescription = "Editar")
|
|
||||||
}
|
}
|
||||||
IconButton(onClick = {
|
},
|
||||||
scope.launch {
|
trailingContent = {
|
||||||
repository.delete(item.id).onSuccess { loadItems() }
|
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 ns by remember { mutableStateOf("") }
|
||||||
var modelo by remember { mutableStateOf("") }
|
var modelo by remember { mutableStateOf("") }
|
||||||
|
var estado by remember { mutableStateOf("Disponible") }
|
||||||
var isLoading by remember { mutableStateOf(true) }
|
var isLoading by remember { mutableStateOf(true) }
|
||||||
var isSaving by remember { mutableStateOf(false) }
|
var isSaving by remember { mutableStateOf(false) }
|
||||||
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val statusOptions = listOf("Disponible", "Asignado", "En reparación", "Baja")
|
||||||
|
|
||||||
LaunchedEffect(saiId) {
|
LaunchedEffect(saiId) {
|
||||||
repository.getById(saiId).onSuccess {
|
repository.getById(saiId).onSuccess {
|
||||||
ns = it.ns
|
ns = it.ns
|
||||||
modelo = it.modelo ?: ""
|
modelo = it.modelo ?: ""
|
||||||
|
estado = it.estado
|
||||||
isLoading = false
|
isLoading = false
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
Toast.makeText(context, "Error al cargar SAI", Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, "Error al cargar SAI", Toast.LENGTH_SHORT).show()
|
||||||
@@ -77,13 +82,44 @@ fun EditSaiScreen(
|
|||||||
label = { Text(stringResource(R.string.label_modelo)) },
|
label = { Text(stringResource(R.string.label_modelo)) },
|
||||||
modifier = Modifier.fillMaxWidth()
|
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))
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (ns.isNotBlank()) {
|
if (ns.isNotBlank()) {
|
||||||
isSaving = true
|
isSaving = true
|
||||||
scope.launch {
|
scope.launch {
|
||||||
repository.update(Sai(id = saiId, ns = ns, modelo = modelo))
|
repository.update(Sai(id = saiId, ns = ns, modelo = modelo, estado = estado))
|
||||||
.onSuccess {
|
.onSuccess {
|
||||||
Toast.makeText(context, "SAI actualizado", Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, "SAI actualizado", Toast.LENGTH_SHORT).show()
|
||||||
onNavigateBack()
|
onNavigateBack()
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
package com.example.saipp.ui.management
|
package com.example.saipp.ui.management
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.*
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
import com.example.saipp.ui.ConnectionIndicator
|
import com.example.saipp.ui.ConnectionIndicator
|
||||||
import com.example.saipp.R
|
import com.example.saipp.R
|
||||||
import com.example.saipp.data.MariaDbConnection
|
import com.example.saipp.data.*
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -17,14 +22,31 @@ fun HomeScreen(
|
|||||||
onNavigateToSais: () -> Unit,
|
onNavigateToSais: () -> Unit,
|
||||||
onNavigateToBatteries: () -> Unit,
|
onNavigateToBatteries: () -> Unit,
|
||||||
onNavigateToPeople: () -> Unit,
|
onNavigateToPeople: () -> Unit,
|
||||||
onNavigateToAssignments: () -> Unit
|
onNavigateToAssignments: () -> Unit,
|
||||||
|
onNavigateToAvailableSais: () -> Unit,
|
||||||
|
onNavigateToQuickInfo: () -> Unit
|
||||||
) {
|
) {
|
||||||
var connectionStatus by remember { mutableStateOf<Result<Unit>?>(null) }
|
var connectionStatus by remember { mutableStateOf<Result<Unit>?>(null) }
|
||||||
var isCheckingConnection by remember { mutableStateOf(true) }
|
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) {
|
LaunchedEffect(Unit) {
|
||||||
isCheckingConnection = true
|
isCheckingConnection = true
|
||||||
connectionStatus = MariaDbConnection.testConnection()
|
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
|
isCheckingConnection = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,61 +59,163 @@ fun HomeScreen(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
.padding(16.dp),
|
.padding(horizontal = 16.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
verticalArrangement = Arrangement.Center
|
|
||||||
) {
|
) {
|
||||||
ConnectionIndicator(
|
ConnectionIndicator(
|
||||||
isChecking = isCheckingConnection,
|
isChecking = isCheckingConnection,
|
||||||
status = connectionStatus
|
status = connectionStatus
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(32.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
MenuButton(
|
// Dashboard Stats
|
||||||
text = stringResource(R.string.menu_sais),
|
StatsDashboard(saiStats, batteryCount, peopleCount)
|
||||||
onClick = onNavigateToSais,
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
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))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
MenuButton(
|
// Main Menu Grid
|
||||||
text = stringResource(R.string.menu_batteries),
|
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
onClick = onNavigateToBatteries,
|
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
modifier = Modifier.fillMaxWidth()
|
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))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
MenuButton(
|
Button(
|
||||||
text = stringResource(R.string.menu_people),
|
onClick = onNavigateToAvailableSais,
|
||||||
onClick = onNavigateToPeople,
|
modifier = Modifier.fillMaxWidth().height(56.dp),
|
||||||
modifier = Modifier.fillMaxWidth()
|
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))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
MenuButton(
|
|
||||||
text = stringResource(R.string.menu_assignments),
|
|
||||||
onClick = onNavigateToAssignments,
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MenuButton(
|
fun MaintenanceAlert(count: Int) {
|
||||||
text: String,
|
Card(
|
||||||
onClick: () -> Unit,
|
modifier = Modifier.fillMaxWidth(),
|
||||||
modifier: Modifier = Modifier
|
colors = CardDefaults.cardColors(
|
||||||
) {
|
containerColor = MaterialTheme.colorScheme.errorContainer,
|
||||||
Button(
|
contentColor = MaterialTheme.colorScheme.onErrorContainer
|
||||||
onClick = onClick,
|
)
|
||||||
modifier = modifier.height(64.dp),
|
|
||||||
shape = MaterialTheme.shapes.medium
|
|
||||||
) {
|
) {
|
||||||
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.ArrowBack
|
||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
import androidx.compose.material.icons.filled.Edit
|
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.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.example.saipp.data.PersonRepository
|
import com.example.saipp.data.PersonRepository
|
||||||
import com.example.saipp.data.model.Person
|
import com.example.saipp.data.model.Person
|
||||||
import com.example.saipp.ui.SearchBar
|
import com.example.saipp.ui.SearchBar
|
||||||
|
import com.example.saipp.ui.DeleteConfirmDialog
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@@ -34,6 +38,8 @@ fun PersonListScreen(
|
|||||||
var isLoading by remember { mutableStateOf(true) }
|
var isLoading by remember { mutableStateOf(true) }
|
||||||
var searchQuery by remember { mutableStateOf("") }
|
var searchQuery by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
var personToDelete by remember { mutableStateOf<Person?>(null) }
|
||||||
|
|
||||||
fun loadItems() {
|
fun loadItems() {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
@@ -76,31 +82,71 @@ fun PersonListScreen(
|
|||||||
modifier = Modifier.padding(16.dp)
|
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) {
|
if (isLoading) {
|
||||||
Box(modifier = Modifier.weight(1f).fillMaxWidth(), contentAlignment = Alignment.Center) {
|
Box(modifier = Modifier.weight(1f).fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||||
CircularProgressIndicator()
|
CircularProgressIndicator()
|
||||||
}
|
}
|
||||||
} else {
|
} 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 ->
|
items(filteredItems) { item ->
|
||||||
ListItem(
|
Card(
|
||||||
headlineContent = { Text(item.name) },
|
modifier = Modifier.fillMaxWidth(),
|
||||||
trailingContent = {
|
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
||||||
Row {
|
) {
|
||||||
IconButton(onClick = { onNavigateToEdit(item.id) }) {
|
ListItem(
|
||||||
Icon(Icons.Default.Edit, contentDescription = "Editar")
|
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 {
|
trailingContent = {
|
||||||
repository.delete(item.id).onSuccess { loadItems() }
|
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.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.*
|
||||||
import androidx.compose.material.icons.filled.ArrowBack
|
|
||||||
import androidx.compose.material.icons.filled.Delete
|
|
||||||
import androidx.compose.material.icons.filled.Edit
|
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.example.saipp.data.SaiRepository
|
import com.example.saipp.data.SaiRepository
|
||||||
import com.example.saipp.data.model.Sai
|
import com.example.saipp.data.model.Sai
|
||||||
import com.example.saipp.ui.ScannerDialog
|
|
||||||
import com.example.saipp.ui.SearchBar
|
import com.example.saipp.ui.SearchBar
|
||||||
|
import com.example.saipp.ui.ScannerDialog
|
||||||
|
import com.example.saipp.ui.DeleteConfirmDialog
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@@ -26,7 +25,8 @@ import kotlinx.coroutines.launch
|
|||||||
fun SaiListScreen(
|
fun SaiListScreen(
|
||||||
onNavigateBack: () -> Unit,
|
onNavigateBack: () -> Unit,
|
||||||
onNavigateToScan: () -> Unit,
|
onNavigateToScan: () -> Unit,
|
||||||
onNavigateToEdit: (Int) -> Unit
|
onNavigateToEdit: (Int) -> Unit,
|
||||||
|
onNavigateToDetail: (Int) -> Unit
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
@@ -36,6 +36,8 @@ fun SaiListScreen(
|
|||||||
var searchQuery by remember { mutableStateOf("") }
|
var searchQuery by remember { mutableStateOf("") }
|
||||||
var showQuickScanner by remember { mutableStateOf(false) }
|
var showQuickScanner by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
var saiToDelete by remember { mutableStateOf<Sai?>(null) }
|
||||||
|
|
||||||
fun loadSais() {
|
fun loadSais() {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
@@ -92,32 +94,71 @@ 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) {
|
if (isLoading) {
|
||||||
Box(modifier = Modifier.weight(1f).fillMaxWidth(), contentAlignment = Alignment.Center) {
|
Box(modifier = Modifier.weight(1f).fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||||
CircularProgressIndicator()
|
CircularProgressIndicator()
|
||||||
}
|
}
|
||||||
} else {
|
} 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 ->
|
items(filteredSais) { sai ->
|
||||||
ListItem(
|
Card(
|
||||||
headlineContent = { Text("NS: ${sai.ns}") },
|
modifier = Modifier.fillMaxWidth(),
|
||||||
supportingContent = { Text("Modelo: ${sai.modelo ?: "N/A"}") },
|
onClick = { onNavigateToDetail(sai.id) },
|
||||||
trailingContent = {
|
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
||||||
Row {
|
) {
|
||||||
IconButton(onClick = { onNavigateToEdit(sai.id) }) {
|
ListItem(
|
||||||
Icon(Icons.Default.Edit, contentDescription = "Editar")
|
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 {
|
trailingContent = {
|
||||||
repository.delete(sai.id).onSuccess { loadSais() }
|
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="placeholder_search">Buscar...</string>
|
||||||
<string name="error_duplicate_sai">Este SAI ya está registrado en el sistema.</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="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_sais">Gestión de SAIs</string>
|
||||||
<string name="menu_batteries">Gestión de Baterías</string>
|
<string name="menu_batteries">Gestión de Baterías</string>
|
||||||
<string name="menu_people">Gestión de Personas</string>
|
<string name="menu_people">Gestión de Personas</string>
|
||||||
<string name="menu_assignments">Gestion SAI - Persona</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_person">Persona</string>
|
||||||
<string name="label_assigned_date">Fecha Asignación (DD-MM-AAAA)</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>
|
||||||
@@ -12,6 +12,7 @@ cameraX = "1.6.1"
|
|||||||
mlkitBarcode = "17.3.0"
|
mlkitBarcode = "17.3.0"
|
||||||
mariadbDriver = "3.5.1"
|
mariadbDriver = "3.5.1"
|
||||||
navigationCompose = "2.9.8"
|
navigationCompose = "2.9.8"
|
||||||
|
coreSplashscreen = "1.0.1"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||||
@@ -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-material3 = { group = "androidx.compose.material3", name = "material3" }
|
||||||
androidx-compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" }
|
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-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-core = { group = "androidx.camera", name = "camera-core", version.ref = "cameraX" }
|
||||||
androidx-camera-camera2 = { group = "androidx.camera", name = "camera-camera2", version.ref = "cameraX" }
|
androidx-camera-camera2 = { group = "androidx.camera", name = "camera-camera2", version.ref = "cameraX" }
|
||||||
androidx-camera-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", version.ref = "cameraX" }
|
androidx-camera-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", version.ref = "cameraX" }
|
||||||
|
|||||||
Reference in New Issue
Block a user