Scan to open on your phone
Point a camera at the QR code - it opens this exact paste, no app needed.
https://codepastes.com/zzgs8ht28z6aqm10untitledPlain Text
package com.negocio.desinfectantes.ui.produccion
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import com.negocio.desinfectantes.data.*
import java.util.UUID
private val Navy = Color(0xFF101D35)
private val CardBlue = Color(0xFF1B2D4A)
private val AccentBlue = Color(0xFF2585F5)
private val SoftBlue = Color(0xFFA9BEDB)
private data class DetalleLoteFormulario(
val presentacion: PresentacionEntity,
val cantidad: String = "",
val barrancas: String = "0",
val valledupar: String = "0"
)
private data class GastoLoteFormulario(
val concepto: String = "",
val monto: String = "",
val observacion: String = ""
)
@Composable
fun ProduccionScreen(
modifier: Modifier = Modifier,
productos: List<ProductoEntity>,
aromas: List<AromaEntity>,
presentaciones: List<PresentacionEntity>,
onVolver: () -> Unit,
onGuardar: (
LoteProduccionEntity,
List<LoteDetalleEntity>,
List<LoteGastoEntity>,
List<LoteInventarioEntity>
) -> Unit
) {
var productoSeleccionado by remember { mutableStateOf<ProductoEntity?>(null) }
var aromaSeleccionado by remember { mutableStateOf<AromaEntity?>(null) }
var menuProducto by remember { mutableStateOf(false) }
var menuAroma by remember { mutableStateOf(false) }
var menuPresentacion by remember { mutableStateOf(false) }
val detalles = remember { mutableStateListOf<DetalleLoteFormulario>() }
val gastos = remember { mutableStateListOf<GastoLoteFormulario>() }
val aromasDisponibles = aromas.filter {
it.productoId == productoSeleccionado?.id && it.activo
}
val presentacionesDisponibles = presentaciones.filter {
it.aromaId == aromaSeleccionado?.id && it.activo &&
detalles.none { detalle -> detalle.presentacion.id == it.id }
}
val detallesCorrectos = detalles.isNotEmpty() && detalles.all { detalle ->
val cantidad = detalle.cantidad.toIntOrNull()
val barrancas = detalle.barrancas.toIntOrNull()
val valledupar = detalle.valledupar.toIntOrNull()
cantidad != null && cantidad > 0 &&
barrancas != null && valledupar != null &&
barrancas >= 0 && valledupar >= 0 &&
barrancas + valledupar == cantidad
}
val gastosCorrectos = gastos.all { gasto ->
val vacio = gasto.concepto.isBlank() && gasto.monto.isBlank()
val monto = gasto.monto.replace(",", ".").toDoubleOrNull()
vacio || (
gasto.concepto.isNotBlank() &&
monto != null && monto > 0
)
}
val formularioCorrecto =
productoSeleccionado != null &&
aromaSeleccionado != null &&
detallesCorrectos &&
gastosCorrectos
Column(
modifier = modifier
.fillMaxSize()
.background(Navy)
.verticalScroll(rememberScrollState())
.padding(20.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
TextButton(onClick = onVolver) {
Text("← Volver", color = SoftBlue)
}
Text(
"Nuevo lote de producción",
color = Color.White,
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold
)
Text(
"Selecciona los registros existentes y añade las cantidades fabricadas.",
color = SoftBlue
)
// PRODUCTO
Text("1. Producto", color = Color.White, fontWeight = FontWeight.Bold)
Box {
OutlinedButton(
onClick = { menuProducto = true },
modifier = Modifier.fillMaxWidth()
) {
Text(
productoSeleccionado?.nombre ?: "Seleccionar producto",
modifier = Modifier.weight(1f)
)
Text("▼")
}
DropdownMenu(
expanded = menuProducto,
onDismissRequest = { menuProducto = false }
) {
productos.filter { it.activo }.forEach { producto ->
DropdownMenuItem(
text = { Text(producto.nombre) },
onClick = {
productoSeleccionado = producto
aromaSeleccionado = null
detalles.clear()
menuProducto = false
}
)
}
}
}
// AROMA
Text("2. Aroma", color = Color.White, fontWeight = FontWeight.Bold)
Box {
OutlinedButton(
onClick = { menuAroma = true },
enabled = productoSeleccionado != null,
modifier = Modifier.fillMaxWidth()
) {
Text(
aromaSeleccionado?.nombre ?: "Seleccionar aroma",
modifier = Modifier.weight(1f)
)
Text("▼")
}
DropdownMenu(
expanded = menuAroma,
onDismissRequest = { menuAroma = false }
) {
aromasDisponibles.forEach { aroma ->
DropdownMenuItem(
text = { Text(aroma.nombre) },
onClick = {
aromaSeleccionado = aroma
detalles.clear()
menuAroma = false
}
)
}
}
}
// PRESENTACIONES
Text(
"3. Presentaciones del lote",
color = Color.White,
fontWeight = FontWeight.Bold
)
Text(
"Puedes agregar varias presentaciones al mismo lote.",
color = SoftBlue
)
Box {
OutlinedButton(
onClick = { menuPresentacion = true },
enabled = aromaSeleccionado != null &&
presentacionesDisponibles.isNotEmpty(),
modifier = Modifier.fillMaxWidth()
) {
Text(
"+ Agregar presentación",
modifier = Modifier.weight(1f)
)
Text("▼")
}
DropdownMenu(
expanded = menuPresentacion,
onDismissRequest = { menuPresentacion = false }
) {
presentacionesDisponibles.forEach { presentacion ->
DropdownMenuItem(
text = {
Text(
"${presentacion.nombre} • ${presentacion.contenidoMl} ml"
)
},
onClick = {
detalles.add(
DetalleLoteFormulario(
presentacion = presentacion
)
)
menuPresentacion = false
}
)
}
}
}
if (aromaSeleccionado != null &&
presentacionesDisponibles.isEmpty() &&
detalles.isEmpty()
) {
Text(
"No hay presentaciones disponibles para este aroma.",
color = SoftBlue
)
}
detalles.forEachIndexed { index, detalle ->
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = CardBlue
),
shape = RoundedCornerShape(16.dp)
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
Text(
detalle.presentacion.nombre,
color = Color.White,
fontWeight = FontWeight.Bold
)
Text(
"${detalle.presentacion.contenidoMl} ml",
color = SoftBlue
)
}
TextButton(
onClick = { detalles.removeAt(index) }
) {
Text("Quitar", color = Color(0xFFFF8888))
}
}
OutlinedTextField(
value = detalle.cantidad,
onValueChange = {
detalles[index] = detalle.copy(
cantidad = it.filter { c -> c.isDigit() }
)
},
modifier = Modifier.fillMaxWidth(),
label = { Text("Unidades fabricadas") },
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number
),
singleLine = true
)
Text("Distribución por sede", color = SoftBlue)
OutlinedTextField(
value = detalle.barrancas,
onValueChange = {
detalles[index] = detalle.copy(
barrancas = it.filter { c -> c.isDigit() }
)
},
modifier = Modifier.fillMaxWidth(),
label = { Text("Barrancas") },
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number
),
singleLine = true
)
OutlinedTextField(
value = detalle.valledupar,
onValueChange = {
detalles[index] = detalle.copy(
valledupar = it.filter { c -> c.isDigit() }
)
},
modifier = Modifier.fillMaxWidth(),
label = { Text("Valledupar") },
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number
),
singleLine = true
)
val cantidad = detalle.cantidad.toIntOrNull()
val barrancas = detalle.barrancas.toIntOrNull()
val valledupar = detalle.valledupar.toIntOrNull()
if (cantidad != null && barrancas != null &&
valledupar != null && cantidad > 0
) {
val diferencia = cantidad - barrancas - valledupar
Text(
when {
diferencia == 0 ->
"✓ Distribución correcta"
diferencia > 0 ->
"Faltan $diferencia unidades por distribuir"
else ->
"Hay ${-diferencia} unidades distribuidas de más"
},
color = if (diferencia == 0)
Color(0xFF65D69A)
else
Color(0xFFFF8888)
)
}
}
}
}
// GASTOS
Text(
"4. Gastos del lote",
color = Color.White,
fontWeight = FontWeight.Bold
)
Text(
"Registra los gastos relacionados con la fabricación.",
color = SoftBlue
)
gastos.forEachIndexed { index, gasto ->
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = CardBlue
),
shape = RoundedCornerShape(16.dp)
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
"Gasto ${index + 1}",
color = Color.White,
fontWeight = FontWeight.Bold,
modifier = Modifier.weight(1f)
)
TextButton(
onClick = { gastos.removeAt(index) }
) {
Text("Quitar", color = Color(0xFFFF8888))
}
}
OutlinedTextField(
value = gasto.concepto,
onValueChange = {
gastos[index] = gasto.copy(concepto = it)
},
modifier = Modifier.fillMaxWidth(),
label = { Text("Concepto del gasto") },
placeholder = { Text("Ej. Esencias, envases, etiquetas") },
singleLine = true
)
OutlinedTextField(
value = gasto.monto,
onValueChange = {
gastos[index] = gasto.copy(
monto = it.filter { c ->
c.isDigit() || c == '.' || c == ','
}
)
},
modifier = Modifier.fillMaxWidth(),
label = { Text("Valor del gasto") },
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Decimal
),
singleLine = true
)
OutlinedTextField(
value = gasto.observacion,
onValueChange = {
gastos[index] = gasto.copy(observacion = it)
},
modifier = Modifier.fillMaxWidth(),
label = { Text("Observación (opcional)") }
)
}
}
}
OutlinedButton(
onClick = { gastos.add(GastoLoteFormulario()) },
modifier = Modifier.fillMaxWidth()
) {
Text("+ Agregar gasto")
}
// RESUMEN
val totalUnidades = detalles.sumOf {
it.cantidad.toIntOrNull() ?: 0
}
val totalGastos = gastos.sumOf {
it.monto.replace(",", ".").toDoubleOrNull() ?: 0.0
}
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = CardBlue
)
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(6.dp)
) {
Text("Resumen del lote", color = Color.White, fontWeight = FontWeight.Bold)
Text("Presentaciones: ${detalles.size}", color = SoftBlue)
Text("Unidades fabricadas: $totalUnidades", color = SoftBlue)
Text(
"Gastos registrados: $${totalGastos.toLong()}",
color = SoftBlue
)
}
}
Button(
onClick = {
val producto = productoSeleccionado!!
val aroma = aromaSeleccionado!!
val fecha = System.currentTimeMillis()
val loteId = UUID.randomUUID().toString()
val lote = LoteProduccionEntity(
id = loteId,
productoId = producto.id,
aromaId = aroma.id,
fechaCreacion = fecha
)
val detallesLote = detalles.map { detalle ->
LoteDetalleEntity(
id = UUID.randomUUID().toString(),
loteId = loteId,
presentacionId = detalle.presentacion.id,
cantidadFabricada = detalle.cantidad.toInt()
)
}
val gastosLote = gastos
.filter { it.concepto.isNotBlank() }
.map { gasto ->
LoteGastoEntity(
id = UUID.randomUUID().toString(),
loteId = loteId,
concepto = gasto.concepto.trim(),
monto = gasto.monto.replace(",", ".").toDouble(),
observacion = gasto.observacion
.trim()
.ifBlank { null },
fechaCreacion = fecha
)
}
val inventariosLote = mutableListOf<LoteInventarioEntity>()
detalles.forEach { detalle ->
val cantidadBarrancas = detalle.barrancas.toInt()
val cantidadValledupar = detalle.valledupar.toInt()
if (cantidadBarrancas > 0) {
inventariosLote.add(
LoteInventarioEntity(
id = UUID.randomUUID().toString(),
loteId = loteId,
presentacionId = detalle.presentacion.id,
sede = "Barrancas",
cantidadDisponible = cantidadBarrancas
)
)
}
if (cantidadValledupar > 0) {
inventariosLote.add(
LoteInventarioEntity(
id = UUID.randomUUID().toString(),
loteId = loteId,
presentacionId = detalle.presentacion.id,
sede = "Valledupar",
cantidadDisponible = cantidadValledupar
)
)
}
}
onGuardar(
lote,
detallesLote,
gastosLote,
inventariosLote
)
},
enabled = formularioCorrecto,
modifier = Modifier
.fillMaxWidth()
.height(54.dp),
shape = RoundedCornerShape(14.dp),
colors = ButtonDefaults.buttonColors(
containerColor = AccentBlue,
disabledContainerColor = Color(0xFF34445D)
)
) {
Text(
"Guardar lote de producción",
fontWeight = FontWeight.Bold
)
}
if (!formularioCorrecto) {
Text(
"Completa los datos y verifica que la distribución de cada presentación coincida con las unidades fabricadas.",
color = SoftBlue
)
}
Spacer(modifier = Modifier.height(24.dp))
}
}
package com.negocio.desinfectantes.data
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "lotes_produccion")
data class LoteProduccionEntity(
@PrimaryKey
val id: String,
val productoId: String,
val aromaId: String,
@ColumnInfo(defaultValue = "''")
val numeroLote: String = "",
val fechaCreacion: Long = System.currentTimeMillis()
)
package com.negocio.desinfectantes.data
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "lotes_detalle")
data class LoteDetalleEntity(
@PrimaryKey
val id: String,
val loteId: String,
val presentacionId: String,
val cantidadFabricada: Int
)
21,493 chars · codepastes.com