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
|
||||
* **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)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
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 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user