Nueva opcion si esta en nevera, congelador u otro lugar
This commit is contained in:
@@ -4,10 +4,12 @@ Aplicación Android moderna y colaborativa diseñada para gestionar el inventari
|
|||||||
|
|
||||||
## 🚀 Novedades Recientes
|
## 🚀 Novedades Recientes
|
||||||
* **Seguimiento de Cambios:** Ahora puedes ver qué miembro de la familia realizó la última modificación en cada producto.
|
* **Seguimiento de Cambios:** Ahora puedes ver qué miembro de la familia realizó la última modificación en cada producto.
|
||||||
|
* **Easy Family Sync:** Invita a miembros de tu familia compartiendo un código directamente desde los ajustes.
|
||||||
* **Historial de Precios:** Seguimiento de la evolución de precios por supermercado.
|
* **Historial de Precios:** Seguimiento de la evolución de precios por supermercado.
|
||||||
* **Comparativa de Supermercados:** Identifica dónde encontraste el producto por última vez y a qué precio.
|
* **Comparativa de Supermercados:** Identifica dónde encontraste el producto por última vez y a qué precio.
|
||||||
* **Configuración de Alertas:** Frecuencia de notificaciones personalizable (12h, 24h, 48h o Semanal).
|
* **Configuración de Alertas:** Frecuencia de notificaciones personalizable (12h, 24h, 48h o Semanal).
|
||||||
* **Gestión de Imágenes:** Sube tus propias fotos desde la galería o haz una foto directamente si el producto no tiene imagen en Open Food Facts.
|
* **Gestión de Imágenes:** Sube tus propias fotos desde la galería o haz una foto directamente si el producto no tiene imagen en Open Food Facts.
|
||||||
|
* **Inventario por Zonas (Smart Zoned Inventory):** Organiza tus productos por ubicación (Despensa, Nevera, Congelador, Baño, etc.) para un control más preciso.
|
||||||
* **Valoración Financiera:** Visualiza el valor total de tu despensa y el presupuesto necesario para reponer lo que falta.
|
* **Valoración Financiera:** Visualiza el valor total de tu despensa y el presupuesto necesario para reponer lo que falta.
|
||||||
|
|
||||||
## 🛠 Funcionalidades Principales
|
## 🛠 Funcionalidades Principales
|
||||||
|
|||||||
@@ -7,8 +7,11 @@ data class Product(
|
|||||||
val minQuantity: Int,
|
val minQuantity: Int,
|
||||||
val unit: String = "unidad",
|
val unit: String = "unidad",
|
||||||
val category: String = "General",
|
val category: String = "General",
|
||||||
|
val location: String = "Despensa",
|
||||||
val price: Double = 0.0,
|
val price: Double = 0.0,
|
||||||
val lastSupermarket: String? = null,
|
val lastSupermarket: String? = null,
|
||||||
|
val bestPrice: Double? = null,
|
||||||
|
val bestPriceSupermarket: String? = null,
|
||||||
val imageUrl: String? = null,
|
val imageUrl: String? = null,
|
||||||
val expirationDate: String? = null,
|
val expirationDate: String? = null,
|
||||||
val createdAt: String? = null,
|
val createdAt: String? = null,
|
||||||
|
|||||||
@@ -51,11 +51,18 @@ class PantryRepository {
|
|||||||
return withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val connection = getConnection()
|
val connection = getConnection()
|
||||||
val sql = if (familyCode != null) {
|
val sql = """
|
||||||
"SELECT * FROM productos WHERE visible = 1 AND codigo_familia = ?"
|
SELECT p.*, h.precio as mejor_precio, h.supermercado as mejor_supermercado
|
||||||
} else {
|
FROM productos p
|
||||||
"SELECT * FROM productos WHERE visible = 1"
|
LEFT JOIN historial_precios h ON h.id = (
|
||||||
}
|
SELECT id FROM historial_precios
|
||||||
|
WHERE id_producto = p.id
|
||||||
|
ORDER BY precio ASC, fecha DESC
|
||||||
|
LIMIT 1
|
||||||
|
)
|
||||||
|
WHERE p.visible = 1 ${if (familyCode != null) "AND p.codigo_familia = ?" else ""}
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
val statement = connection.prepareStatement(sql)
|
val statement = connection.prepareStatement(sql)
|
||||||
if (familyCode != null) {
|
if (familyCode != null) {
|
||||||
statement.setString(1, familyCode)
|
statement.setString(1, familyCode)
|
||||||
@@ -72,8 +79,11 @@ class PantryRepository {
|
|||||||
minQuantity = resultSet.getInt("cantidad_minima"),
|
minQuantity = resultSet.getInt("cantidad_minima"),
|
||||||
unit = resultSet.getString("unidad") ?: "unidad",
|
unit = resultSet.getString("unidad") ?: "unidad",
|
||||||
category = resultSet.getString("categoria"),
|
category = resultSet.getString("categoria"),
|
||||||
|
location = resultSet.getString("ubicacion") ?: "Despensa",
|
||||||
price = resultSet.getDouble("precio"),
|
price = resultSet.getDouble("precio"),
|
||||||
lastSupermarket = resultSet.getString("ultimo_supermercado"),
|
lastSupermarket = resultSet.getString("ultimo_supermercado"),
|
||||||
|
bestPrice = if (resultSet.getObject("mejor_precio") != null) resultSet.getDouble("mejor_precio") else null,
|
||||||
|
bestPriceSupermarket = resultSet.getString("mejor_supermercado"),
|
||||||
imageUrl = resultSet.getString("imagen_url"),
|
imageUrl = resultSet.getString("imagen_url"),
|
||||||
expirationDate = resultSet.getString("fecha_caducidad"),
|
expirationDate = resultSet.getString("fecha_caducidad"),
|
||||||
createdAt = resultSet.getString("fecha_alta"),
|
createdAt = resultSet.getString("fecha_alta"),
|
||||||
@@ -140,6 +150,7 @@ class PantryRepository {
|
|||||||
minCantidad: Int,
|
minCantidad: Int,
|
||||||
unidad: String,
|
unidad: String,
|
||||||
category: String,
|
category: String,
|
||||||
|
location: String,
|
||||||
price: Double,
|
price: Double,
|
||||||
supermarket: String? = null,
|
supermarket: String? = null,
|
||||||
fechaCaducidad: String?,
|
fechaCaducidad: String?,
|
||||||
@@ -149,19 +160,20 @@ class PantryRepository {
|
|||||||
return withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val connection = getConnection()
|
val connection = getConnection()
|
||||||
val sql = "UPDATE productos SET nombre = ?, cantidad = ?, cantidad_minima = ?, unidad = ?, categoria = ?, precio = ?, ultimo_supermercado = ?, fecha_caducidad = ?, imagen_url = ?, visible = 1, modificado_por = ? WHERE id = ?"
|
val sql = "UPDATE productos SET nombre = ?, cantidad = ?, cantidad_minima = ?, unidad = ?, categoria = ?, ubicacion = ?, precio = ?, ultimo_supermercado = ?, fecha_caducidad = ?, imagen_url = ?, visible = 1, modificado_por = ? WHERE id = ?"
|
||||||
val statement = connection.prepareStatement(sql)
|
val statement = connection.prepareStatement(sql)
|
||||||
statement.setString(1, nombre)
|
statement.setString(1, nombre)
|
||||||
statement.setInt(2, cantidad)
|
statement.setInt(2, cantidad)
|
||||||
statement.setInt(3, minCantidad)
|
statement.setInt(3, minCantidad)
|
||||||
statement.setString(4, unidad)
|
statement.setString(4, unidad)
|
||||||
statement.setString(5, category)
|
statement.setString(5, category)
|
||||||
statement.setDouble(6, price)
|
statement.setString(6, location)
|
||||||
statement.setString(7, supermarket)
|
statement.setDouble(7, price)
|
||||||
statement.setString(8, fechaCaducidad)
|
statement.setString(8, supermarket)
|
||||||
statement.setString(9, imageUrl)
|
statement.setString(9, fechaCaducidad)
|
||||||
statement.setString(10, userName)
|
statement.setString(10, imageUrl)
|
||||||
statement.setInt(11, productId)
|
statement.setString(11, userName)
|
||||||
|
statement.setInt(12, productId)
|
||||||
|
|
||||||
val rowsUpdated = statement.executeUpdate()
|
val rowsUpdated = statement.executeUpdate()
|
||||||
|
|
||||||
@@ -184,6 +196,7 @@ class PantryRepository {
|
|||||||
minCantidad: Int,
|
minCantidad: Int,
|
||||||
unidad: String,
|
unidad: String,
|
||||||
categoria: String,
|
categoria: String,
|
||||||
|
location: String,
|
||||||
price: Double,
|
price: Double,
|
||||||
supermarket: String? = null,
|
supermarket: String? = null,
|
||||||
imageUrl: String?,
|
imageUrl: String?,
|
||||||
@@ -194,14 +207,15 @@ class PantryRepository {
|
|||||||
try {
|
try {
|
||||||
val connection = getConnection()
|
val connection = getConnection()
|
||||||
val sql = """
|
val sql = """
|
||||||
INSERT INTO productos (barcode, nombre, cantidad, cantidad_minima, unidad, categoria, precio, ultimo_supermercado, imagen_url, fecha_caducidad, visible, modificado_por)
|
INSERT INTO productos (barcode, nombre, cantidad, cantidad_minima, unidad, categoria, ubicacion, precio, ultimo_supermercado, imagen_url, fecha_caducidad, visible, modificado_por)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?)
|
||||||
ON DUPLICATE KEY UPDATE
|
ON DUPLICATE KEY UPDATE
|
||||||
nombre = VALUES(nombre),
|
nombre = VALUES(nombre),
|
||||||
cantidad = VALUES(cantidad),
|
cantidad = VALUES(cantidad),
|
||||||
cantidad_minima = VALUES(cantidad_minima),
|
cantidad_minima = VALUES(cantidad_minima),
|
||||||
unidad = VALUES(unidad),
|
unidad = VALUES(unidad),
|
||||||
categoria = VALUES(categoria),
|
categoria = VALUES(categoria),
|
||||||
|
ubicacion = VALUES(ubicacion),
|
||||||
precio = VALUES(precio),
|
precio = VALUES(precio),
|
||||||
ultimo_supermercado = VALUES(ultimo_supermercado),
|
ultimo_supermercado = VALUES(ultimo_supermercado),
|
||||||
imagen_url = IF(VALUES(imagen_url) IS NULL, imagen_url, VALUES(imagen_url)),
|
imagen_url = IF(VALUES(imagen_url) IS NULL, imagen_url, VALUES(imagen_url)),
|
||||||
@@ -217,11 +231,12 @@ class PantryRepository {
|
|||||||
statement.setInt(4, minCantidad)
|
statement.setInt(4, minCantidad)
|
||||||
statement.setString(5, unidad)
|
statement.setString(5, unidad)
|
||||||
statement.setString(6, categoria)
|
statement.setString(6, categoria)
|
||||||
statement.setDouble(7, price)
|
statement.setString(7, location)
|
||||||
statement.setString(8, supermarket)
|
statement.setDouble(8, price)
|
||||||
statement.setString(9, imageUrl)
|
statement.setString(9, supermarket)
|
||||||
statement.setString(10, fechaCaducidad)
|
statement.setString(10, imageUrl)
|
||||||
statement.setString(11, userName)
|
statement.setString(11, fechaCaducidad)
|
||||||
|
statement.setString(12, userName)
|
||||||
|
|
||||||
val rowsInserted = statement.executeUpdate()
|
val rowsInserted = statement.executeUpdate()
|
||||||
|
|
||||||
@@ -275,7 +290,17 @@ class PantryRepository {
|
|||||||
return withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val connection = getConnection()
|
val connection = getConnection()
|
||||||
val sql = "SELECT * FROM productos WHERE id = ?"
|
val sql = """
|
||||||
|
SELECT p.*, h.precio as mejor_precio, h.supermercado as mejor_supermercado
|
||||||
|
FROM productos p
|
||||||
|
LEFT JOIN historial_precios h ON h.id = (
|
||||||
|
SELECT id FROM historial_precios
|
||||||
|
WHERE id_producto = p.id
|
||||||
|
ORDER BY precio ASC, fecha DESC
|
||||||
|
LIMIT 1
|
||||||
|
)
|
||||||
|
WHERE p.id = ?
|
||||||
|
""".trimIndent()
|
||||||
val statement = connection.prepareStatement(sql)
|
val statement = connection.prepareStatement(sql)
|
||||||
statement.setInt(1, productId)
|
statement.setInt(1, productId)
|
||||||
val resultSet = statement.executeQuery()
|
val resultSet = statement.executeQuery()
|
||||||
@@ -289,8 +314,11 @@ class PantryRepository {
|
|||||||
minQuantity = resultSet.getInt("cantidad_minima"),
|
minQuantity = resultSet.getInt("cantidad_minima"),
|
||||||
unit = resultSet.getString("unidad") ?: "unidad",
|
unit = resultSet.getString("unidad") ?: "unidad",
|
||||||
category = resultSet.getString("categoria"),
|
category = resultSet.getString("categoria"),
|
||||||
|
location = resultSet.getString("ubicacion") ?: "Despensa",
|
||||||
price = resultSet.getDouble("precio"),
|
price = resultSet.getDouble("precio"),
|
||||||
lastSupermarket = resultSet.getString("ultimo_supermercado"),
|
lastSupermarket = resultSet.getString("ultimo_supermercado"),
|
||||||
|
bestPrice = if (resultSet.getObject("mejor_precio") != null) resultSet.getDouble("mejor_precio") else null,
|
||||||
|
bestPriceSupermarket = resultSet.getString("mejor_supermercado"),
|
||||||
imageUrl = resultSet.getString("imagen_url"),
|
imageUrl = resultSet.getString("imagen_url"),
|
||||||
expirationDate = resultSet.getString("fecha_caducidad"),
|
expirationDate = resultSet.getString("fecha_caducidad"),
|
||||||
createdAt = resultSet.getString("fecha_alta"),
|
createdAt = resultSet.getString("fecha_alta"),
|
||||||
@@ -309,7 +337,17 @@ class PantryRepository {
|
|||||||
return withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val connection = getConnection()
|
val connection = getConnection()
|
||||||
val sql = "SELECT * FROM productos WHERE barcode = ?"
|
val sql = """
|
||||||
|
SELECT p.*, h.precio as mejor_precio, h.supermercado as mejor_supermercado
|
||||||
|
FROM productos p
|
||||||
|
LEFT JOIN historial_precios h ON h.id = (
|
||||||
|
SELECT id FROM historial_precios
|
||||||
|
WHERE id_producto = p.id
|
||||||
|
ORDER BY precio ASC, fecha DESC
|
||||||
|
LIMIT 1
|
||||||
|
)
|
||||||
|
WHERE p.barcode = ?
|
||||||
|
""".trimIndent()
|
||||||
val statement = connection.prepareStatement(sql)
|
val statement = connection.prepareStatement(sql)
|
||||||
statement.setString(1, barcode)
|
statement.setString(1, barcode)
|
||||||
val resultSet = statement.executeQuery()
|
val resultSet = statement.executeQuery()
|
||||||
@@ -323,8 +361,11 @@ class PantryRepository {
|
|||||||
minQuantity = resultSet.getInt("cantidad_minima"),
|
minQuantity = resultSet.getInt("cantidad_minima"),
|
||||||
unit = resultSet.getString("unidad") ?: "unidad",
|
unit = resultSet.getString("unidad") ?: "unidad",
|
||||||
category = resultSet.getString("categoria"),
|
category = resultSet.getString("categoria"),
|
||||||
|
location = resultSet.getString("ubicacion") ?: "Despensa",
|
||||||
price = resultSet.getDouble("precio"),
|
price = resultSet.getDouble("precio"),
|
||||||
lastSupermarket = resultSet.getString("ultimo_supermercado"),
|
lastSupermarket = resultSet.getString("ultimo_supermercado"),
|
||||||
|
bestPrice = if (resultSet.getObject("mejor_precio") != null) resultSet.getDouble("mejor_precio") else null,
|
||||||
|
bestPriceSupermarket = resultSet.getString("mejor_supermercado"),
|
||||||
imageUrl = resultSet.getString("imagen_url"),
|
imageUrl = resultSet.getString("imagen_url"),
|
||||||
expirationDate = resultSet.getString("fecha_caducidad"),
|
expirationDate = resultSet.getString("fecha_caducidad"),
|
||||||
createdAt = resultSet.getString("fecha_alta"),
|
createdAt = resultSet.getString("fecha_alta"),
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ fun AddProductScreen(onProductAdded: () -> Unit) {
|
|||||||
var productPrice by remember { mutableStateOf(0.0) }
|
var productPrice by remember { mutableStateOf(0.0) }
|
||||||
var productImageUrl by remember { mutableStateOf<String?>(null) }
|
var productImageUrl by remember { mutableStateOf<String?>(null) }
|
||||||
var expirationDate by remember { mutableStateOf<String?>(null) }
|
var expirationDate by remember { mutableStateOf<String?>(null) }
|
||||||
|
var selectedLocation by remember { mutableStateOf("Despensa") }
|
||||||
var quantity by remember { mutableStateOf(1) }
|
var quantity by remember { mutableStateOf(1) }
|
||||||
var minQuantity by remember { mutableStateOf(1) }
|
var minQuantity by remember { mutableStateOf(1) }
|
||||||
var selectedUnit by remember { mutableStateOf("unidad") }
|
var selectedUnit by remember { mutableStateOf("unidad") }
|
||||||
@@ -348,6 +349,49 @@ fun AddProductScreen(onProductAdded: () -> Unit) {
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
// Selector de Ubicación
|
||||||
|
val locationMap = listOf(
|
||||||
|
"Despensa" to stringResource(R.string.location_despensa),
|
||||||
|
"Nevera" to stringResource(R.string.location_nevera),
|
||||||
|
"Congelador" to stringResource(R.string.location_congelador),
|
||||||
|
"Baño/Aseo" to stringResource(R.string.location_bano),
|
||||||
|
"Otros" to stringResource(R.string.location_otro)
|
||||||
|
)
|
||||||
|
var locationExpanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
ExposedDropdownMenuBox(
|
||||||
|
expanded = locationExpanded,
|
||||||
|
onExpandedChange = { locationExpanded = !locationExpanded },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = locationMap.find { it.first == selectedLocation }?.second ?: selectedLocation,
|
||||||
|
onValueChange = {},
|
||||||
|
readOnly = true,
|
||||||
|
label = { Text(stringResource(R.string.pantry_location_label)) },
|
||||||
|
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = locationExpanded) },
|
||||||
|
modifier = Modifier.menuAnchor().fillMaxWidth(),
|
||||||
|
colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(),
|
||||||
|
leadingIcon = { Icon(Icons.Default.Place, contentDescription = null) }
|
||||||
|
)
|
||||||
|
ExposedDropdownMenu(
|
||||||
|
expanded = locationExpanded,
|
||||||
|
onDismissRequest = { locationExpanded = false }
|
||||||
|
) {
|
||||||
|
locationMap.forEach { (key, label) ->
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(label) },
|
||||||
|
onClick = {
|
||||||
|
selectedLocation = key
|
||||||
|
locationExpanded = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
// Selector de Unidad
|
// Selector de Unidad
|
||||||
ExposedDropdownMenuBox(
|
ExposedDropdownMenuBox(
|
||||||
expanded = unitsExpanded,
|
expanded = unitsExpanded,
|
||||||
@@ -418,6 +462,7 @@ fun AddProductScreen(onProductAdded: () -> Unit) {
|
|||||||
minCantidad = minQuantity,
|
minCantidad = minQuantity,
|
||||||
unidad = selectedUnit,
|
unidad = selectedUnit,
|
||||||
categoria = productCategory,
|
categoria = productCategory,
|
||||||
|
location = selectedLocation,
|
||||||
price = productPrice,
|
price = productPrice,
|
||||||
supermarket = selectedSupermarket.ifEmpty { null },
|
supermarket = selectedSupermarket.ifEmpty { null },
|
||||||
imageUrl = productImageUrl,
|
imageUrl = productImageUrl,
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ fun PantryScreen(
|
|||||||
// Filters and Search
|
// Filters and Search
|
||||||
var searchQuery by remember { mutableStateOf("") }
|
var searchQuery by remember { mutableStateOf("") }
|
||||||
var selectedCategory by remember { mutableStateOf<String?>(null) }
|
var selectedCategory by remember { mutableStateOf<String?>(null) }
|
||||||
|
var selectedLocation by remember { mutableStateOf<String?>(null) }
|
||||||
var showOnlyNearExpiry by remember { mutableStateOf(false) }
|
var showOnlyNearExpiry by remember { mutableStateOf(false) }
|
||||||
var showShoppingList by remember { mutableStateOf(false) }
|
var showShoppingList by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
@@ -82,10 +83,11 @@ fun PantryScreen(
|
|||||||
val filteredProducts = allProducts.filter { product ->
|
val filteredProducts = allProducts.filter { product ->
|
||||||
val matchesSearch = product.name.contains(searchQuery, ignoreCase = true)
|
val matchesSearch = product.name.contains(searchQuery, ignoreCase = true)
|
||||||
val matchesCategory = selectedCategory == null || product.category == selectedCategory
|
val matchesCategory = selectedCategory == null || product.category == selectedCategory
|
||||||
|
val matchesLocation = selectedLocation == null || product.location == selectedLocation
|
||||||
val matchesExpiry = !showOnlyNearExpiry || isNearExpiry(product.expirationDate)
|
val matchesExpiry = !showOnlyNearExpiry || isNearExpiry(product.expirationDate)
|
||||||
val matchesLowStock = !showShoppingList || product.isLowStock
|
val matchesLowStock = !showShoppingList || product.isLowStock
|
||||||
|
|
||||||
matchesSearch && matchesCategory && matchesExpiry && matchesLowStock
|
matchesSearch && matchesCategory && matchesLocation && matchesExpiry && matchesLowStock
|
||||||
}
|
}
|
||||||
|
|
||||||
val lowStockCount = allProducts.count { it.isLowStock }
|
val lowStockCount = allProducts.count { it.isLowStock }
|
||||||
@@ -261,6 +263,36 @@ fun PantryScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filtros de Ubicación
|
||||||
|
val locations = listOf(
|
||||||
|
"Despensa" to stringResource(R.string.location_despensa),
|
||||||
|
"Nevera" to stringResource(R.string.location_nevera),
|
||||||
|
"Congelador" to stringResource(R.string.location_congelador),
|
||||||
|
"Baño/Aseo" to stringResource(R.string.location_bano),
|
||||||
|
"Otros" to stringResource(R.string.location_otro)
|
||||||
|
)
|
||||||
|
|
||||||
|
ScrollableTabRow(
|
||||||
|
selectedTabIndex = if (selectedLocation == null) 0 else locations.indexOfFirst { it.first == selectedLocation } + 1,
|
||||||
|
edgePadding = 16.dp,
|
||||||
|
divider = {},
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
indicator = {}
|
||||||
|
) {
|
||||||
|
Tab(
|
||||||
|
selected = selectedLocation == null,
|
||||||
|
onClick = { selectedLocation = null },
|
||||||
|
text = { Text(stringResource(R.string.pantry_filter_all)) }
|
||||||
|
)
|
||||||
|
locations.forEach { (key, label) ->
|
||||||
|
Tab(
|
||||||
|
selected = selectedLocation == key,
|
||||||
|
onClick = { selectedLocation = key },
|
||||||
|
text = { Text(label) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Categorías Scrollable
|
// Categorías Scrollable
|
||||||
ScrollableTabRow(
|
ScrollableTabRow(
|
||||||
selectedTabIndex = if (selectedCategory == null) 0 else categories.indexOf(selectedCategory) + 1,
|
selectedTabIndex = if (selectedCategory == null) 0 else categories.indexOf(selectedCategory) + 1,
|
||||||
@@ -327,8 +359,6 @@ fun PantryScreen(
|
|||||||
product = product,
|
product = product,
|
||||||
onItemClick = { onNavigateToDetail(product.id) },
|
onItemClick = { onNavigateToDetail(product.id) },
|
||||||
onNavigateToPriceScanner = onNavigateToPriceScanner,
|
onNavigateToPriceScanner = onNavigateToPriceScanner,
|
||||||
showPriceUpdater = showShoppingList,
|
|
||||||
onUpdatePrice = { onNavigateToPriceScanner(product.id) },
|
|
||||||
onQuantityChange = { newQty ->
|
onQuantityChange = { newQty ->
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val success = repository.updateProductQuantity(product.id, newQty, userName)
|
val success = repository.updateProductQuantity(product.id, newQty, userName)
|
||||||
@@ -407,8 +437,6 @@ fun ProductItem(
|
|||||||
product: Product,
|
product: Product,
|
||||||
onItemClick: () -> Unit,
|
onItemClick: () -> Unit,
|
||||||
onNavigateToPriceScanner: (Int) -> Unit = {},
|
onNavigateToPriceScanner: (Int) -> Unit = {},
|
||||||
showPriceUpdater: Boolean = false,
|
|
||||||
onUpdatePrice: () -> Unit = {},
|
|
||||||
onQuantityChange: (Int) -> Unit
|
onQuantityChange: (Int) -> Unit
|
||||||
) {
|
) {
|
||||||
Card(
|
Card(
|
||||||
@@ -485,6 +513,20 @@ fun ProductItem(
|
|||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
color = Color.Gray
|
color = Color.Gray
|
||||||
)
|
)
|
||||||
|
// Ubicación Badge
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier.padding(top = 2.dp),
|
||||||
|
shape = MaterialTheme.shapes.extraSmall,
|
||||||
|
color = MaterialTheme.colorScheme.tertiaryContainer.copy(alpha = 0.5f),
|
||||||
|
contentColor = MaterialTheme.colorScheme.onTertiaryContainer
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = product.location,
|
||||||
|
fontSize = 10.sp,
|
||||||
|
modifier = Modifier.padding(horizontal = 4.dp, vertical = 2.dp),
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Botones de acción arriba a la derecha en columna (+ arriba, - abajo)
|
// Botones de acción arriba a la derecha en columna (+ arriba, - abajo)
|
||||||
@@ -498,16 +540,19 @@ fun ProductItem(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
|
||||||
// Nuevo Botón: Registrar solo Precio (Historial)
|
// Nuevo Botón: Registrar solo Precio (Historial) - Ahora más visible
|
||||||
IconButton(
|
FilledTonalIconButton(
|
||||||
onClick = { onNavigateToPriceScanner(product.id) },
|
onClick = { onNavigateToPriceScanner(product.id) },
|
||||||
modifier = Modifier.size(36.dp)
|
modifier = Modifier.size(36.dp),
|
||||||
|
colors = IconButtonDefaults.filledTonalIconButtonColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.LocalOffer,
|
Icons.Default.Sell,
|
||||||
contentDescription = stringResource(R.string.pantry_update_price),
|
contentDescription = stringResource(R.string.pantry_update_price),
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
modifier = Modifier.size(18.dp)
|
||||||
modifier = Modifier.size(20.dp)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -565,6 +610,28 @@ fun ProductItem(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mejor Precio Histórico (con Fallback al precio actual si no hay historial)
|
||||||
|
val displayBestPrice = product.bestPrice ?: if (product.price > 0) product.price else null
|
||||||
|
val displayBestSupermarket = product.bestPriceSupermarket ?: product.lastSupermarket
|
||||||
|
|
||||||
|
if (displayBestPrice != null && displayBestSupermarket != null) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(top = 2.dp)) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Stars,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(12.dp),
|
||||||
|
tint = Color(0xFF4CAF50) // Verde éxito
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.pantry_best_price, displayBestPrice, displayBestSupermarket),
|
||||||
|
fontSize = 11.sp,
|
||||||
|
color = Color(0xFF388E3C),
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (product.createdAt != null) {
|
if (product.createdAt != null) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -633,21 +700,6 @@ fun ProductItem(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Botón de OCR Precio (Solo en modo Lista Compra)
|
|
||||||
if (showPriceUpdater) {
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
IconButton(
|
|
||||||
onClick = onUpdatePrice,
|
|
||||||
modifier = Modifier.size(32.dp)
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
Icons.Default.QrCodeScanner,
|
|
||||||
contentDescription = stringResource(R.string.pantry_update_price),
|
|
||||||
tint = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ fun ProductDetailScreen(productId: Int, onBack: () -> Unit) {
|
|||||||
var editMinQuantity by remember { mutableStateOf(0) }
|
var editMinQuantity by remember { mutableStateOf(0) }
|
||||||
var editUnit by remember { mutableStateOf("") }
|
var editUnit by remember { mutableStateOf("") }
|
||||||
var editCategory by remember { mutableStateOf("") }
|
var editCategory by remember { mutableStateOf("") }
|
||||||
|
var editLocation by remember { mutableStateOf("Despensa") }
|
||||||
var editPrice by remember { mutableStateOf(0.0) }
|
var editPrice by remember { mutableStateOf(0.0) }
|
||||||
var editSupermarket by remember { mutableStateOf("") }
|
var editSupermarket by remember { mutableStateOf("") }
|
||||||
var editExpirationDate by remember { mutableStateOf<String?>(null) }
|
var editExpirationDate by remember { mutableStateOf<String?>(null) }
|
||||||
@@ -202,6 +203,7 @@ fun ProductDetailScreen(productId: Int, onBack: () -> Unit) {
|
|||||||
editMinQuantity = p.minQuantity
|
editMinQuantity = p.minQuantity
|
||||||
editUnit = p.unit
|
editUnit = p.unit
|
||||||
editCategory = p.category
|
editCategory = p.category
|
||||||
|
editLocation = p.location
|
||||||
editPrice = p.price
|
editPrice = p.price
|
||||||
editSupermarket = p.lastSupermarket ?: ""
|
editSupermarket = p.lastSupermarket ?: ""
|
||||||
editExpirationDate = p.expirationDate
|
editExpirationDate = p.expirationDate
|
||||||
@@ -256,6 +258,7 @@ fun ProductDetailScreen(productId: Int, onBack: () -> Unit) {
|
|||||||
minCantidad = editMinQuantity,
|
minCantidad = editMinQuantity,
|
||||||
unidad = editUnit,
|
unidad = editUnit,
|
||||||
category = editCategory,
|
category = editCategory,
|
||||||
|
location = editLocation,
|
||||||
price = editPrice,
|
price = editPrice,
|
||||||
supermarket = editSupermarket.ifEmpty { null },
|
supermarket = editSupermarket.ifEmpty { null },
|
||||||
fechaCaducidad = editExpirationDate,
|
fechaCaducidad = editExpirationDate,
|
||||||
@@ -346,6 +349,7 @@ fun ProductDetailScreen(productId: Int, onBack: () -> Unit) {
|
|||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
DetailRow(icon = Icons.Default.Inventory, label = stringResource(R.string.detail_stock_label), value = stringResource(R.string.pantry_units_label, p.quantity, p.unit))
|
DetailRow(icon = Icons.Default.Inventory, label = stringResource(R.string.detail_stock_label), value = stringResource(R.string.pantry_units_label, p.quantity, p.unit))
|
||||||
|
DetailRow(icon = Icons.Default.Place, label = stringResource(R.string.pantry_location_label), value = p.location)
|
||||||
DetailRow(icon = Icons.Default.Payments, label = "Precio unitario", value = stringResource(R.string.pantry_price_label, p.price))
|
DetailRow(icon = Icons.Default.Payments, label = "Precio unitario", value = stringResource(R.string.pantry_price_label, p.price))
|
||||||
|
|
||||||
if (p.lastSupermarket != null) {
|
if (p.lastSupermarket != null) {
|
||||||
@@ -459,6 +463,49 @@ fun ProductDetailScreen(productId: Int, onBack: () -> Unit) {
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
// Selector de Ubicación
|
||||||
|
val locationMap = listOf(
|
||||||
|
"Despensa" to stringResource(R.string.location_despensa),
|
||||||
|
"Nevera" to stringResource(R.string.location_nevera),
|
||||||
|
"Congelador" to stringResource(R.string.location_congelador),
|
||||||
|
"Baño/Aseo" to stringResource(R.string.location_bano),
|
||||||
|
"Otros" to stringResource(R.string.location_otro)
|
||||||
|
)
|
||||||
|
var locationExpanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
ExposedDropdownMenuBox(
|
||||||
|
expanded = locationExpanded,
|
||||||
|
onExpandedChange = { locationExpanded = !locationExpanded },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = locationMap.find { it.first == editLocation }?.second ?: editLocation,
|
||||||
|
onValueChange = {},
|
||||||
|
readOnly = true,
|
||||||
|
label = { Text(stringResource(R.string.pantry_location_label)) },
|
||||||
|
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = locationExpanded) },
|
||||||
|
modifier = Modifier.menuAnchor().fillMaxWidth(),
|
||||||
|
colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(),
|
||||||
|
leadingIcon = { Icon(Icons.Default.Place, contentDescription = null) }
|
||||||
|
)
|
||||||
|
ExposedDropdownMenu(
|
||||||
|
expanded = locationExpanded,
|
||||||
|
onDismissRequest = { locationExpanded = false }
|
||||||
|
) {
|
||||||
|
locationMap.forEach { (key, label) ->
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(label) },
|
||||||
|
onClick = {
|
||||||
|
editLocation = key
|
||||||
|
locationExpanded = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
SupermarketDropdown(
|
SupermarketDropdown(
|
||||||
selectedSupermarket = editSupermarket,
|
selectedSupermarket = editSupermarket,
|
||||||
onSupermarketChange = { editSupermarket = it },
|
onSupermarketChange = { editSupermarket = it },
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
package com.example.despensapp.ui.screens
|
package com.example.despensapp.ui.screens
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.selection.selectable
|
import androidx.compose.foundation.selection.selectable
|
||||||
import androidx.compose.foundation.selection.selectableGroup
|
import androidx.compose.foundation.selection.selectableGroup
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
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
|
||||||
@@ -12,6 +14,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.semantics.Role
|
import androidx.compose.ui.semantics.Role
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.example.despensapp.R
|
import com.example.despensapp.R
|
||||||
import com.example.despensapp.data.session.UserSession
|
import com.example.despensapp.data.session.UserSession
|
||||||
@@ -120,6 +123,57 @@ fun SettingsScreen(onBack: () -> Unit) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
HorizontalDivider()
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.settings_family_title),
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
modifier = Modifier.padding(bottom = 16.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
val familyCode = remember { UserSession.getFamilyCode(context) ?: "DEFAULT" }
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.settings_family_code_label),
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = familyCode,
|
||||||
|
style = MaterialTheme.typography.headlineMedium,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.padding(vertical = 8.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
val shareIntent = Intent(Intent.ACTION_SEND).apply {
|
||||||
|
type = "text/plain"
|
||||||
|
putExtra(Intent.EXTRA_TEXT, context.getString(R.string.settings_family_invite_message, familyCode))
|
||||||
|
}
|
||||||
|
context.startActivity(Intent.createChooser(shareIntent, context.getString(R.string.share)))
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.Share, contentDescription = null)
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
|
Text(stringResource(R.string.settings_family_invite_button))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,6 +70,13 @@
|
|||||||
<string name="pantry_history_title">Historial de Precios</string>
|
<string name="pantry_history_title">Historial de Precios</string>
|
||||||
<string name="pantry_no_history">No hay historial de precios registrado</string>
|
<string name="pantry_no_history">No hay historial de precios registrado</string>
|
||||||
<string name="pantry_last_supermarket">Visto en: %1$s</string>
|
<string name="pantry_last_supermarket">Visto en: %1$s</string>
|
||||||
|
<string name="pantry_best_price">Mejor: %1$.2f € (%2$s)</string>
|
||||||
|
<string name="pantry_location_label">Ubicación</string>
|
||||||
|
<string name="location_despensa">Despensa</string>
|
||||||
|
<string name="location_nevera">Nevera</string>
|
||||||
|
<string name="location_congelador">Congelador</string>
|
||||||
|
<string name="location_bano">Baño/Aseo</string>
|
||||||
|
<string name="location_otro">Otros</string>
|
||||||
|
|
||||||
<!-- Supermarkets -->
|
<!-- Supermarkets -->
|
||||||
<string name="supermarket_mercadona">MERCADONA</string>
|
<string name="supermarket_mercadona">MERCADONA</string>
|
||||||
@@ -150,6 +157,10 @@
|
|||||||
<string name="settings_freq_weekly">Una vez a la semana</string>
|
<string name="settings_freq_weekly">Una vez a la semana</string>
|
||||||
<string name="settings_save_success">Configuración guardada correctamente</string>
|
<string name="settings_save_success">Configuración guardada correctamente</string>
|
||||||
<string name="settings_biometric_enable">Habilitar desbloqueo biométrico</string>
|
<string name="settings_biometric_enable">Habilitar desbloqueo biométrico</string>
|
||||||
|
<string name="settings_family_title">Mi Familia</string>
|
||||||
|
<string name="settings_family_code_label">Tu Código de Familia:</string>
|
||||||
|
<string name="settings_family_invite_button">Invitar Familiar</string>
|
||||||
|
<string name="settings_family_invite_message">¡Hola! Únete a nuestra despensa compartida en Despensapp. \n\nUsa este código al registrarte: *%1$s* \n\n¡Así sabremos qué falta en casa en tiempo real! 🥫🛒</string>
|
||||||
<string name="biometric_title">Desbloqueo de Despensapp</string>
|
<string name="biometric_title">Desbloqueo de Despensapp</string>
|
||||||
<string name="biometric_subtitle">Usa tu huella o rostro para entrar</string>
|
<string name="biometric_subtitle">Usa tu huella o rostro para entrar</string>
|
||||||
<string name="biometric_error_not_available">Tu dispositivo no soporta biometría o no está configurada</string>
|
<string name="biometric_error_not_available">Tu dispositivo no soporta biometría o no está configurada</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user