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
* **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.
* **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).
* **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.
## 🛠 Funcionalidades Principales
@@ -7,8 +7,11 @@ data class Product(
val minQuantity: Int,
val unit: String = "unidad",
val category: String = "General",
val location: String = "Despensa",
val price: Double = 0.0,
val lastSupermarket: String? = null,
val bestPrice: Double? = null,
val bestPriceSupermarket: String? = null,
val imageUrl: String? = null,
val expirationDate: String? = null,
val createdAt: String? = null,
@@ -51,11 +51,18 @@ class PantryRepository {
return withContext(Dispatchers.IO) {
try {
val connection = getConnection()
val sql = if (familyCode != null) {
"SELECT * FROM productos WHERE visible = 1 AND codigo_familia = ?"
} else {
"SELECT * FROM productos WHERE visible = 1"
}
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.visible = 1 ${if (familyCode != null) "AND p.codigo_familia = ?" else ""}
""".trimIndent()
val statement = connection.prepareStatement(sql)
if (familyCode != null) {
statement.setString(1, familyCode)
@@ -72,8 +79,11 @@ class PantryRepository {
minQuantity = resultSet.getInt("cantidad_minima"),
unit = resultSet.getString("unidad") ?: "unidad",
category = resultSet.getString("categoria"),
location = resultSet.getString("ubicacion") ?: "Despensa",
price = resultSet.getDouble("precio"),
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"),
expirationDate = resultSet.getString("fecha_caducidad"),
createdAt = resultSet.getString("fecha_alta"),
@@ -140,6 +150,7 @@ class PantryRepository {
minCantidad: Int,
unidad: String,
category: String,
location: String,
price: Double,
supermarket: String? = null,
fechaCaducidad: String?,
@@ -149,19 +160,20 @@ class PantryRepository {
return withContext(Dispatchers.IO) {
try {
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)
statement.setString(1, nombre)
statement.setInt(2, cantidad)
statement.setInt(3, minCantidad)
statement.setString(4, unidad)
statement.setString(5, category)
statement.setDouble(6, price)
statement.setString(7, supermarket)
statement.setString(8, fechaCaducidad)
statement.setString(9, imageUrl)
statement.setString(10, userName)
statement.setInt(11, productId)
statement.setString(6, location)
statement.setDouble(7, price)
statement.setString(8, supermarket)
statement.setString(9, fechaCaducidad)
statement.setString(10, imageUrl)
statement.setString(11, userName)
statement.setInt(12, productId)
val rowsUpdated = statement.executeUpdate()
@@ -184,6 +196,7 @@ class PantryRepository {
minCantidad: Int,
unidad: String,
categoria: String,
location: String,
price: Double,
supermarket: String? = null,
imageUrl: String?,
@@ -194,14 +207,15 @@ class PantryRepository {
try {
val connection = getConnection()
val sql = """
INSERT INTO productos (barcode, nombre, cantidad, cantidad_minima, unidad, categoria, precio, ultimo_supermercado, imagen_url, fecha_caducidad, visible, modificado_por)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?)
INSERT INTO productos (barcode, nombre, cantidad, cantidad_minima, unidad, categoria, ubicacion, precio, ultimo_supermercado, imagen_url, fecha_caducidad, visible, modificado_por)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?)
ON DUPLICATE KEY UPDATE
nombre = VALUES(nombre),
cantidad = VALUES(cantidad),
cantidad_minima = VALUES(cantidad_minima),
unidad = VALUES(unidad),
categoria = VALUES(categoria),
ubicacion = VALUES(ubicacion),
precio = VALUES(precio),
ultimo_supermercado = VALUES(ultimo_supermercado),
imagen_url = IF(VALUES(imagen_url) IS NULL, imagen_url, VALUES(imagen_url)),
@@ -217,11 +231,12 @@ class PantryRepository {
statement.setInt(4, minCantidad)
statement.setString(5, unidad)
statement.setString(6, categoria)
statement.setDouble(7, price)
statement.setString(8, supermarket)
statement.setString(9, imageUrl)
statement.setString(10, fechaCaducidad)
statement.setString(11, userName)
statement.setString(7, location)
statement.setDouble(8, price)
statement.setString(9, supermarket)
statement.setString(10, imageUrl)
statement.setString(11, fechaCaducidad)
statement.setString(12, userName)
val rowsInserted = statement.executeUpdate()
@@ -275,7 +290,17 @@ class PantryRepository {
return withContext(Dispatchers.IO) {
try {
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)
statement.setInt(1, productId)
val resultSet = statement.executeQuery()
@@ -289,8 +314,11 @@ class PantryRepository {
minQuantity = resultSet.getInt("cantidad_minima"),
unit = resultSet.getString("unidad") ?: "unidad",
category = resultSet.getString("categoria"),
location = resultSet.getString("ubicacion") ?: "Despensa",
price = resultSet.getDouble("precio"),
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"),
expirationDate = resultSet.getString("fecha_caducidad"),
createdAt = resultSet.getString("fecha_alta"),
@@ -309,7 +337,17 @@ class PantryRepository {
return withContext(Dispatchers.IO) {
try {
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)
statement.setString(1, barcode)
val resultSet = statement.executeQuery()
@@ -323,8 +361,11 @@ class PantryRepository {
minQuantity = resultSet.getInt("cantidad_minima"),
unit = resultSet.getString("unidad") ?: "unidad",
category = resultSet.getString("categoria"),
location = resultSet.getString("ubicacion") ?: "Despensa",
price = resultSet.getDouble("precio"),
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"),
expirationDate = resultSet.getString("fecha_caducidad"),
createdAt = resultSet.getString("fecha_alta"),
@@ -77,6 +77,7 @@ fun AddProductScreen(onProductAdded: () -> Unit) {
var productPrice by remember { mutableStateOf(0.0) }
var productImageUrl 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 minQuantity by remember { mutableStateOf(1) }
var selectedUnit by remember { mutableStateOf("unidad") }
@@ -348,6 +349,49 @@ fun AddProductScreen(onProductAdded: () -> Unit) {
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
ExposedDropdownMenuBox(
expanded = unitsExpanded,
@@ -418,6 +462,7 @@ fun AddProductScreen(onProductAdded: () -> Unit) {
minCantidad = minQuantity,
unidad = selectedUnit,
categoria = productCategory,
location = selectedLocation,
price = productPrice,
supermarket = selectedSupermarket.ifEmpty { null },
imageUrl = productImageUrl,
@@ -60,6 +60,7 @@ fun PantryScreen(
// Filters and Search
var searchQuery by remember { mutableStateOf("") }
var selectedCategory by remember { mutableStateOf<String?>(null) }
var selectedLocation by remember { mutableStateOf<String?>(null) }
var showOnlyNearExpiry by remember { mutableStateOf(false) }
var showShoppingList by remember { mutableStateOf(false) }
@@ -82,10 +83,11 @@ fun PantryScreen(
val filteredProducts = allProducts.filter { product ->
val matchesSearch = product.name.contains(searchQuery, ignoreCase = true)
val matchesCategory = selectedCategory == null || product.category == selectedCategory
val matchesLocation = selectedLocation == null || product.location == selectedLocation
val matchesExpiry = !showOnlyNearExpiry || isNearExpiry(product.expirationDate)
val matchesLowStock = !showShoppingList || product.isLowStock
matchesSearch && matchesCategory && matchesExpiry && matchesLowStock
matchesSearch && matchesCategory && matchesLocation && matchesExpiry && matchesLowStock
}
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
ScrollableTabRow(
selectedTabIndex = if (selectedCategory == null) 0 else categories.indexOf(selectedCategory) + 1,
@@ -327,8 +359,6 @@ fun PantryScreen(
product = product,
onItemClick = { onNavigateToDetail(product.id) },
onNavigateToPriceScanner = onNavigateToPriceScanner,
showPriceUpdater = showShoppingList,
onUpdatePrice = { onNavigateToPriceScanner(product.id) },
onQuantityChange = { newQty ->
scope.launch {
val success = repository.updateProductQuantity(product.id, newQty, userName)
@@ -407,8 +437,6 @@ fun ProductItem(
product: Product,
onItemClick: () -> Unit,
onNavigateToPriceScanner: (Int) -> Unit = {},
showPriceUpdater: Boolean = false,
onUpdatePrice: () -> Unit = {},
onQuantityChange: (Int) -> Unit
) {
Card(
@@ -485,6 +513,20 @@ fun ProductItem(
fontSize = 14.sp,
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)
@@ -498,16 +540,19 @@ fun ProductItem(
Spacer(modifier = Modifier.height(4.dp))
// Nuevo Botón: Registrar solo Precio (Historial)
IconButton(
// Nuevo Botón: Registrar solo Precio (Historial) - Ahora más visible
FilledTonalIconButton(
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(
Icons.Default.LocalOffer,
Icons.Default.Sell,
contentDescription = stringResource(R.string.pantry_update_price),
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(20.dp)
modifier = Modifier.size(18.dp)
)
}
@@ -564,6 +609,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) {
Row(verticalAlignment = Alignment.CenterVertically) {
@@ -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 editUnit by remember { mutableStateOf("") }
var editCategory by remember { mutableStateOf("") }
var editLocation by remember { mutableStateOf("Despensa") }
var editPrice by remember { mutableStateOf(0.0) }
var editSupermarket by remember { mutableStateOf("") }
var editExpirationDate by remember { mutableStateOf<String?>(null) }
@@ -202,6 +203,7 @@ fun ProductDetailScreen(productId: Int, onBack: () -> Unit) {
editMinQuantity = p.minQuantity
editUnit = p.unit
editCategory = p.category
editLocation = p.location
editPrice = p.price
editSupermarket = p.lastSupermarket ?: ""
editExpirationDate = p.expirationDate
@@ -256,6 +258,7 @@ fun ProductDetailScreen(productId: Int, onBack: () -> Unit) {
minCantidad = editMinQuantity,
unidad = editUnit,
category = editCategory,
location = editLocation,
price = editPrice,
supermarket = editSupermarket.ifEmpty { null },
fechaCaducidad = editExpirationDate,
@@ -346,6 +349,7 @@ fun ProductDetailScreen(productId: Int, onBack: () -> Unit) {
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.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))
if (p.lastSupermarket != null) {
@@ -459,6 +463,49 @@ fun ProductDetailScreen(productId: Int, onBack: () -> Unit) {
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(
selectedSupermarket = editSupermarket,
onSupermarketChange = { editSupermarket = it },
@@ -1,10 +1,12 @@
package com.example.despensapp.ui.screens
import android.content.Intent
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.selection.selectableGroup
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Share
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
@@ -12,6 +14,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.example.despensapp.R
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_no_history">No hay historial de precios registrado</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 -->
<string name="supermarket_mercadona">MERCADONA</string>
@@ -150,6 +157,10 @@
<string name="settings_freq_weekly">Una vez a la semana</string>
<string name="settings_save_success">Configuración guardada correctamente</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_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>