Nueva opcion si esta en nevera, congelador u otro lugar

This commit is contained in:
2026-08-10 10:10:50 +02:00
parent 1b1b455f1a
commit 6078ab5010
8 changed files with 302 additions and 47 deletions
+2
View File
@@ -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))
}
}
}
} }
} }
} }
+11
View File
@@ -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>