Scan to open on your phone
Point a camera at the QR code - it opens this exact paste, no app needed.
https://codepastes.com/zzgsa1mb543hfbqzsin-tituloPlain Text
package com.negocio.desinfectantes.data
import android.content.Context
import androidx.room.Room
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
object DatabaseProvider {
private val MIGRATION_2_3 = object : Migration(2, 3) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("""
CREATE TABLE IF NOT EXISTS producciones (
id TEXT NOT NULL,
presentacionId TEXT NOT NULL,
cantidadProducida INTEGER NOT NULL,
fechaCreacion INTEGER NOT NULL,
PRIMARY KEY(id)
)
""")
database.execSQL("""
CREATE TABLE IF NOT EXISTS distribuciones_produccion (
id TEXT NOT NULL,
produccionId TEXT NOT NULL,
sede TEXT NOT NULL,
cantidad INTEGER NOT NULL,
fechaCreacion INTEGER NOT NULL,
PRIMARY KEY(id)
)
""")
database.execSQL("""
CREATE TABLE IF NOT EXISTS movimientos_inventario (
id TEXT NOT NULL,
presentacionId TEXT NOT NULL,
sede TEXT NOT NULL,
tipoMovimiento TEXT NOT NULL,
cantidad INTEGER NOT NULL,
referenciaId TEXT,
observacion TEXT,
fechaCreacion INTEGER NOT NULL,
PRIMARY KEY(id)
)
""")
}
}
private val MIGRATION_3_4 = object : Migration(3, 4) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("""
CREATE TABLE IF NOT EXISTS clientes (
id TEXT NOT NULL,
nombre TEXT NOT NULL,
telefono TEXT,
direccion TEXT,
observaciones TEXT,
activo INTEGER NOT NULL,
fechaCreacion INTEGER NOT NULL,
PRIMARY KEY(id)
)
""")
}
}
private val MIGRATION_4_5 = object : Migration(4, 5) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("""
CREATE TABLE IF NOT EXISTS ventas (
id TEXT NOT NULL,
clienteId TEXT NOT NULL,
sede TEXT NOT NULL,
tipoVenta TEXT NOT NULL,
estadoEntrega TEXT NOT NULL,
total REAL NOT NULL,
fechaCreacion INTEGER NOT NULL,
PRIMARY KEY(id)
)
""")
database.execSQL("""
CREATE TABLE IF NOT EXISTS detalles_venta (
id TEXT NOT NULL,
ventaId TEXT NOT NULL,
presentacionId TEXT NOT NULL,
cantidad INTEGER NOT NULL,
precioUnitario REAL NOT NULL,
subtotal REAL NOT NULL,
PRIMARY KEY(id)
)
""")
database.execSQL("""
CREATE TABLE IF NOT EXISTS pagos_venta (
id TEXT NOT NULL,
ventaId TEXT NOT NULL,
monto REAL NOT NULL,
metodoPago TEXT NOT NULL,
observacion TEXT,
fechaCreacion INTEGER NOT NULL,
PRIMARY KEY(id)
)
""")
}
}
@Volatile
private var INSTANCE: PrestigeDatabase? = null
fun obtenerBaseDatos(context: Context): PrestigeDatabase {
return INSTANCE ?: synchronized(this) {
val instancia = Room.databaseBuilder(
context.applicationContext,
PrestigeDatabase::class.java,
"prestige_clean_database"
)
.addMigrations(
MIGRATION_2_3,
MIGRATION_3_4,
MIGRATION_4_5
)
.build()
INSTANCE = instancia
instancia
}
}
}
package com.negocio.desinfectantes.data
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "detalles_venta")
data class DetalleVentaEntity(
@PrimaryKey
val id: String,
val ventaId: String,
val presentacionId: String,
val cantidad: Int,
val precioUnitario: Double,
val subtotal: Double
)
package com.negocio.desinfectantes.ui.produccion
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.background
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.Alignment
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import com.negocio.desinfectantes.data.*
private val Navy = Color(0xFF101D35)
private val CardBlue = Color(0xFF1B2D4A)
private val AccentBlue = Color(0xFF2585F5)
private val SoftBlue = Color(0xFFA9BEDB)
@Composable
fun ProduccionScreen(
modifier: Modifier = Modifier,
productos: List<ProductoEntity>,
aromas: List<AromaEntity>,
presentaciones: List<PresentacionEntity>,
onVolver: () -> Unit,
onGuardar: (PresentacionEntity, Int, Int, Int) -> Unit
) {
var productoSeleccionado by remember {
mutableStateOf<ProductoEntity?>(null)
}
var aromaSeleccionado by remember {
mutableStateOf<AromaEntity?>(null)
}
var presentacionSeleccionada by remember {
mutableStateOf<PresentacionEntity?>(null)
}
var cantidadProducida by remember { mutableStateOf("") }
var cantidadBarrancas by remember { mutableStateOf("") }
var cantidadValledupar by remember { mutableStateOf("") }
var menuProducto by remember { mutableStateOf(false) }
var menuAroma by remember { mutableStateOf(false) }
var menuPresentacion by remember { mutableStateOf(false) }
val aromasDisponibles = aromas.filter {
it.productoId == productoSeleccionado?.id && it.activo
}
val presentacionesDisponibles = presentaciones.filter {
it.aromaId == aromaSeleccionado?.id && it.activo
}
val producidas = cantidadProducida.toIntOrNull()
val barrancas = cantidadBarrancas.toIntOrNull()
val valledupar = cantidadValledupar.toIntOrNull()
val totalDistribuido =
(barrancas ?: 0) + (valledupar ?: 0)
val datosCompletos =
presentacionSeleccionada != null &&
producidas != null && producidas > 0 &&
barrancas != null && valledupar != null &&
barrancas >= 0 && valledupar >= 0
val distribucionCorrecta =
datosCompletos && totalDistribuido == producidas
val mensajeValidacion = when {
cantidadProducida.isNotEmpty() && (producidas == null || producidas <= 0) ->
"Escribe una cantidad fabricada válida."
cantidadBarrancas.isNotEmpty() && barrancas == null ->
"La cantidad de Barrancas no es válida."
cantidadValledupar.isNotEmpty() && valledupar == null ->
"La cantidad de Valledupar no es válida."
datosCompletos && totalDistribuido > producidas!! ->
"Has distribuido ${totalDistribuido - producidas} unidades de más."
datosCompletos && totalDistribuido < producidas!! ->
"Faltan ${producidas - totalDistribuido} unidades por distribuir."
distribucionCorrecta ->
null
else ->
null
}
Column(
modifier = modifier
.fillMaxSize()
.background(Navy)
.verticalScroll(rememberScrollState())
.padding(20.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
TextButton(onClick = onVolver) {
Text("← Volver", color = SoftBlue)
}
}
Text(
"Nueva producción",
color = Color.White,
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
Text(
"Registra lo que fabricaste y distribuye las unidades entre tus sedes.",
color = SoftBlue,
style = MaterialTheme.typography.bodyMedium
)
Spacer(modifier = Modifier.height(24.dp))
// PRODUCTO
Text("1. Seleccionar producto", color = Color.White)
Spacer(modifier = Modifier.height(8.dp))
Box {
OutlinedButton(
onClick = { menuProducto = true },
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp)
) {
Text(
productoSeleccionado?.nombre ?: "Elegir 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
presentacionSeleccionada = null
menuProducto = false
}
)
}
}
}
Spacer(modifier = Modifier.height(18.dp))
// AROMA
Text("2. Seleccionar aroma", color = Color.White)
Spacer(modifier = Modifier.height(8.dp))
Box {
OutlinedButton(
onClick = { menuAroma = true },
enabled = productoSeleccionado != null,
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp)
) {
Text(
aromaSeleccionado?.nombre ?: "Elegir aroma",
modifier = Modifier.weight(1f)
)
Text("▼")
}
DropdownMenu(
expanded = menuAroma,
onDismissRequest = { menuAroma = false }
) {
aromasDisponibles.forEach { aroma ->
DropdownMenuItem(
text = { Text(aroma.nombre) },
onClick = {
aromaSeleccionado = aroma
presentacionSeleccionada = null
menuAroma = false
}
)
}
}
}
Spacer(modifier = Modifier.height(18.dp))
// PRESENTACION
Text("3. Seleccionar presentación", color = Color.White)
Spacer(modifier = Modifier.height(8.dp))
Box {
OutlinedButton(
onClick = { menuPresentacion = true },
enabled = aromaSeleccionado != null,
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp)
) {
Text(
presentacionSeleccionada?.nombre
?: "Elegir 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 = {
presentacionSeleccionada = presentacion
menuPresentacion = false
}
)
}
}
}
Spacer(modifier = Modifier.height(24.dp))
// CANTIDAD FABRICADA
Text(
"4. Cantidad fabricada",
color = Color.White,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = cantidadProducida,
onValueChange = {
cantidadProducida = it.filter { c -> c.isDigit() }
},
modifier = Modifier.fillMaxWidth(),
placeholder = { Text("Ejemplo: 100") },
label = { Text("Unidades fabricadas") },
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number
),
singleLine = true
)
Spacer(modifier = Modifier.height(24.dp))
// DISTRIBUCION
Text(
"5. Distribución por sede",
color = Color.White,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
Card(
colors = CardDefaults.cardColors(
containerColor = CardBlue
),
shape = RoundedCornerShape(16.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text(
"Barrancas",
color = Color.White,
fontWeight = FontWeight.Medium
)
Spacer(modifier = Modifier.height(6.dp))
OutlinedTextField(
value = cantidadBarrancas,
onValueChange = {
cantidadBarrancas = it.filter { c -> c.isDigit() }
},
modifier = Modifier.fillMaxWidth(),
placeholder = { Text("Unidades para Barrancas") },
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number
),
singleLine = true
)
Spacer(modifier = Modifier.height(18.dp))
Text(
"Valledupar",
color = Color.White,
fontWeight = FontWeight.Medium
)
Spacer(modifier = Modifier.height(6.dp))
OutlinedTextField(
value = cantidadValledupar,
onValueChange = {
cantidadValledupar = it.filter { c -> c.isDigit() }
},
modifier = Modifier.fillMaxWidth(),
placeholder = { Text("Unidades para Valledupar") },
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number
),
singleLine = true
)
}
}
Spacer(modifier = Modifier.height(18.dp))
// RESUMEN DE DISTRIBUCION
Text(
"Total distribuido: $totalDistribuido unidades",
color = SoftBlue,
fontWeight = FontWeight.Medium
)
Spacer(modifier = Modifier.height(10.dp))
if (mensajeValidacion != null) {
Text(
"⚠ $mensajeValidacion",
color = Color(0xFFFF7777),
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Medium
)
}
if (distribucionCorrecta) {
Text(
"✓ Distribución correcta. Todas las unidades están asignadas.",
color = Color(0xFF65D69A),
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Medium
)
}
Spacer(modifier = Modifier.height(20.dp))
// GUARDAR PRODUCCION
Button(
onClick = {
if (distribucionCorrecta) {
onGuardar(
presentacionSeleccionada!!,
producidas!!,
barrancas!!,
valledupar!!
)
}
},
enabled = distribucionCorrecta,
modifier = Modifier
.fillMaxWidth()
.height(54.dp),
shape = RoundedCornerShape(14.dp),
colors = ButtonDefaults.buttonColors(
containerColor = AccentBlue,
disabledContainerColor = Color(0xFF34445D)
)
) {
Text(
"Guardar producción",
fontWeight = FontWeight.Bold
)
}
Spacer(modifier = Modifier.height(30.dp))
}
}
package com.negocio.desinfectantes.ui.ventas
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.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.negocio.desinfectantes.data.ClienteEntity
import com.negocio.desinfectantes.data.ProductoEntity
import com.negocio.desinfectantes.data.AromaEntity
import com.negocio.desinfectantes.data.PresentacionEntity
import com.negocio.desinfectantes.data.VentaEntity
import com.negocio.desinfectantes.data.DetalleVentaEntity
import com.negocio.desinfectantes.data.PagoVentaEntity
import com.negocio.desinfectantes.data.MovimientoInventarioEntity
import com.negocio.desinfectantes.data.ProductoDao
import androidx.compose.runtime.rememberCoroutineScope
import kotlinx.coroutines.launch
private val Navy = Color(0xFF101D35)
private val CardBlue = Color(0xFF1B2D4A)
private val AccentBlue = Color(0xFF2585F5)
private val SoftBlue = Color(0xFFA9BEDB)
private data class DetalleTemporalVenta(
val presentacionId: String,
val producto: String,
val aroma: String,
val presentacion: String,
val cantidad: Int,
val precioUnitario: Double
) {
val subtotal: Double
get() = cantidad * precioUnitario
}
private fun generarId(): String {
return java.util.UUID.randomUUID().toString()
}
@Composable
fun VentaScreen(
modifier: Modifier = Modifier,
clientes: List<ClienteEntity>,
productos: List<ProductoEntity>,
aromas: List<AromaEntity>,
presentaciones: List<PresentacionEntity>,
productoDao: ProductoDao,
onVolver: () -> Unit
) {
val scope = rememberCoroutineScope()
var guardando by remember { mutableStateOf(false) }
var mensajeError by remember { mutableStateOf<String?>(null) }
var ventaRegistrada by remember { mutableStateOf(false) }
var clienteSeleccionado by remember { mutableStateOf<ClienteEntity?>(null) }
var sedeSeleccionada by remember { mutableStateOf("Barrancas") }
var mostrarClientes by remember { mutableStateOf(false) }
var productoSeleccionado by remember {
mutableStateOf<ProductoEntity?>(null)
}
var aromaSeleccionado by remember {
mutableStateOf<AromaEntity?>(null)
}
var presentacionSeleccionada by remember {
mutableStateOf<PresentacionEntity?>(null)
}
var mostrarProductos by remember { mutableStateOf(false) }
var mostrarAromas by remember { mutableStateOf(false) }
var mostrarPresentaciones by remember { mutableStateOf(false) }
var cantidadTexto by remember { mutableStateOf("1") }
var precioTexto by remember { mutableStateOf("") }
var detallesVenta by remember {
mutableStateOf(listOf<DetalleTemporalVenta>())
}
var tipoVenta by remember {
mutableStateOf("CONTADO")
}
var estadoEntrega by remember {
mutableStateOf("ENTREGADA")
}
var montoPagoTexto by remember {
mutableStateOf("")
}
var metodoPago by remember {
mutableStateOf("Efectivo")
}
var mostrarMetodosPago by remember {
mutableStateOf(false)
}
var otroMetodoPago by remember {
mutableStateOf("")
}
Column(
modifier = modifier
.fillMaxSize()
.background(Navy)
.verticalScroll(rememberScrollState())
.padding(20.dp)
) {
TextButton(onClick = onVolver) {
Text("← Volver", color = SoftBlue)
}
Text(
"Nueva venta",
color = Color.White,
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
Text(
"Registra los productos y pagos de tus clientes.",
color = SoftBlue
)
Spacer(modifier = Modifier.height(24.dp))
// CLIENTE
Card(
colors = CardDefaults.cardColors(
containerColor = CardBlue
),
shape = RoundedCornerShape(16.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text(
"1. Información del cliente",
color = Color.White,
fontWeight = FontWeight.Bold,
style = MaterialTheme.typography.titleLarge
)
Spacer(modifier = Modifier.height(12.dp))
Box {
OutlinedButton(
onClick = { mostrarClientes = true },
modifier = Modifier.fillMaxWidth()
) {
Text(
clienteSeleccionado?.nombre
?: "Seleccionar cliente",
color = Color.White
)
}
DropdownMenu(
expanded = mostrarClientes,
onDismissRequest = { mostrarClientes = false }
) {
clientes.forEach { cliente ->
DropdownMenuItem(
text = { Text(cliente.nombre) },
onClick = {
clienteSeleccionado = cliente
mostrarClientes = false
}
)
}
}
}
if (clientes.isEmpty()) {
Spacer(modifier = Modifier.height(8.dp))
Text(
"Todavía no hay clientes registrados.",
color = SoftBlue
)
}
}
}
Spacer(modifier = Modifier.height(18.dp))
// SEDE
Card(
colors = CardDefaults.cardColors(
containerColor = CardBlue
),
shape = RoundedCornerShape(16.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text(
"2. Sede de la venta",
color = Color.White,
fontWeight = FontWeight.Bold,
style = MaterialTheme.typography.titleLarge
)
Spacer(modifier = Modifier.height(14.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(10.dp)
) {
FilterChip(
selected = sedeSeleccionada == "Barrancas",
onClick = { sedeSeleccionada = "Barrancas" },
label = { Text("Barrancas") }
)
FilterChip(
selected = sedeSeleccionada == "Valledupar",
onClick = { sedeSeleccionada = "Valledupar" },
label = { Text("Valledupar") }
)
}
}
}
Spacer(modifier = Modifier.height(18.dp))
// PRODUCTOS
Card(
colors = CardDefaults.cardColors(
containerColor = CardBlue
),
shape = RoundedCornerShape(16.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text(
"3. Productos",
color = Color.White,
fontWeight = FontWeight.Bold,
style = MaterialTheme.typography.titleLarge
)
Spacer(modifier = Modifier.height(14.dp))
// SELECCIONAR PRODUCTO
Text("Producto", color = SoftBlue)
Box {
OutlinedButton(
onClick = { mostrarProductos = true },
modifier = Modifier.fillMaxWidth()
) {
Text(
productoSeleccionado?.nombre ?: "Seleccionar producto",
color = Color.White
)
}
DropdownMenu(
expanded = mostrarProductos,
onDismissRequest = { mostrarProductos = false }
) {
productos.filter { it.activo }.forEach { producto ->
DropdownMenuItem(
text = { Text(producto.nombre) },
onClick = {
productoSeleccionado = producto
aromaSeleccionado = null
presentacionSeleccionada = null
mostrarProductos = false
}
)
}
}
}
Spacer(modifier = Modifier.height(14.dp))
// SELECCIONAR AROMA
Text("Aroma", color = SoftBlue)
Box {
OutlinedButton(
onClick = {
if (productoSeleccionado != null) {
mostrarAromas = true
}
},
enabled = productoSeleccionado != null,
modifier = Modifier.fillMaxWidth()
) {
Text(
aromaSeleccionado?.nombre ?: "Seleccionar aroma",
color = Color.White
)
}
DropdownMenu(
expanded = mostrarAromas,
onDismissRequest = { mostrarAromas = false }
) {
aromas.filter {
it.productoId == productoSeleccionado?.id && it.activo
}.forEach { aroma ->
DropdownMenuItem(
text = { Text(aroma.nombre) },
onClick = {
aromaSeleccionado = aroma
presentacionSeleccionada = null
mostrarAromas = false
}
)
}
}
}
Spacer(modifier = Modifier.height(14.dp))
// SELECCIONAR PRESENTACION
Text("Presentación", color = SoftBlue)
Box {
OutlinedButton(
onClick = {
if (aromaSeleccionado != null) {
mostrarPresentaciones = true
}
},
enabled = aromaSeleccionado != null,
modifier = Modifier.fillMaxWidth()
) {
Text(
presentacionSeleccionada?.nombre
?: "Seleccionar presentación",
color = Color.White
)
}
DropdownMenu(
expanded = mostrarPresentaciones,
onDismissRequest = { mostrarPresentaciones = false }
) {
presentaciones.filter {
it.aromaId == aromaSeleccionado?.id && it.activo
}.forEach { presentacion ->
DropdownMenuItem(
text = { Text(presentacion.nombre) },
onClick = {
presentacionSeleccionada = presentacion
precioTexto = presentacion.precio.toString()
cantidadTexto = "1"
mostrarPresentaciones = false
}
)
}
}
}
if (productoSeleccionado != null &&
aromaSeleccionado != null &&
presentacionSeleccionada != null
) {
Spacer(modifier = Modifier.height(12.dp))
Text(
"Precio del catálogo: \$${presentacionSeleccionada!!.precio}",
color = SoftBlue
)
Spacer(modifier = Modifier.height(12.dp))
OutlinedTextField(
value = cantidadTexto,
onValueChange = { cantidadTexto = it },
label = { Text("Cantidad") },
singleLine = true,
modifier = Modifier.fillMaxWidth(),
colors = OutlinedTextFieldDefaults.colors(
focusedTextColor = Color.White,
unfocusedTextColor = Color.White,
focusedLabelColor = SoftBlue,
unfocusedLabelColor = SoftBlue
)
)
Spacer(modifier = Modifier.height(10.dp))
OutlinedTextField(
value = precioTexto,
onValueChange = { precioTexto = it },
label = { Text("Precio acordado por unidad") },
singleLine = true,
modifier = Modifier.fillMaxWidth(),
colors = OutlinedTextFieldDefaults.colors(
focusedTextColor = Color.White,
unfocusedTextColor = Color.White,
focusedLabelColor = SoftBlue,
unfocusedLabelColor = SoftBlue
)
)
Spacer(modifier = Modifier.height(14.dp))
Button(
onClick = {
val presentacion = presentacionSeleccionada
val cantidad = cantidadTexto.toIntOrNull()
val precio = precioTexto.replace(",", ".").toDoubleOrNull()
if (
presentacion != null &&
cantidad != null &&
cantidad > 0 &&
precio != null &&
precio >= 0
) {
val nuevoDetalle = DetalleTemporalVenta(
presentacionId = presentacion.id,
producto = productoSeleccionado!!.nombre,
aroma = aromaSeleccionado!!.nombre,
presentacion = presentacion.nombre,
cantidad = cantidad,
precioUnitario = precio
)
detallesVenta = detallesVenta + nuevoDetalle
presentacionSeleccionada = null
aromaSeleccionado = null
productoSeleccionado = null
cantidadTexto = "1"
precioTexto = ""
}
},
modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(
containerColor = AccentBlue
)
) {
Text("+ Agregar a la venta", color = Color.White)
}
}
}
}
// RESUMEN DE PRODUCTOS AGREGADOS
if (detallesVenta.isNotEmpty()) {
Spacer(modifier = Modifier.height(14.dp))
Card(
colors = CardDefaults.cardColors(
containerColor = CardBlue
),
shape = RoundedCornerShape(16.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text(
"Artículos de la venta",
color = Color.White,
fontWeight = FontWeight.Bold,
style = MaterialTheme.typography.titleLarge
)
Spacer(modifier = Modifier.height(12.dp))
detallesVenta.forEachIndexed { index, detalle ->
Text(
detalle.producto,
color = Color.White,
fontWeight = FontWeight.Bold
)
Text(
"${detalle.aroma} · ${detalle.presentacion}",
color = SoftBlue
)
Text(
"${detalle.cantidad} × \$${detalle.precioUnitario}",
color = SoftBlue
)
Text(
"Subtotal: \$${detalle.subtotal}",
color = Color.White,
fontWeight = FontWeight.Bold
)
TextButton(
onClick = {
detallesVenta = detallesVenta.toMutableList().also {
it.removeAt(index)
}
}
) {
Text(
"Eliminar artículo",
color = Color(0xFFFF6B6B)
)
}
if (index < detallesVenta.lastIndex) {
HorizontalDivider(
modifier = Modifier.padding(vertical = 10.dp),
color = SoftBlue.copy(alpha = 0.3f)
)
}
}
HorizontalDivider(
modifier = Modifier.padding(vertical = 12.dp),
color = SoftBlue
)
Text(
"TOTAL: \$${detallesVenta.sumOf { it.subtotal }}",
color = Color.White,
fontWeight = FontWeight.Bold,
style = MaterialTheme.typography.titleLarge
)
}
}
}
// PAGOS
Card(
colors = CardDefaults.cardColors(
containerColor = CardBlue
),
shape = RoundedCornerShape(16.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text(
"4. Pago y entrega",
color = Color.White,
fontWeight = FontWeight.Bold,
style = MaterialTheme.typography.titleLarge
)
Spacer(modifier = Modifier.height(8.dp))
Text(
"Modalidad de venta",
color = SoftBlue,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
Column {
listOf(
"CONTADO" to "Contado",
"CREDITO" to "Crédito",
"PEDIDO_ANTICIPADO" to "Pedido anticipado"
).forEach { (valor, etiqueta) ->
Row(
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically
) {
RadioButton(
selected = tipoVenta == valor,
onClick = {
tipoVenta = valor
estadoEntrega = if (valor == "PEDIDO_ANTICIPADO") {
"PENDIENTE_FABRICACION"
} else {
"ENTREGADA"
}
},
colors = RadioButtonDefaults.colors(
selectedColor = AccentBlue,
unselectedColor = SoftBlue
)
)
Text(
etiqueta,
color = Color.White
)
}
}
Spacer(modifier = Modifier.height(18.dp))
Spacer(modifier = Modifier.height(18.dp))
if (tipoVenta != "CONTADO") {
Text(
"Pago inicial",
color = SoftBlue,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = montoPagoTexto,
onValueChange = { montoPagoTexto = it },
label = { Text("¿Cuánto dinero entrega el cliente?") },
placeholder = { Text("Ejemplo: 40000") },
singleLine = true,
modifier = Modifier.fillMaxWidth(),
colors = OutlinedTextFieldDefaults.colors(
focusedTextColor = Color.White,
unfocusedTextColor = Color.White,
focusedLabelColor = SoftBlue,
unfocusedLabelColor = SoftBlue,
focusedPlaceholderColor = SoftBlue,
unfocusedPlaceholderColor = SoftBlue
)
)
}
Spacer(modifier = Modifier.height(14.dp))
Text(
"Método de pago",
color = SoftBlue,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
Box {
OutlinedButton(
onClick = { mostrarMetodosPago = true },
modifier = Modifier.fillMaxWidth()
) {
Text(metodoPago, color = Color.White)
}
DropdownMenu(
expanded = mostrarMetodosPago,
onDismissRequest = { mostrarMetodosPago = false }
) {
listOf(
"Efectivo",
"Transferencia bancaria",
"Nequi",
"Daviplata",
"Otro"
).forEach { metodo ->
DropdownMenuItem(
text = { Text(metodo) },
onClick = {
metodoPago = metodo
mostrarMetodosPago = false
}
)
}
}
}
if (metodoPago == "Otro") {
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = otroMetodoPago,
onValueChange = { otroMetodoPago = it },
label = { Text("Especifica el método de pago") },
modifier = Modifier.fillMaxWidth(),
singleLine = true
)
}
Spacer(modifier = Modifier.height(18.dp))
val totalVenta = detallesVenta.sumOf { it.subtotal }
val pagoInicial = if (tipoVenta == "CONTADO") {
totalVenta
} else {
montoPagoTexto
.replace(",", ".")
.toDoubleOrNull() ?: 0.0
}
val saldoPendiente = (totalVenta - pagoInicial).coerceAtLeast(0.0)
val pagoExcedeTotal = tipoVenta != "CONTADO" &&
pagoInicial > totalVenta
val creditoPagadoCompleto = tipoVenta == "CREDITO" &&
totalVenta > 0 &&
pagoInicial >= totalVenta
if (pagoExcedeTotal || creditoPagadoCompleto) {
Text(
text = if (pagoExcedeTotal) {
"El pago inicial no puede superar el total de la venta."
} else {
"Una venta a crédito debe tener un saldo pendiente. Si paga todo, selecciona Contado."
},
color = Color(0xFFFF6B6B),
fontWeight = FontWeight.Bold
)
}
HorizontalDivider(
color = SoftBlue.copy(alpha = 0.3f)
)
Spacer(modifier = Modifier.height(12.dp))
Text(
"Resumen de pago",
color = Color.White,
fontWeight = FontWeight.Bold,
style = MaterialTheme.typography.titleMedium
)
Spacer(modifier = Modifier.height(10.dp))
Text(
"Modalidad: ${
when (tipoVenta) {
"CONTADO" -> "Contado"
"CREDITO" -> "Crédito"
"PEDIDO_ANTICIPADO" -> "Pedido anticipado"
else -> tipoVenta
}
}",
color = SoftBlue
)
Text(
"Método de pago: ${
if (metodoPago == "Otro") {
otroMetodoPago.ifBlank { "Sin especificar" }
} else {
metodoPago
}
}",
color = SoftBlue
)
Spacer(modifier = Modifier.height(8.dp))
HorizontalDivider(
color = SoftBlue.copy(alpha = 0.3f)
)
Spacer(modifier = Modifier.height(8.dp))
Text(
"Total de la venta: \$${totalVenta}",
color = SoftBlue
)
Text(
"Pago inicial: \$${pagoInicial}",
color = SoftBlue
)
Text(
"Saldo pendiente: \$${saldoPendiente}",
color = if (saldoPendiente > 0) {
Color(0xFFFFB74D)
} else {
Color(0xFF81C784)
},
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(12.dp))
Text(
"Estado de entrega",
color = SoftBlue,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
if (tipoVenta == "PEDIDO_ANTICIPADO") {
Text(
"Este pedido quedará pendiente de fabricación. " +
"Cuando fabriques los productos, podrás reservarlos desde el historial.",
color = Color(0xFFFFB74D)
)
} else {
Text(
"El producto se registrará como entregado.",
color = SoftBlue
)
}
}
}
}
if (mensajeError != null) {
Text(
text = mensajeError!!,
color = Color(0xFFFF6B6B),
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
}
if (ventaRegistrada) {
Text(
text = "¡Venta registrada correctamente!",
color = Color(0xFF81C784),
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
}
Button(
onClick = {
val cliente = clienteSeleccionado
val total = detallesVenta.sumOf { it.subtotal }
val pagoCalculado = if (tipoVenta == "CONTADO") {
total
} else if (montoPagoTexto.isBlank()) {
0.0
} else {
montoPagoTexto.replace(",", ".").toDoubleOrNull() ?: -1.0
}
val metodoFinal = if (metodoPago == "Otro") {
otroMetodoPago.trim()
} else {
metodoPago
}
when {
cliente == null -> {
mensajeError = "Selecciona un cliente para continuar."
}
detallesVenta.isEmpty() -> {
mensajeError = "Agrega al menos un producto a la venta."
}
total <= 0 -> {
mensajeError = "El total de la venta debe ser mayor que cero."
}
(tipoVenta == "CREDITO" || tipoVenta == "PEDIDO_ANTICIPADO") &&
montoPagoTexto.isBlank() -> {
mensajeError = "Ingresa el pago inicial. Si no paga nada, escribe 0."
}
pagoCalculado < 0 || pagoCalculado > total -> {
mensajeError = "El valor del pago inicial no es válido."
}
tipoVenta == "CREDITO" && pagoCalculado >= total -> {
mensajeError = "Una venta a crédito debe tener saldo pendiente."
}
pagoCalculado > 0 && metodoFinal.isBlank() -> {
mensajeError = "Especifica el método de pago."
}
else -> {
mensajeError = null
guardando = true
scope.launch {
try {
val fecha = System.currentTimeMillis()
val ventaId = generarId()
val nuevaVenta = VentaEntity(
id = ventaId,
clienteId = cliente.id,
sede = sedeSeleccionada,
tipoVenta = tipoVenta,
estadoEntrega = estadoEntrega,
total = total,
fechaCreacion = fecha
)
val detalles = detallesVenta.map { detalle ->
DetalleVentaEntity(
id = generarId(),
ventaId = ventaId,
presentacionId = detalle.presentacionId,
cantidad = detalle.cantidad,
precioUnitario = detalle.precioUnitario,
subtotal = detalle.subtotal
)
}
val pagos = if (pagoCalculado > 0) {
listOf(
PagoVentaEntity(
id = generarId(),
ventaId = ventaId,
monto = pagoCalculado,
metodoPago = metodoFinal,
fechaCreacion = fecha
)
)
} else {
emptyList()
}
val descontarInventario =
estadoEntrega == "ENTREGADA"
val movimientos = if (descontarInventario) {
detallesVenta.map { detalle ->
MovimientoInventarioEntity(
id = generarId(),
presentacionId = detalle.presentacionId,
sede = sedeSeleccionada,
tipoMovimiento = "VENTA",
cantidad = detalle.cantidad,
referenciaId = ventaId,
observacion = "Venta registrada",
fechaCreacion = fecha
)
}
} else {
emptyList()
}
productoDao.registrarVentaCompleta(
venta = nuevaVenta,
detalles = detalles,
pagos = pagos,
movimientos = movimientos,
descontarInventario = descontarInventario
)
ventaRegistrada = true
} catch (e: Exception) {
mensajeError = e.message
?: "No fue posible registrar la venta."
} finally {
guardando = false
}
}
}
}
},
enabled = !guardando && !ventaRegistrada,
modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(
containerColor = AccentBlue
)
) {
Text(
text = if (guardando) {
"Registrando venta..."
} else {
"Registrar venta"
},
color = Color.White,
fontWeight = FontWeight.Bold
)
}
Spacer(modifier = Modifier.height(24.dp))
}
}
54,833 chars · codepastes.com