feat: user registration with WhatsApp OTP verification, platform order registration with auto WhatsApp alerts, and splash screen overflow fix
This commit is contained in:
parent
fef85c3ff4
commit
b7f4d600fc
@ -35,7 +35,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private var allProducts: List<Product> = emptyList()
|
private var allProducts: List<Product> = emptyList()
|
||||||
private var availableCities: List<City> = City.defaultCities()
|
private var availableCities: List<City> = City.defaultCities()
|
||||||
private var selectedCity: City = City.defaultCities().first()
|
var selectedCity: City = City.defaultCities().first()
|
||||||
private var selectedCategory: String = "Todos"
|
private var selectedCategory: String = "Todos"
|
||||||
private var searchQuery: String = ""
|
private var searchQuery: String = ""
|
||||||
|
|
||||||
|
|||||||
@ -10,8 +10,10 @@ import com.google.gson.reflect.TypeToken
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import okhttp3.ConnectionSpec
|
import okhttp3.ConnectionSpec
|
||||||
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import okhttp3.TlsVersion
|
import okhttp3.TlsVersion
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
@ -189,5 +191,160 @@ object ApiClient {
|
|||||||
}
|
}
|
||||||
co.dripzone.app.model.City.defaultCities()
|
co.dripzone.app.model.City.defaultCities()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Envía código OTP de 6 dígitos al WhatsApp del cliente
|
||||||
|
*/
|
||||||
|
suspend fun sendOtp(phone: String, name: String): Result<String> =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val jsonPayload = gson.toJson(
|
||||||
|
mapOf(
|
||||||
|
"identifier" to phone,
|
||||||
|
"name" to name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val mediaType = "application/json; charset=utf-8".toMediaTypeOrNull()
|
||||||
|
val requestBody = jsonPayload.toRequestBody(mediaType)
|
||||||
|
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url("https://dripzone.almeira.com.co/api/auth/send-otp")
|
||||||
|
.post(requestBody)
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val response = client.newCall(request).execute()
|
||||||
|
val responseBody = response.body?.string() ?: ""
|
||||||
|
val json = gson.fromJson(responseBody, com.google.gson.JsonObject::class.java)
|
||||||
|
|
||||||
|
if (response.isSuccessful && json.get("success")?.asBoolean == true) {
|
||||||
|
val message = json.get("message")?.asString ?: "Código OTP enviado por WhatsApp"
|
||||||
|
Result.success(message)
|
||||||
|
} else {
|
||||||
|
val errorMsg = json.get("error")?.asString ?: "Error al enviar código OTP"
|
||||||
|
Result.failure(Exception(errorMsg))
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Error sending OTP: ${e.message}")
|
||||||
|
Result.failure(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valida el código OTP de WhatsApp y registra/actualiza el usuario en la plataforma
|
||||||
|
*/
|
||||||
|
suspend fun verifyOtp(
|
||||||
|
phone: String,
|
||||||
|
code: String,
|
||||||
|
name: String,
|
||||||
|
city: String,
|
||||||
|
address: String
|
||||||
|
): Result<Boolean> =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val jsonPayload = gson.toJson(
|
||||||
|
mapOf(
|
||||||
|
"identifier" to phone,
|
||||||
|
"code" to code,
|
||||||
|
"name" to name,
|
||||||
|
"city" to city,
|
||||||
|
"address" to address
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val mediaType = "application/json; charset=utf-8".toMediaTypeOrNull()
|
||||||
|
val requestBody = jsonPayload.toRequestBody(mediaType)
|
||||||
|
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url("https://dripzone.almeira.com.co/api/auth/verify-otp")
|
||||||
|
.post(requestBody)
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val response = client.newCall(request).execute()
|
||||||
|
val responseBody = response.body?.string() ?: ""
|
||||||
|
val json = gson.fromJson(responseBody, com.google.gson.JsonObject::class.java)
|
||||||
|
|
||||||
|
if (response.isSuccessful && json.get("success")?.asBoolean == true) {
|
||||||
|
Result.success(true)
|
||||||
|
} else {
|
||||||
|
val errorMsg = json.get("error")?.asString ?: "Código inválido o expirado"
|
||||||
|
Result.failure(Exception(errorMsg))
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Error verifying OTP: ${e.message}")
|
||||||
|
Result.failure(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registra un pedido en la plataforma (Appwrite/DataService) y dispara notificación por WhatsApp a Carlos
|
||||||
|
*/
|
||||||
|
suspend fun submitOrder(orderData: Map<String, Any?>): Result<String> =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val jsonPayload = gson.toJson(orderData)
|
||||||
|
val mediaType = "application/json; charset=utf-8".toMediaTypeOrNull()
|
||||||
|
val requestBody = jsonPayload.toRequestBody(mediaType)
|
||||||
|
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url("https://dripzone.almeira.com.co/api/orders")
|
||||||
|
.post(requestBody)
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val response = client.newCall(request).execute()
|
||||||
|
val responseBody = response.body?.string() ?: ""
|
||||||
|
val json = gson.fromJson(responseBody, com.google.gson.JsonObject::class.java)
|
||||||
|
|
||||||
|
if (response.isSuccessful && json.get("success")?.asBoolean == true) {
|
||||||
|
val orderObj = json.getAsJsonObject("order")
|
||||||
|
val orderId = orderObj?.get("id")?.asString ?: "ORD-${System.currentTimeMillis()}"
|
||||||
|
Result.success(orderId)
|
||||||
|
} else {
|
||||||
|
val errorMsg = json.get("error")?.asString ?: "Error al registrar pedido en la plataforma"
|
||||||
|
Result.failure(Exception(errorMsg))
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Error submitting order: ${e.message}")
|
||||||
|
Result.failure(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registra una solicitud de encargo especial en la plataforma y dispara notificación a Carlos
|
||||||
|
*/
|
||||||
|
suspend fun submitCustomRequest(requestData: Map<String, Any?>): Result<String> =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val jsonPayload = gson.toJson(requestData)
|
||||||
|
val mediaType = "application/json; charset=utf-8".toMediaTypeOrNull()
|
||||||
|
val requestBody = jsonPayload.toRequestBody(mediaType)
|
||||||
|
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url("https://dripzone.almeira.com.co/api/custom-requests")
|
||||||
|
.post(requestBody)
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val response = client.newCall(request).execute()
|
||||||
|
val responseBody = response.body?.string() ?: ""
|
||||||
|
val json = gson.fromJson(responseBody, com.google.gson.JsonObject::class.java)
|
||||||
|
|
||||||
|
if (response.isSuccessful && json.get("success")?.asBoolean == true) {
|
||||||
|
val msg = json.get("message")?.asString ?: "¡Solicitud de encargo recibida con éxito!"
|
||||||
|
Result.success(msg)
|
||||||
|
} else {
|
||||||
|
val errorMsg = json.get("error")?.asString ?: "Error al registrar encargo"
|
||||||
|
Result.failure(Exception(errorMsg))
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Error submitting custom request: ${e.message}")
|
||||||
|
Result.failure(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,55 @@
|
|||||||
|
package co.dripzone.app.model
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import co.dripzone.app.security.SecurityManager
|
||||||
|
|
||||||
|
data class UserProfile(
|
||||||
|
val name: String = "",
|
||||||
|
val phone: String = "",
|
||||||
|
val city: String = "Cali",
|
||||||
|
val address: String = "",
|
||||||
|
val isPhoneVerified: Boolean = false
|
||||||
|
)
|
||||||
|
|
||||||
|
object UserManager {
|
||||||
|
|
||||||
|
private const val KEY_USER_NAME = "user_profile_name"
|
||||||
|
private const val KEY_USER_PHONE = "user_profile_phone"
|
||||||
|
private const val KEY_USER_CITY = "user_profile_city"
|
||||||
|
private const val KEY_USER_ADDRESS = "user_profile_address"
|
||||||
|
private const val KEY_PHONE_VERIFIED = "user_phone_verified"
|
||||||
|
|
||||||
|
fun getUser(context: Context): UserProfile {
|
||||||
|
val prefs = SecurityManager.getSecurePreferences(context)
|
||||||
|
return UserProfile(
|
||||||
|
name = prefs.getString(KEY_USER_NAME, "") ?: "",
|
||||||
|
phone = prefs.getString(KEY_USER_PHONE, "") ?: "",
|
||||||
|
city = prefs.getString(KEY_USER_CITY, "Cali") ?: "Cali",
|
||||||
|
address = prefs.getString(KEY_USER_ADDRESS, "") ?: "",
|
||||||
|
isPhoneVerified = prefs.getBoolean(KEY_PHONE_VERIFIED, false)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveUser(
|
||||||
|
context: Context,
|
||||||
|
name: String,
|
||||||
|
phone: String,
|
||||||
|
city: String,
|
||||||
|
address: String,
|
||||||
|
isVerified: Boolean
|
||||||
|
) {
|
||||||
|
val prefs = SecurityManager.getSecurePreferences(context)
|
||||||
|
prefs.edit()
|
||||||
|
.putString(KEY_USER_NAME, name.trim())
|
||||||
|
.putString(KEY_USER_PHONE, phone.trim())
|
||||||
|
.putString(KEY_USER_CITY, city.trim())
|
||||||
|
.putString(KEY_USER_ADDRESS, address.trim())
|
||||||
|
.putBoolean(KEY_PHONE_VERIFIED, isVerified)
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setPhoneVerified(context: Context, isVerified: Boolean) {
|
||||||
|
val prefs = SecurityManager.getSecurePreferences(context)
|
||||||
|
prefs.edit().putBoolean(KEY_PHONE_VERIFIED, isVerified).apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -60,34 +60,13 @@ class CartBottomSheet : BottomSheetDialogFragment() {
|
|||||||
val cartList = CartManager.getItems()
|
val cartList = CartManager.getItems()
|
||||||
if (cartList.isEmpty()) return@setOnClickListener
|
if (cartList.isEmpty()) return@setOnClickListener
|
||||||
|
|
||||||
val subtotal = CartManager.getSubtotal()
|
val verificationSheet = OrderVerificationBottomSheet.newInstance(
|
||||||
val total = subtotal + selectedCity.shippingCost
|
city = selectedCity,
|
||||||
val balanceDue = if (total > 10000.0) total - 10000.0 else 0.0
|
isDirect = false
|
||||||
|
|
||||||
val itemsText = cartList.joinToString("\n") {
|
|
||||||
"👟 *${it.product.name}* (Talla: *${it.selectedSize}*, Cant: ${it.quantity}) - ${it.formattedItemTotal()}"
|
|
||||||
}
|
|
||||||
|
|
||||||
val format = NumberFormat.getCurrencyInstance(Locale("es", "CO"))
|
|
||||||
format.maximumFractionDigits = 0
|
|
||||||
val formattedTotal = format.format(total) + " COP"
|
|
||||||
val formattedBalance = format.format(balanceDue) + " COP"
|
|
||||||
|
|
||||||
val message = buildString {
|
|
||||||
append("¡Hola DripZone! 🔥 Quiero ordenar este pedido desde la app móvil:\n\n")
|
|
||||||
append("📦 *RESUMEN DEL PEDIDO:*\n")
|
|
||||||
append(itemsText)
|
|
||||||
append("\n\n📍 *Ciudad de Despacho:* ${selectedCity.name} (${selectedCity.formattedShipping()})\n")
|
|
||||||
append("💰 *TOTAL A PAGAR:* $formattedTotal\n")
|
|
||||||
append("⚡ *Condición:* Pago Contraentrega (Separo con $10.000 COP y cancelo saldo de $formattedBalance al recibir en mi puerta).\n\n")
|
|
||||||
append("¿Me confirman disponibilidad para programar el despacho?")
|
|
||||||
}
|
|
||||||
|
|
||||||
SecurityManager.openWhatsApp(
|
|
||||||
requireContext(),
|
|
||||||
SecurityManager.WHATSAPP_STORE_PHONE,
|
|
||||||
message
|
|
||||||
)
|
)
|
||||||
|
parentFragmentManager.let {
|
||||||
|
verificationSheet.show(it, OrderVerificationBottomSheet.TAG)
|
||||||
|
}
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,9 +5,13 @@ import android.view.LayoutInflater
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import co.dripzone.app.api.ApiClient
|
||||||
import co.dripzone.app.databinding.DialogCustomOrderBinding
|
import co.dripzone.app.databinding.DialogCustomOrderBinding
|
||||||
|
import co.dripzone.app.model.UserManager
|
||||||
import co.dripzone.app.security.SecurityManager
|
import co.dripzone.app.security.SecurityManager
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class CustomOrderBottomSheet : BottomSheetDialogFragment() {
|
class CustomOrderBottomSheet : BottomSheetDialogFragment() {
|
||||||
|
|
||||||
@ -17,6 +21,13 @@ class CustomOrderBottomSheet : BottomSheetDialogFragment() {
|
|||||||
private var initialModel: String = ""
|
private var initialModel: String = ""
|
||||||
private var initialBrand: String = ""
|
private var initialBrand: String = ""
|
||||||
|
|
||||||
|
private var customerName: String = ""
|
||||||
|
private var customerPhone: String = ""
|
||||||
|
private var customerCity: String = ""
|
||||||
|
private var modelDesired: String = ""
|
||||||
|
private var sizeDesired: String = ""
|
||||||
|
private var notes: String = ""
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
arguments?.let {
|
arguments?.let {
|
||||||
@ -37,36 +48,34 @@ class CustomOrderBottomSheet : BottomSheetDialogFragment() {
|
|||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
binding.btnCloseCustomModal.setOnClickListener { dismiss() }
|
||||||
|
|
||||||
|
// Pre-fill user data if registered locally
|
||||||
|
val user = UserManager.getUser(requireContext())
|
||||||
|
if (user.name.isNotBlank()) binding.etCustomCustomerName.setText(user.name)
|
||||||
|
if (user.phone.isNotBlank()) binding.etCustomCustomerPhone.setText(user.phone)
|
||||||
|
if (user.city.isNotBlank()) binding.etCustomCustomerCity.setText(user.city)
|
||||||
|
|
||||||
if (initialModel.isNotBlank()) {
|
if (initialModel.isNotBlank()) {
|
||||||
binding.etCustomModel.setText(initialModel)
|
binding.etCustomModel.setText(initialModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.btnSubmitCustomOrder.setOnClickListener {
|
binding.btnSubmitCustomOrder.setOnClickListener {
|
||||||
val rawModel = binding.etCustomModel.text?.toString()
|
handleContinueToCustomOtp()
|
||||||
val rawSize = binding.etCustomSize.text?.toString()
|
}
|
||||||
val rawNotes = binding.etCustomNotes.text?.toString()
|
|
||||||
|
|
||||||
val sanitizedModel = SecurityManager.sanitizeInput(rawModel)
|
binding.btnVerifyCustomOtp.setOnClickListener {
|
||||||
val sanitizedSize = SecurityManager.sanitizeInput(rawSize)
|
handleVerifyCustomOtp()
|
||||||
val sanitizedNotes = SecurityManager.sanitizeInput(rawNotes)
|
}
|
||||||
|
|
||||||
if (sanitizedModel.isBlank()) {
|
binding.tvResendCustomOtp.setOnClickListener {
|
||||||
binding.etCustomModel.error = "Por favor indica el modelo deseado"
|
sendCustomOtp()
|
||||||
return@setOnClickListener
|
}
|
||||||
}
|
|
||||||
|
|
||||||
val message = buildString {
|
binding.btnFinishCustom.setOnClickListener { dismiss() }
|
||||||
append("¡Hola DripZone! 🚀 Quiero consultar por un *Pedido Especial / Encargo*:\n\n")
|
|
||||||
append("👟 *Modelo deseado:* $sanitizedModel\n")
|
|
||||||
if (sanitizedSize.isNotBlank()) {
|
|
||||||
append("📏 *Talla estimada:* $sanitizedSize\n")
|
|
||||||
}
|
|
||||||
if (sanitizedNotes.isNotBlank()) {
|
|
||||||
append("📝 *Detalles / Colorway:* $sanitizedNotes\n")
|
|
||||||
}
|
|
||||||
append("\n¿Tienen disponibilidad o pueden cotizarlo con sus proveedores mayoristas?")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
binding.btnCustomOpenWhatsApp.setOnClickListener {
|
||||||
|
val message = "¡Hola Carlos! Acabo de registrar un encargo en la app de DripZone para las *$modelDesired* en talla *$sizeDesired*. ¿Me confirmas cotización con los proveedores mayoristas?"
|
||||||
SecurityManager.openWhatsApp(
|
SecurityManager.openWhatsApp(
|
||||||
requireContext(),
|
requireContext(),
|
||||||
SecurityManager.WHATSAPP_STORE_PHONE,
|
SecurityManager.WHATSAPP_STORE_PHONE,
|
||||||
@ -76,6 +85,141 @@ class CustomOrderBottomSheet : BottomSheetDialogFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleContinueToCustomOtp() {
|
||||||
|
val rawName = binding.etCustomCustomerName.text?.toString()?.trim() ?: ""
|
||||||
|
val rawPhone = binding.etCustomCustomerPhone.text?.toString()?.replace("\\D".toRegex(), "") ?: ""
|
||||||
|
val rawCity = binding.etCustomCustomerCity.text?.toString()?.trim() ?: ""
|
||||||
|
val rawModel = binding.etCustomModel.text?.toString()?.trim() ?: ""
|
||||||
|
val rawSize = binding.etCustomSize.text?.toString()?.trim() ?: ""
|
||||||
|
val rawNotes = binding.etCustomNotes.text?.toString()?.trim() ?: ""
|
||||||
|
|
||||||
|
if (rawName.isBlank()) {
|
||||||
|
binding.etCustomCustomerName.error = "Ingresa tu nombre completo"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (rawPhone.length < 10) {
|
||||||
|
binding.etCustomCustomerPhone.error = "Ingresa tu WhatsApp a 10 dígitos"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (rawModel.isBlank()) {
|
||||||
|
binding.etCustomModel.error = "Indica el modelo o referencia"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (rawSize.isBlank()) {
|
||||||
|
binding.etCustomSize.error = "Indica la talla deseada"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
customerName = rawName
|
||||||
|
customerPhone = rawPhone
|
||||||
|
customerCity = if (rawCity.isNotBlank()) rawCity else "Colombia"
|
||||||
|
modelDesired = rawModel
|
||||||
|
sizeDesired = rawSize
|
||||||
|
notes = rawNotes
|
||||||
|
|
||||||
|
val user = UserManager.getUser(requireContext())
|
||||||
|
if (user.isPhoneVerified && user.phone == customerPhone) {
|
||||||
|
submitFinalCustomRequest()
|
||||||
|
} else {
|
||||||
|
sendCustomOtp()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendCustomOtp() {
|
||||||
|
binding.layoutCustomForm.visibility = View.GONE
|
||||||
|
binding.layoutCustomOtp.visibility = View.VISIBLE
|
||||||
|
binding.pbCustomLoading.visibility = View.VISIBLE
|
||||||
|
binding.btnVerifyCustomOtp.isEnabled = false
|
||||||
|
|
||||||
|
val masked = if (customerPhone.length >= 10) {
|
||||||
|
"${customerPhone.take(3)}***${customerPhone.takeLast(4)}"
|
||||||
|
} else customerPhone
|
||||||
|
|
||||||
|
binding.tvCustomOtpInstruction.text = "Enviamos un código de seguridad de 6 dígitos a tu WhatsApp (+57 $masked) para validar tu cuenta y registrar tu encargo:"
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
val result = ApiClient.sendOtp(customerPhone, customerName)
|
||||||
|
binding.pbCustomLoading.visibility = View.GONE
|
||||||
|
binding.btnVerifyCustomOtp.isEnabled = true
|
||||||
|
|
||||||
|
result.onSuccess {
|
||||||
|
Toast.makeText(requireContext(), "Código enviado a tu WhatsApp", Toast.LENGTH_SHORT).show()
|
||||||
|
}.onFailure { err ->
|
||||||
|
Toast.makeText(requireContext(), "Aviso: ${err.message}", Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleVerifyCustomOtp() {
|
||||||
|
val code = binding.etCustomOtpCode.text?.toString()?.trim() ?: ""
|
||||||
|
if (code.length < 6) {
|
||||||
|
binding.etCustomOtpCode.error = "Ingresa el código de 6 dígitos"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.pbCustomLoading.visibility = View.VISIBLE
|
||||||
|
binding.btnVerifyCustomOtp.isEnabled = false
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
val verifyResult = ApiClient.verifyOtp(
|
||||||
|
phone = customerPhone,
|
||||||
|
code = code,
|
||||||
|
name = customerName,
|
||||||
|
city = customerCity,
|
||||||
|
address = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
if (verifyResult.isSuccess && verifyResult.getOrNull() == true) {
|
||||||
|
UserManager.saveUser(
|
||||||
|
requireContext(),
|
||||||
|
customerName,
|
||||||
|
customerPhone,
|
||||||
|
customerCity,
|
||||||
|
"",
|
||||||
|
isVerified = true
|
||||||
|
)
|
||||||
|
submitFinalCustomRequest()
|
||||||
|
} else {
|
||||||
|
binding.pbCustomLoading.visibility = View.GONE
|
||||||
|
binding.btnVerifyCustomOtp.isEnabled = true
|
||||||
|
val errorMsg = verifyResult.exceptionOrNull()?.message ?: "Código incorrecto o expirado"
|
||||||
|
binding.etCustomOtpCode.error = errorMsg
|
||||||
|
Toast.makeText(requireContext(), errorMsg, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun submitFinalCustomRequest() {
|
||||||
|
binding.pbCustomLoading.visibility = View.VISIBLE
|
||||||
|
|
||||||
|
val effectiveBrand = if (initialBrand.isNotBlank()) initialBrand else "Sneakers"
|
||||||
|
val requestData = mapOf(
|
||||||
|
"customerName" to customerName,
|
||||||
|
"customerPhone" to customerPhone,
|
||||||
|
"customerCity" to customerCity,
|
||||||
|
"brand" to effectiveBrand,
|
||||||
|
"modelName" to modelDesired,
|
||||||
|
"size" to sizeDesired,
|
||||||
|
"colorway" to notes,
|
||||||
|
"notes" to "Solicitud registrada desde App Android Nativa (WhatsApp OTP Verificado)"
|
||||||
|
)
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
val submitResult = ApiClient.submitCustomRequest(requestData)
|
||||||
|
binding.pbCustomLoading.visibility = View.GONE
|
||||||
|
|
||||||
|
submitResult.onSuccess {
|
||||||
|
binding.layoutCustomForm.visibility = View.GONE
|
||||||
|
binding.layoutCustomOtp.visibility = View.GONE
|
||||||
|
binding.layoutCustomSuccess.visibility = View.VISIBLE
|
||||||
|
binding.tvCustomSuccessMessage.text = "¡Tu número de WhatsApp (+57 $customerPhone) fue verificado y tu encargo para las '$modelDesired' fue registrado en la plataforma!\n\nLa plataforma notificó al administrador Carlos (+57 316 745 5877) y te contactaremos por WhatsApp con la cotización exacta."
|
||||||
|
}.onFailure { err ->
|
||||||
|
binding.btnVerifyCustomOtp.isEnabled = true
|
||||||
|
Toast.makeText(requireContext(), "Error al registrar: ${err.message}", Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
_binding = null
|
_binding = null
|
||||||
|
|||||||
@ -0,0 +1,299 @@
|
|||||||
|
package co.dripzone.app.ui
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import co.dripzone.app.databinding.BottomSheetOrderVerificationBinding
|
||||||
|
import co.dripzone.app.api.ApiClient
|
||||||
|
import co.dripzone.app.model.CartItem
|
||||||
|
import co.dripzone.app.model.CartManager
|
||||||
|
import co.dripzone.app.model.City
|
||||||
|
import co.dripzone.app.model.UserManager
|
||||||
|
import co.dripzone.app.security.SecurityManager
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.text.NumberFormat
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class OrderVerificationBottomSheet : BottomSheetDialogFragment() {
|
||||||
|
|
||||||
|
private var _binding: BottomSheetOrderVerificationBinding? = null
|
||||||
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
|
private var selectedCity: City = City.defaultCities().first()
|
||||||
|
private var isDirectProductOrder: Boolean = false
|
||||||
|
private var directItem: CartItem? = null
|
||||||
|
|
||||||
|
private var customerName: String = ""
|
||||||
|
private var customerPhone: String = ""
|
||||||
|
private var customerAddress: String = ""
|
||||||
|
private var createdOrderId: String = ""
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
arguments?.let {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
val cityArg = it.getSerializable(ARG_CITY) as? City
|
||||||
|
if (cityArg != null) selectedCity = cityArg
|
||||||
|
isDirectProductOrder = it.getBoolean(ARG_IS_DIRECT, false)
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
directItem = it.getSerializable(ARG_DIRECT_ITEM) as? CartItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
_binding = BottomSheetOrderVerificationBinding.inflate(inflater, container, false)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
binding.btnCloseOrderModal.setOnClickListener { dismiss() }
|
||||||
|
|
||||||
|
// Pre-fill user data if already registered locally
|
||||||
|
val user = UserManager.getUser(requireContext())
|
||||||
|
if (user.name.isNotBlank()) binding.etCustomerName.setText(user.name)
|
||||||
|
if (user.phone.isNotBlank()) binding.etCustomerPhone.setText(user.phone)
|
||||||
|
if (user.address.isNotBlank()) binding.etCustomerAddress.setText(user.address)
|
||||||
|
|
||||||
|
// Setup economic calculations
|
||||||
|
val subtotal = if (isDirectProductOrder && directItem != null) {
|
||||||
|
directItem!!.itemTotal
|
||||||
|
} else {
|
||||||
|
CartManager.getSubtotal()
|
||||||
|
}
|
||||||
|
val total = subtotal + selectedCity.shippingCost
|
||||||
|
val balanceDue = if (total > 10000.0) total - 10000.0 else 0.0
|
||||||
|
|
||||||
|
val format = NumberFormat.getCurrencyInstance(Locale("es", "CO")).apply {
|
||||||
|
maximumFractionDigits = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.tvOrderSummaryCity.text = "Destino: ${selectedCity.name} (${selectedCity.formattedShipping()})"
|
||||||
|
binding.tvOrderSummaryTotal.text = "${format.format(total)} COP"
|
||||||
|
binding.tvOrderSummaryBreakdown.text = "Pago Contraentrega • Separas con $10.000 COP y cancelas ${format.format(balanceDue)} COP al recibir."
|
||||||
|
|
||||||
|
binding.btnContinueToOtp.setOnClickListener {
|
||||||
|
handleContinueToOtp()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.btnVerifyOtpAndSubmit.setOnClickListener {
|
||||||
|
handleVerifyOtpAndSubmit()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.tvResendOtp.setOnClickListener {
|
||||||
|
sendOtpCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.btnFinishOrder.setOnClickListener {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.btnOpenWhatsAppChat.setOnClickListener {
|
||||||
|
val message = "¡Hola Carlos! Acabo de registrar mi pedido en la app oficial de DripZone con la orden *$createdOrderId*. ¿Me confirmas la programación del despacho contraentrega a ${selectedCity.name}?"
|
||||||
|
SecurityManager.openWhatsApp(
|
||||||
|
requireContext(),
|
||||||
|
SecurityManager.WHATSAPP_STORE_PHONE,
|
||||||
|
message
|
||||||
|
)
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleContinueToOtp() {
|
||||||
|
val rawName = binding.etCustomerName.text?.toString()?.trim() ?: ""
|
||||||
|
val rawPhone = binding.etCustomerPhone.text?.toString()?.replace("\\D".toRegex(), "") ?: ""
|
||||||
|
val rawAddress = binding.etCustomerAddress.text?.toString()?.trim() ?: ""
|
||||||
|
|
||||||
|
if (rawName.isBlank()) {
|
||||||
|
binding.etCustomerName.error = "Por favor ingresa tu nombre completo"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (rawPhone.length < 10) {
|
||||||
|
binding.etCustomerPhone.error = "Ingresa tu celular de WhatsApp a 10 dígitos"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (rawAddress.isBlank()) {
|
||||||
|
binding.etCustomerAddress.error = "Ingresa tu dirección completa de entrega"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
customerName = rawName
|
||||||
|
customerPhone = rawPhone
|
||||||
|
customerAddress = rawAddress
|
||||||
|
|
||||||
|
val user = UserManager.getUser(requireContext())
|
||||||
|
// Si el usuario ya está verificado para este número, procede directo a la orden
|
||||||
|
if (user.isPhoneVerified && user.phone == customerPhone) {
|
||||||
|
submitFinalOrder()
|
||||||
|
} else {
|
||||||
|
sendOtpCode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendOtpCode() {
|
||||||
|
binding.layoutStepCustomer.visibility = View.GONE
|
||||||
|
binding.layoutStepOtp.visibility = View.VISIBLE
|
||||||
|
binding.pbOtpLoading.visibility = View.VISIBLE
|
||||||
|
binding.btnVerifyOtpAndSubmit.isEnabled = false
|
||||||
|
|
||||||
|
val masked = if (customerPhone.length >= 10) {
|
||||||
|
"${customerPhone.take(3)}***${customerPhone.takeLast(4)}"
|
||||||
|
} else customerPhone
|
||||||
|
|
||||||
|
binding.tvOtpInstruction.text = "Enviamos un código de seguridad de 6 dígitos a tu WhatsApp (+57 $masked) para validar tu cuenta y registrar tu orden:"
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
val result = ApiClient.sendOtp(customerPhone, customerName)
|
||||||
|
binding.pbOtpLoading.visibility = View.GONE
|
||||||
|
binding.btnVerifyOtpAndSubmit.isEnabled = true
|
||||||
|
|
||||||
|
result.onSuccess { msg ->
|
||||||
|
Toast.makeText(requireContext(), "Código enviado por WhatsApp: Revisa tu chat", Toast.LENGTH_LONG).show()
|
||||||
|
}.onFailure { err ->
|
||||||
|
Toast.makeText(requireContext(), "Aviso: ${err.message}", Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleVerifyOtpAndSubmit() {
|
||||||
|
val otpCode = binding.etOtpCode.text?.toString()?.trim() ?: ""
|
||||||
|
if (otpCode.length < 6) {
|
||||||
|
binding.etOtpCode.error = "Ingresa el código de 6 dígitos"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.pbOtpLoading.visibility = View.VISIBLE
|
||||||
|
binding.btnVerifyOtpAndSubmit.isEnabled = false
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
val verifyResult = ApiClient.verifyOtp(
|
||||||
|
phone = customerPhone,
|
||||||
|
code = otpCode,
|
||||||
|
name = customerName,
|
||||||
|
city = selectedCity.name,
|
||||||
|
address = customerAddress
|
||||||
|
)
|
||||||
|
|
||||||
|
if (verifyResult.isSuccess && verifyResult.getOrNull() == true) {
|
||||||
|
// Save verified user locally
|
||||||
|
UserManager.saveUser(
|
||||||
|
requireContext(),
|
||||||
|
customerName,
|
||||||
|
customerPhone,
|
||||||
|
selectedCity.name,
|
||||||
|
customerAddress,
|
||||||
|
isVerified = true
|
||||||
|
)
|
||||||
|
submitFinalOrder()
|
||||||
|
} else {
|
||||||
|
binding.pbOtpLoading.visibility = View.GONE
|
||||||
|
binding.btnVerifyOtpAndSubmit.isEnabled = true
|
||||||
|
val errorMsg = verifyResult.exceptionOrNull()?.message ?: "Código inválido o expirado"
|
||||||
|
binding.etOtpCode.error = errorMsg
|
||||||
|
Toast.makeText(requireContext(), errorMsg, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun submitFinalOrder() {
|
||||||
|
binding.pbOtpLoading.visibility = View.VISIBLE
|
||||||
|
|
||||||
|
val items = if (isDirectProductOrder && directItem != null) {
|
||||||
|
listOf(directItem!!)
|
||||||
|
} else {
|
||||||
|
CartManager.getItems()
|
||||||
|
}
|
||||||
|
|
||||||
|
val subtotal = items.sumOf { it.itemTotal }
|
||||||
|
val total = subtotal + selectedCity.shippingCost
|
||||||
|
val balanceDue = if (total > 10000.0) total - 10000.0 else 0.0
|
||||||
|
|
||||||
|
val itemsPayload = items.map {
|
||||||
|
mapOf(
|
||||||
|
"productId" to it.product.id,
|
||||||
|
"productName" to it.product.name,
|
||||||
|
"brand" to it.product.brand,
|
||||||
|
"size" to it.selectedSize,
|
||||||
|
"quantity" to it.quantity,
|
||||||
|
"price" to it.product.retailPrice,
|
||||||
|
"image" to it.product.firstImage()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val orderData = mapOf(
|
||||||
|
"customerName" to customerName,
|
||||||
|
"customerPhone" to customerPhone,
|
||||||
|
"address" to customerAddress,
|
||||||
|
"cityId" to selectedCity.id,
|
||||||
|
"cityName" to selectedCity.name,
|
||||||
|
"items" to itemsPayload,
|
||||||
|
"subtotal" to subtotal,
|
||||||
|
"shippingCost" to selectedCity.shippingCost,
|
||||||
|
"totalAmount" to total,
|
||||||
|
"depositAmount" to 10000.0,
|
||||||
|
"balanceDue" to balanceDue,
|
||||||
|
"paymentMethodName" to "Pago Contraentrega",
|
||||||
|
"notes" to "Pedido registrado desde App Android Nativa (WhatsApp OTP Verificado)"
|
||||||
|
)
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
val orderResult = ApiClient.submitOrder(orderData)
|
||||||
|
binding.pbOtpLoading.visibility = View.GONE
|
||||||
|
|
||||||
|
orderResult.onSuccess { orderId ->
|
||||||
|
createdOrderId = orderId
|
||||||
|
// Limpiar carrito
|
||||||
|
if (!isDirectProductOrder) {
|
||||||
|
CartManager.clear(requireContext())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mostrar pantalla de éxito
|
||||||
|
binding.layoutStepCustomer.visibility = View.GONE
|
||||||
|
binding.layoutStepOtp.visibility = View.GONE
|
||||||
|
binding.layoutStepSuccess.visibility = View.VISIBLE
|
||||||
|
binding.tvSuccessOrderId.text = "Orden #$orderId"
|
||||||
|
binding.tvSuccessMessage.text = "¡Tu número de WhatsApp (+57 $customerPhone) fue validado y tu pedido fue registrado en la plataforma!\n\nLa plataforma notificó automáticamente al administrador Carlos (+57 316 745 5877) para preparar tu paquete."
|
||||||
|
}.onFailure { err ->
|
||||||
|
binding.btnVerifyOtpAndSubmit.isEnabled = true
|
||||||
|
Toast.makeText(requireContext(), "Error al registrar orden: ${err.message}", Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
super.onDestroyView()
|
||||||
|
_binding = null
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG = "OrderVerificationBottomSheet"
|
||||||
|
private const val ARG_CITY = "arg_city"
|
||||||
|
private const val ARG_IS_DIRECT = "arg_is_direct"
|
||||||
|
private const val ARG_DIRECT_ITEM = "arg_direct_item"
|
||||||
|
|
||||||
|
fun newInstance(
|
||||||
|
city: City,
|
||||||
|
isDirect: Boolean = false,
|
||||||
|
directItem: CartItem? = null
|
||||||
|
): OrderVerificationBottomSheet {
|
||||||
|
return OrderVerificationBottomSheet().apply {
|
||||||
|
arguments = Bundle().apply {
|
||||||
|
putSerializable(ARG_CITY, city)
|
||||||
|
putBoolean(ARG_IS_DIRECT, isDirect)
|
||||||
|
if (directItem != null) {
|
||||||
|
putSerializable(ARG_DIRECT_ITEM, directItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -78,27 +78,23 @@ class ProductDetailBottomSheet : BottomSheetDialogFragment() {
|
|||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
// WhatsApp Order Checkout Button
|
// Direct Order Checkout Button with Verification
|
||||||
binding.btnOrderWhatsApp.setOnClickListener {
|
binding.btnOrderWhatsApp.setOnClickListener {
|
||||||
val sizeToOrder = if (selectedSize.isNotBlank()) selectedSize else sizeAdapter?.getSelectedSize() ?: "Por coordinar"
|
val sizeToOrder = if (selectedSize.isNotBlank()) selectedSize else sizeAdapter?.getSelectedSize() ?: "39"
|
||||||
val whatsappMessage = buildString {
|
val directItem = co.dripzone.app.model.CartItem(
|
||||||
append("¡Hola DripZone! 🔥 Me interesa ordenar este producto:\n\n")
|
product = p,
|
||||||
append("👟 *Producto:* ${p.name}\n")
|
selectedSize = sizeToOrder,
|
||||||
append("🏷️ *Marca:* ${p.brand}\n")
|
quantity = 1
|
||||||
append("📏 *Talla solicitada:* $sizeToOrder\n")
|
|
||||||
append("💰 *Precio:* ${p.formattedPrice()}\n")
|
|
||||||
append("🚚 *Método:* Envío con Pago Contraentrega\n")
|
|
||||||
if (imageUrl.isNotBlank()) {
|
|
||||||
append("\n🔗 *Foto:* $imageUrl\n")
|
|
||||||
}
|
|
||||||
append("\n¿Tienen disponibilidad inmediata para despacho?")
|
|
||||||
}
|
|
||||||
|
|
||||||
SecurityManager.openWhatsApp(
|
|
||||||
requireContext(),
|
|
||||||
SecurityManager.WHATSAPP_STORE_PHONE,
|
|
||||||
whatsappMessage
|
|
||||||
)
|
)
|
||||||
|
val city = (activity as? co.dripzone.app.MainActivity)?.selectedCity ?: co.dripzone.app.model.City.defaultCities().first()
|
||||||
|
val verificationSheet = OrderVerificationBottomSheet.newInstance(
|
||||||
|
city = city,
|
||||||
|
isDirect = true,
|
||||||
|
directItem = directItem
|
||||||
|
)
|
||||||
|
parentFragmentManager.let {
|
||||||
|
verificationSheet.show(it, OrderVerificationBottomSheet.TAG)
|
||||||
|
}
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,16 +8,18 @@
|
|||||||
android:id="@+id/ivSplash"
|
android:id="@+id/ivSplash"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:adjustViewBounds="true"
|
||||||
android:contentDescription="DripZone Splash Screen"
|
android:contentDescription="DripZone Splash Screen"
|
||||||
android:scaleType="centerCrop"
|
android:scaleType="fitCenter"
|
||||||
android:src="@drawable/dripzone_splash_clean" />
|
android:src="@drawable/dripzone_splash_clean" />
|
||||||
|
|
||||||
<!-- Subtle bottom indicator -->
|
<!-- Subtle bottom indicator -->
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
android:layout_width="36dp"
|
android:layout_width="32dp"
|
||||||
android:layout_height="36dp"
|
android:layout_height="32dp"
|
||||||
android:layout_gravity="bottom|center_horizontal"
|
android:layout_gravity="bottom|center_horizontal"
|
||||||
android:layout_marginBottom="48dp"
|
android:layout_marginBottom="36dp"
|
||||||
android:indeterminateTint="@color/drip_fuchsia" />
|
android:indeterminateTint="@color/drip_fuchsia" />
|
||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|||||||
@ -0,0 +1,358 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@drawable/bg_bottom_sheet"
|
||||||
|
android:fillViewport="true">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="20dp">
|
||||||
|
|
||||||
|
<!-- Drag handle -->
|
||||||
|
<View
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="4dp"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:background="#40FFFFFF" />
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<RelativeLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvOrderModalTitle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_centerVertical="true"
|
||||||
|
android:fontFamily="sans-serif-black"
|
||||||
|
android:text="CONFIRMAR PEDIDO"
|
||||||
|
android:textColor="@color/text_primary"
|
||||||
|
android:textSize="17sp" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btnCloseOrderModal"
|
||||||
|
android:layout_width="36dp"
|
||||||
|
android:layout_height="36dp"
|
||||||
|
android:layout_alignParentEnd="true"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:contentDescription="Cerrar"
|
||||||
|
android:src="@drawable/ic_close"
|
||||||
|
app:tint="@color/text_secondary" />
|
||||||
|
</RelativeLayout>
|
||||||
|
|
||||||
|
<!-- STEP 1: Customer & Shipping Data Layout -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/layoutStepCustomer"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Ingresa tus datos para registrar tu pedido en la plataforma y coordinar el despacho contraentrega."
|
||||||
|
android:textColor="@color/text_secondary"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
|
||||||
|
<!-- Nombre -->
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="14dp"
|
||||||
|
android:fontFamily="sans-serif-medium"
|
||||||
|
android:text="Nombre y Apellido *"
|
||||||
|
android:textColor="@color/text_primary"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/etCustomerName"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="46dp"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
android:background="@drawable/bg_search_input"
|
||||||
|
android:hint="Ej. Juan Carlos Pérez"
|
||||||
|
android:inputType="textPersonName"
|
||||||
|
android:paddingHorizontal="14dp"
|
||||||
|
android:textColor="@color/text_primary"
|
||||||
|
android:textColorHint="@color/text_muted"
|
||||||
|
android:textSize="13sp" />
|
||||||
|
|
||||||
|
<!-- Celular WhatsApp -->
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:fontFamily="sans-serif-medium"
|
||||||
|
android:text="Celular / WhatsApp (10 dígitos) *"
|
||||||
|
android:textColor="@color/text_primary"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/etCustomerPhone"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="46dp"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
android:background="@drawable/bg_search_input"
|
||||||
|
android:hint="Ej. 3167455877"
|
||||||
|
android:inputType="phone"
|
||||||
|
android:maxLength="10"
|
||||||
|
android:paddingHorizontal="14dp"
|
||||||
|
android:textColor="@color/text_primary"
|
||||||
|
android:textColorHint="@color/text_muted"
|
||||||
|
android:textSize="13sp" />
|
||||||
|
|
||||||
|
<!-- Dirección -->
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:fontFamily="sans-serif-medium"
|
||||||
|
android:text="Dirección de entrega y Barrio *"
|
||||||
|
android:textColor="@color/text_primary"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/etCustomerAddress"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="46dp"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
android:background="@drawable/bg_search_input"
|
||||||
|
android:hint="Ej. Calle 15 #4-22, Barrio San Antonio"
|
||||||
|
android:inputType="textPostalAddress"
|
||||||
|
android:paddingHorizontal="14dp"
|
||||||
|
android:textColor="@color/text_primary"
|
||||||
|
android:textColorHint="@color/text_muted"
|
||||||
|
android:textSize="13sp" />
|
||||||
|
|
||||||
|
<!-- Resumen económico -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="14dp"
|
||||||
|
android:background="@drawable/bg_btn_secondary"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="12dp">
|
||||||
|
|
||||||
|
<RelativeLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvOrderSummaryCity"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fontFamily="sans-serif-medium"
|
||||||
|
android:text="Destino: Cali"
|
||||||
|
android:textColor="@color/text_primary"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvOrderSummaryTotal"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignParentEnd="true"
|
||||||
|
android:fontFamily="sans-serif-black"
|
||||||
|
android:text="$ 280.000 COP"
|
||||||
|
android:textColor="@color/drip_purple_glow"
|
||||||
|
android:textSize="13sp" />
|
||||||
|
</RelativeLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvOrderSummaryBreakdown"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:text="Pago Contraentrega • Separas con $10.000 COP y pagas el saldo al recibir"
|
||||||
|
android:textColor="@color/text_secondary"
|
||||||
|
android:textSize="10sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Botón Continuar -->
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btnContinueToOtp"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
android:layout_marginTop="18dp"
|
||||||
|
android:filterTouchesWhenObscured="true"
|
||||||
|
android:text="Validar Celular y Registrar Pedido"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
android:textColor="#FFFFFF"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
app:backgroundTint="@color/drip_purple"
|
||||||
|
app:cornerRadius="14dp"
|
||||||
|
app:icon="@drawable/ic_whatsapp"
|
||||||
|
app:iconGravity="textStart"
|
||||||
|
app:iconPadding="8dp"
|
||||||
|
app:iconSize="18dp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- STEP 2: OTP Verification Layout -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/layoutStepOtp"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fontFamily="sans-serif-black"
|
||||||
|
android:text="🔐 Verificación de WhatsApp"
|
||||||
|
android:textColor="@color/text_primary"
|
||||||
|
android:textSize="15sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvOtpInstruction"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
android:text="Ingresa el código de seguridad de 6 dígitos que enviamos por WhatsApp a tu celular para validar tu cuenta y confirmar el pedido:"
|
||||||
|
android:textColor="@color/text_secondary"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
|
||||||
|
<!-- Input OTP de 6 dígitos -->
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/etOtpCode"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="54dp"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:background="@drawable/bg_search_input"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:gravity="center"
|
||||||
|
android:hint="• • • • • •"
|
||||||
|
android:inputType="number"
|
||||||
|
android:letterSpacing="0.3"
|
||||||
|
android:maxLength="6"
|
||||||
|
android:textColor="@color/text_primary"
|
||||||
|
android:textColorHint="@color/text_muted"
|
||||||
|
android:textSize="22sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<!-- Loading indicator -->
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/pbOtpLoading"
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:layout_marginTop="14dp"
|
||||||
|
android:indeterminateTint="@color/drip_fuchsia"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<!-- Botón Verificar y Completar -->
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btnVerifyOtpAndSubmit"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:filterTouchesWhenObscured="true"
|
||||||
|
android:text="Confirmar Código OTP"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
android:textColor="#FFFFFF"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
app:backgroundTint="@color/drip_purple"
|
||||||
|
app:cornerRadius="14dp" />
|
||||||
|
|
||||||
|
<!-- Reenviar código -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvResendOtp"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:layout_marginTop="14dp"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
android:text="¿No recibiste el código? Reenviar por WhatsApp"
|
||||||
|
android:textColor="@color/drip_purple_glow"
|
||||||
|
android:textSize="11sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- STEP 3: Success Confirmation Layout -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/layoutStepSuccess"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:gravity="center_horizontal"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="64dp"
|
||||||
|
android:layout_height="64dp"
|
||||||
|
android:src="@drawable/ic_sparkle"
|
||||||
|
app:tint="@color/drip_fuchsia" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="14dp"
|
||||||
|
android:fontFamily="sans-serif-black"
|
||||||
|
android:text="¡PEDIDO REGISTRADO CON ÉXITO!"
|
||||||
|
android:textColor="@color/text_primary"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvSuccessOrderId"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:text="Orden #P-123456"
|
||||||
|
android:textColor="@color/drip_purple_glow"
|
||||||
|
android:textSize="13sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvSuccessMessage"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="10dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="Tu número fue verificado con éxito y tu pedido ya está registrado en nuestra plataforma.\n\nLa plataforma ha notificado automáticamente al administrador Carlos (+57 316 745 5877) y recibirás confirmación oficial a tu WhatsApp."
|
||||||
|
android:textColor="@color/text_secondary"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btnOpenWhatsAppChat"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
android:layout_marginTop="20dp"
|
||||||
|
android:filterTouchesWhenObscured="true"
|
||||||
|
android:text="Abrir Chat con el Asesor en WhatsApp"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
android:textColor="#FFFFFF"
|
||||||
|
android:textSize="13sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
app:backgroundTint="@color/accent_whatsapp"
|
||||||
|
app:cornerRadius="14dp"
|
||||||
|
app:icon="@drawable/ic_whatsapp"
|
||||||
|
app:iconGravity="textStart"
|
||||||
|
app:iconPadding="8dp"
|
||||||
|
app:iconSize="18dp" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btnFinishOrder"
|
||||||
|
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="46dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="Cerrar y Seguir Comprando"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
android:textColor="@color/text_secondary"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</androidx.core.widget.NestedScrollView>
|
||||||
@ -19,109 +19,346 @@
|
|||||||
android:layout_marginBottom="16dp"
|
android:layout_marginBottom="16dp"
|
||||||
android:background="#40FFFFFF" />
|
android:background="#40FFFFFF" />
|
||||||
|
|
||||||
<TextView
|
<RelativeLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content">
|
||||||
android:fontFamily="sans-serif-black"
|
|
||||||
android:text="🔥 Pedido Especial / Encargo"
|
<TextView
|
||||||
android:textColor="@color/text_primary"
|
android:layout_width="wrap_content"
|
||||||
android:textSize="20sp" />
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_centerVertical="true"
|
||||||
|
android:fontFamily="sans-serif-black"
|
||||||
|
android:text="🔥 Pedido Especial / Encargo"
|
||||||
|
android:textColor="@color/text_primary"
|
||||||
|
android:textSize="18sp" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btnCloseCustomModal"
|
||||||
|
android:layout_width="36dp"
|
||||||
|
android:layout_height="36dp"
|
||||||
|
android:layout_alignParentEnd="true"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:contentDescription="Cerrar"
|
||||||
|
android:src="@drawable/ic_close"
|
||||||
|
app:tint="@color/text_secondary" />
|
||||||
|
</RelativeLayout>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="6dp"
|
android:layout_marginTop="4dp"
|
||||||
android:text="¿Buscas un modelo exclusivo o una talla específica de nuestros proveedores? Te lo conseguimos bajo pedido."
|
android:text="¿Buscas un modelo exclusivo no listado? Te lo cotizamos directamente con nuestras bodegas mayoristas."
|
||||||
android:textColor="@color/text_secondary"
|
android:textColor="@color/text_secondary"
|
||||||
android:textSize="13sp" />
|
android:textSize="12sp" />
|
||||||
|
|
||||||
<!-- Reference / Model input -->
|
<!-- Form Step Layout -->
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
<LinearLayout
|
||||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
android:id="@+id/layoutCustomForm"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="18dp"
|
android:layout_marginTop="12dp"
|
||||||
android:hint="Modelo / Referencia (ej. Jordan 4 Military Black)"
|
android:orientation="vertical">
|
||||||
android:textColorHint="@color/text_muted"
|
|
||||||
app:boxBackgroundColor="@color/bg_input"
|
|
||||||
app:boxCornerRadiusBottomEnd="12dp"
|
|
||||||
app:boxCornerRadiusBottomStart="12dp"
|
|
||||||
app:boxCornerRadiusTopEnd="12dp"
|
|
||||||
app:boxCornerRadiusTopStart="12dp"
|
|
||||||
app:boxStrokeColor="@color/drip_fuchsia">
|
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
<!-- Nombre -->
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fontFamily="sans-serif-medium"
|
||||||
|
android:text="Tu Nombre Completo *"
|
||||||
|
android:textColor="@color/text_primary"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/etCustomCustomerName"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="46dp"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:background="@drawable/bg_search_input"
|
||||||
|
android:hint="Ej. Carlos Mendoza"
|
||||||
|
android:inputType="textPersonName"
|
||||||
|
android:paddingHorizontal="14dp"
|
||||||
|
android:textColor="@color/text_primary"
|
||||||
|
android:textColorHint="@color/text_muted"
|
||||||
|
android:textSize="13sp" />
|
||||||
|
|
||||||
|
<!-- WhatsApp -->
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="10dp"
|
||||||
|
android:fontFamily="sans-serif-medium"
|
||||||
|
android:text="Tu WhatsApp (10 dígitos) *"
|
||||||
|
android:textColor="@color/text_primary"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/etCustomCustomerPhone"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="46dp"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:background="@drawable/bg_search_input"
|
||||||
|
android:hint="Ej. 3167455877"
|
||||||
|
android:inputType="phone"
|
||||||
|
android:maxLength="10"
|
||||||
|
android:paddingHorizontal="14dp"
|
||||||
|
android:textColor="@color/text_primary"
|
||||||
|
android:textColorHint="@color/text_muted"
|
||||||
|
android:textSize="13sp" />
|
||||||
|
|
||||||
|
<!-- Ciudad -->
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="10dp"
|
||||||
|
android:fontFamily="sans-serif-medium"
|
||||||
|
android:text="Ciudad *"
|
||||||
|
android:textColor="@color/text_primary"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/etCustomCustomerCity"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="46dp"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:background="@drawable/bg_search_input"
|
||||||
|
android:hint="Ej. Cali"
|
||||||
|
android:inputType="textCapWords"
|
||||||
|
android:paddingHorizontal="14dp"
|
||||||
|
android:textColor="@color/text_primary"
|
||||||
|
android:textColorHint="@color/text_muted"
|
||||||
|
android:textSize="13sp" />
|
||||||
|
|
||||||
|
<!-- Modelo -->
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="10dp"
|
||||||
|
android:fontFamily="sans-serif-medium"
|
||||||
|
android:text="Modelo / Referencia de Zapatilla *"
|
||||||
|
android:textColor="@color/text_primary"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
android:id="@+id/etCustomModel"
|
android:id="@+id/etCustomModel"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="46dp"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:background="@drawable/bg_search_input"
|
||||||
|
android:hint="Ej. Jordan 4 Retro Military Black"
|
||||||
|
android:inputType="textCapWords"
|
||||||
|
android:paddingHorizontal="14dp"
|
||||||
android:textColor="@color/text_primary"
|
android:textColor="@color/text_primary"
|
||||||
android:textSize="14sp" />
|
android:textColorHint="@color/text_muted"
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
android:textSize="13sp" />
|
||||||
|
|
||||||
<!-- Size input -->
|
<!-- Talla -->
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
<TextView
|
||||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
android:layout_width="wrap_content"
|
||||||
android:layout_width="match_parent"
|
android:layout_height="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_marginTop="10dp"
|
||||||
android:layout_marginTop="12dp"
|
android:fontFamily="sans-serif-medium"
|
||||||
android:hint="Talla deseada (ej. 41 EUR / 8 US)"
|
android:text="Talla deseada *"
|
||||||
android:textColorHint="@color/text_muted"
|
android:textColor="@color/text_primary"
|
||||||
app:boxBackgroundColor="@color/bg_input"
|
android:textSize="12sp" />
|
||||||
app:boxCornerRadiusBottomEnd="12dp"
|
|
||||||
app:boxCornerRadiusBottomStart="12dp"
|
|
||||||
app:boxCornerRadiusTopEnd="12dp"
|
|
||||||
app:boxCornerRadiusTopStart="12dp"
|
|
||||||
app:boxStrokeColor="@color/drip_fuchsia">
|
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
<EditText
|
||||||
android:id="@+id/etCustomSize"
|
android:id="@+id/etCustomSize"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="46dp"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:background="@drawable/bg_search_input"
|
||||||
|
android:hint="Ej. 41 EUR (8 US)"
|
||||||
|
android:inputType="text"
|
||||||
|
android:paddingHorizontal="14dp"
|
||||||
android:textColor="@color/text_primary"
|
android:textColor="@color/text_primary"
|
||||||
android:textSize="14sp" />
|
android:textColorHint="@color/text_muted"
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
android:textSize="13sp" />
|
||||||
|
|
||||||
<!-- Notes input -->
|
<!-- Notas / Presupuesto -->
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
<TextView
|
||||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="10dp"
|
||||||
|
android:fontFamily="sans-serif-medium"
|
||||||
|
android:text="Colorway / Presupuesto / Detalles"
|
||||||
|
android:textColor="@color/text_primary"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/etCustomNotes"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="46dp"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:background="@drawable/bg_search_input"
|
||||||
|
android:hint="Ej. Color blanco/negro, presupuesto hasta $300.000"
|
||||||
|
android:inputType="text"
|
||||||
|
android:paddingHorizontal="14dp"
|
||||||
|
android:textColor="@color/text_primary"
|
||||||
|
android:textColorHint="@color/text_muted"
|
||||||
|
android:textSize="13sp" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btnSubmitCustomOrder"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="52dp"
|
||||||
|
android:layout_marginTop="18dp"
|
||||||
|
android:filterTouchesWhenObscured="true"
|
||||||
|
android:text="Validar y Registrar Encargo"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
android:textColor="#FFFFFF"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
app:backgroundTint="@color/drip_purple"
|
||||||
|
app:cornerRadius="14dp"
|
||||||
|
app:icon="@drawable/ic_encargos"
|
||||||
|
app:iconGravity="textStart"
|
||||||
|
app:iconPadding="8dp"
|
||||||
|
app:iconSize="20dp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- OTP Step Layout for Custom Request -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/layoutCustomOtp"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="12dp"
|
android:layout_marginTop="12dp"
|
||||||
android:hint="Colorway o detalles adicionales"
|
android:orientation="vertical"
|
||||||
android:textColorHint="@color/text_muted"
|
android:visibility="gone">
|
||||||
app:boxBackgroundColor="@color/bg_input"
|
|
||||||
app:boxCornerRadiusBottomEnd="12dp"
|
|
||||||
app:boxCornerRadiusBottomStart="12dp"
|
|
||||||
app:boxCornerRadiusTopEnd="12dp"
|
|
||||||
app:boxCornerRadiusTopStart="12dp"
|
|
||||||
app:boxStrokeColor="@color/drip_fuchsia">
|
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
<TextView
|
||||||
android:id="@+id/etCustomNotes"
|
android:layout_width="wrap_content"
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:lines="2"
|
android:fontFamily="sans-serif-black"
|
||||||
|
android:text="🔐 Verificación de WhatsApp"
|
||||||
android:textColor="@color/text_primary"
|
android:textColor="@color/text_primary"
|
||||||
android:textSize="14sp" />
|
android:textSize="15sp" />
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
|
||||||
|
|
||||||
<!-- Submit to WhatsApp Button with Tapjacking Protection -->
|
<TextView
|
||||||
<com.google.android.material.button.MaterialButton
|
android:id="@+id/tvCustomOtpInstruction"
|
||||||
android:id="@+id/btnSubmitCustomOrder"
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
android:text="Ingresa el código de 6 dígitos que enviamos a tu WhatsApp para validar tu cuenta y registrar tu encargo:"
|
||||||
|
android:textColor="@color/text_secondary"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/etCustomOtpCode"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="54dp"
|
||||||
|
android:layout_marginTop="14dp"
|
||||||
|
android:background="@drawable/bg_search_input"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:gravity="center"
|
||||||
|
android:hint="• • • • • •"
|
||||||
|
android:inputType="number"
|
||||||
|
android:letterSpacing="0.3"
|
||||||
|
android:maxLength="6"
|
||||||
|
android:textColor="@color/text_primary"
|
||||||
|
android:textColorHint="@color/text_muted"
|
||||||
|
android:textSize="22sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/pbCustomLoading"
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:indeterminateTint="@color/drip_fuchsia"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btnVerifyCustomOtp"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
android:layout_marginTop="14dp"
|
||||||
|
android:filterTouchesWhenObscured="true"
|
||||||
|
android:text="Confirmar Código y Registrar Encargo"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
android:textColor="#FFFFFF"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
app:backgroundTint="@color/drip_purple"
|
||||||
|
app:cornerRadius="14dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvResendCustomOtp"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
android:text="¿No recibiste el código? Reenviar por WhatsApp"
|
||||||
|
android:textColor="@color/drip_purple_glow"
|
||||||
|
android:textSize="11sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Success Step Layout -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/layoutCustomSuccess"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="52dp"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="20dp"
|
android:layout_marginTop="16dp"
|
||||||
android:filterTouchesWhenObscured="true"
|
android:gravity="center_horizontal"
|
||||||
android:text="Consultar Encargo en WhatsApp"
|
android:orientation="vertical"
|
||||||
android:textAllCaps="false"
|
android:visibility="gone">
|
||||||
android:textColor="#FFFFFF"
|
|
||||||
android:textSize="15sp"
|
<ImageView
|
||||||
android:textStyle="bold"
|
android:layout_width="56dp"
|
||||||
app:backgroundTint="@color/accent_whatsapp"
|
android:layout_height="56dp"
|
||||||
app:cornerRadius="16dp"
|
android:src="@drawable/ic_sparkle"
|
||||||
app:icon="@drawable/ic_whatsapp"
|
app:tint="@color/drip_fuchsia" />
|
||||||
app:iconGravity="textStart"
|
|
||||||
app:iconPadding="8dp" />
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:fontFamily="sans-serif-black"
|
||||||
|
android:text="¡ENCARGO REGISTRADO CON ÉXITO!"
|
||||||
|
android:textColor="@color/text_primary"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvCustomSuccessMessage"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="Hemos validado tu número de WhatsApp y registrado tu solicitud en la plataforma.\n\nLa plataforma notificó al administrador Carlos (+57 316 745 5877) y te contactaremos por WhatsApp con la cotización exacta."
|
||||||
|
android:textColor="@color/text_secondary"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btnCustomOpenWhatsApp"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
android:layout_marginTop="18dp"
|
||||||
|
android:filterTouchesWhenObscured="true"
|
||||||
|
android:text="Abrir WhatsApp del Asesor Carlos"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
android:textColor="#FFFFFF"
|
||||||
|
android:textSize="13sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
app:backgroundTint="@color/accent_whatsapp"
|
||||||
|
app:cornerRadius="14dp"
|
||||||
|
app:icon="@drawable/ic_whatsapp"
|
||||||
|
app:iconGravity="textStart"
|
||||||
|
app:iconPadding="8dp"
|
||||||
|
app:iconSize="18dp" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btnFinishCustom"
|
||||||
|
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="46dp"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
android:text="Listo, Volver a la Tienda"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
android:textColor="@color/text_secondary"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
|||||||
@ -30,7 +30,7 @@
|
|||||||
|
|
||||||
<!-- Splash Theme -->
|
<!-- Splash Theme -->
|
||||||
<style name="Theme.DripZone.Splash" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
<style name="Theme.DripZone.Splash" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||||
<item name="android:windowBackground">@drawable/dripzone_splash_clean</item>
|
<item name="android:windowBackground">@color/bg_dark</item>
|
||||||
<item name="android:statusBarColor">@color/bg_dark</item>
|
<item name="android:statusBarColor">@color/bg_dark</item>
|
||||||
<item name="android:navigationBarColor">@color/bg_dark</item>
|
<item name="android:navigationBarColor">@color/bg_dark</item>
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { sendWhatsAppMessage } from '@/lib/evolution-api';
|
|||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
try {
|
try {
|
||||||
const { identifier } = await request.json();
|
const { identifier, name } = await request.json();
|
||||||
if (!identifier) {
|
if (!identifier) {
|
||||||
return NextResponse.json({ success: false, error: 'Ingresa tu número de celular o correo' }, { status: 400 });
|
return NextResponse.json({ success: false, error: 'Ingresa tu número de celular o correo' }, { status: 400 });
|
||||||
}
|
}
|
||||||
@ -15,14 +15,13 @@ export async function POST(request: Request) {
|
|||||||
const user = dataService.authenticate(cleanInput);
|
const user = dataService.authenticate(cleanInput);
|
||||||
|
|
||||||
let phoneToSend = cleanInput.replace(/\D/g, '');
|
let phoneToSend = cleanInput.replace(/\D/g, '');
|
||||||
let userName = 'Cliente';
|
let userName = name?.trim() || (user ? user.name : 'Cliente');
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
if (!user.isActive) {
|
if (!user.isActive) {
|
||||||
return NextResponse.json({ success: false, error: 'Esta cuenta se encuentra inactiva.' }, { status: 403 });
|
return NextResponse.json({ success: false, error: 'Esta cuenta se encuentra inactiva.' }, { status: 403 });
|
||||||
}
|
}
|
||||||
phoneToSend = user.phone.replace(/\D/g, '');
|
phoneToSend = user.phone.replace(/\D/g, '');
|
||||||
userName = user.name;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!phoneToSend || phoneToSend.length < 7) {
|
if (!phoneToSend || phoneToSend.length < 7) {
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { otpStore } from '@/lib/otp-store';
|
|||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
try {
|
try {
|
||||||
const { identifier, code } = await request.json();
|
const { identifier, code, name, city, address } = await request.json();
|
||||||
if (!identifier || !code) {
|
if (!identifier || !code) {
|
||||||
return NextResponse.json({ success: false, error: 'Identificador y código son requeridos' }, { status: 400 });
|
return NextResponse.json({ success: false, error: 'Identificador y código son requeridos' }, { status: 400 });
|
||||||
}
|
}
|
||||||
@ -16,18 +16,20 @@ export async function POST(request: Request) {
|
|||||||
return NextResponse.json({ success: false, error: verification.reason || 'Código inválido o expirado' }, { status: 401 });
|
return NextResponse.json({ success: false, error: verification.reason || 'Código inválido o expirado' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Código válido -> Obtener o crear sesión
|
// Código válido -> Obtener o crear usuario registrado en la plataforma
|
||||||
let user = dataService.authenticate(cleanInput);
|
let user = dataService.authenticate(cleanInput);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
// Si es un nuevo cliente autenticado por primera vez con su celular
|
// Si es un nuevo cliente autenticado por primera vez con su celular
|
||||||
const cleanPhone = cleanInput.replace(/\D/g, '');
|
const cleanPhone = cleanInput.replace(/\D/g, '');
|
||||||
|
const assignedName = name?.trim() || `Cliente Verificado (${cleanPhone.slice(-4)})`;
|
||||||
user = dataService.addUser({
|
user = dataService.addUser({
|
||||||
id: `usr-${Date.now()}`,
|
id: `usr-${Date.now()}`,
|
||||||
name: `Cliente Verificado (${cleanPhone.slice(-4)})`,
|
name: assignedName,
|
||||||
email: `${cleanPhone}@cliente.dripzone.co`,
|
email: `${cleanPhone}@cliente.dripzone.co`,
|
||||||
phone: cleanPhone,
|
phone: cleanPhone,
|
||||||
city: 'Cali',
|
city: city?.trim() || 'Cali',
|
||||||
|
address: address?.trim() || '',
|
||||||
role: 'customer',
|
role: 'customer',
|
||||||
isActive: true,
|
isActive: true,
|
||||||
phoneVerified: true,
|
phoneVerified: true,
|
||||||
@ -39,9 +41,12 @@ export async function POST(request: Request) {
|
|||||||
lastLogin: 'Justo ahora',
|
lastLogin: 'Justo ahora',
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Usuario existente: si verificó su teléfono, actualizar estado
|
// Usuario existente: si verificó su teléfono, actualizar estado y datos si se suministraron
|
||||||
const hadRejections = (user.rejectedOrdersCount || 0) > 0;
|
const hadRejections = (user.rejectedOrdersCount || 0) > 0;
|
||||||
dataService.updateUser(user.id, {
|
dataService.updateUser(user.id, {
|
||||||
|
name: name?.trim() || user.name,
|
||||||
|
city: city?.trim() || user.city,
|
||||||
|
address: address?.trim() || user.address,
|
||||||
phoneVerified: true,
|
phoneVerified: true,
|
||||||
requiresShippingDeposit: hadRejections ? true : false,
|
requiresShippingDeposit: hadRejections ? true : false,
|
||||||
depositReason: hadRejections
|
depositReason: hadRejections
|
||||||
|
|||||||
@ -140,6 +140,7 @@ ${customerEmail ? `✉️ *Correo:* ${customerEmail}\n` : ''}
|
|||||||
⚡ ¡Consulta de inmediato con tus proveedores mayoristas para enviar cotización al cliente!`;
|
⚡ ¡Consulta de inmediato con tus proveedores mayoristas para enviar cotización al cliente!`;
|
||||||
|
|
||||||
sendWhatsAppMessage(CARLOS_PHONE, adminMessage).catch(() => {});
|
sendWhatsAppMessage(CARLOS_PHONE, adminMessage).catch(() => {});
|
||||||
|
sendWhatsAppMessage('120363428012905103@g.us', adminMessage).catch(() => {});
|
||||||
|
|
||||||
// 2. Confirmación automática al cliente por WhatsApp
|
// 2. Confirmación automática al cliente por WhatsApp
|
||||||
const clientMessage = `👋 ¡Hola *${customerName}*!
|
const clientMessage = `👋 ¡Hola *${customerName}*!
|
||||||
|
|||||||
@ -5,7 +5,11 @@ import {
|
|||||||
updateOrderInAppwrite,
|
updateOrderInAppwrite,
|
||||||
deleteOrderInAppwrite,
|
deleteOrderInAppwrite,
|
||||||
} from '@/lib/server-appwrite';
|
} from '@/lib/server-appwrite';
|
||||||
import { sendWhatsAppMessage, buildOrderConfirmationMessage } from '@/lib/evolution-api';
|
import {
|
||||||
|
sendWhatsAppMessage,
|
||||||
|
buildOrderConfirmationMessage,
|
||||||
|
notifyAdminNewOrder,
|
||||||
|
} from '@/lib/evolution-api';
|
||||||
import { dataService } from '@/lib/data-service';
|
import { dataService } from '@/lib/data-service';
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
@ -24,13 +28,20 @@ export async function POST(request: Request) {
|
|||||||
const newOrder = await createOrderInAppwrite(body);
|
const newOrder = await createOrderInAppwrite(body);
|
||||||
dataService.addOrder(newOrder);
|
dataService.addOrder(newOrder);
|
||||||
|
|
||||||
// Enviar confirmación por WhatsApp en background si tiene teléfono válido
|
// 1. Notificación INMEDIATA al WhatsApp de Carlos (+57 316 745 5877)
|
||||||
|
try {
|
||||||
|
await notifyAdminNewOrder(newOrder);
|
||||||
|
} catch (adminWaErr) {
|
||||||
|
console.warn('No se pudo enviar WhatsApp al administrador Carlos:', adminWaErr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Enviar confirmación por WhatsApp al cliente en background si tiene teléfono válido
|
||||||
if (newOrder.customerPhone) {
|
if (newOrder.customerPhone) {
|
||||||
try {
|
try {
|
||||||
const msg = buildOrderConfirmationMessage(newOrder);
|
const msg = buildOrderConfirmationMessage(newOrder);
|
||||||
await sendWhatsAppMessage(newOrder.customerPhone, msg);
|
await sendWhatsAppMessage(newOrder.customerPhone, msg);
|
||||||
} catch (waErr) {
|
} catch (waErr) {
|
||||||
console.warn('No se pudo enviar WhatsApp automático:', waErr);
|
console.warn('No se pudo enviar WhatsApp automático al cliente:', waErr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -56,6 +56,10 @@ export default function CheckoutPage() {
|
|||||||
const [orderCompleted, setOrderCompleted] = useState<Order | null>(null);
|
const [orderCompleted, setOrderCompleted] = useState<Order | null>(null);
|
||||||
const [userRequiresDeposit, setUserRequiresDeposit] = useState<boolean>(false);
|
const [userRequiresDeposit, setUserRequiresDeposit] = useState<boolean>(false);
|
||||||
const [userDepositReason, setUserDepositReason] = useState<string>('');
|
const [userDepositReason, setUserDepositReason] = useState<string>('');
|
||||||
|
const [showOtpModal, setShowOtpModal] = useState<boolean>(false);
|
||||||
|
const [otpCode, setOtpCode] = useState<string>('');
|
||||||
|
const [otpLoading, setOtpLoading] = useState<boolean>(false);
|
||||||
|
const [otpError, setOtpError] = useState<string>('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch('/api/cities')
|
fetch('/api/cities')
|
||||||
@ -117,15 +121,8 @@ export default function CheckoutPage() {
|
|||||||
const officialWhatsapp = storeSettings?.whatsappNumber || '573167455877';
|
const officialWhatsapp = storeSettings?.whatsappNumber || '573167455877';
|
||||||
const boldLink = storeSettings?.boldPaymentLink || 'https://bold.co/p/carlos-lasso';
|
const boldLink = storeSettings?.boldPaymentLink || 'https://bold.co/p/carlos-lasso';
|
||||||
|
|
||||||
const handleSubmitOrder = async (e: React.FormEvent) => {
|
const processOrderSubmission = async () => {
|
||||||
e.preventDefault();
|
|
||||||
if (!customerName || !customerPhone || !address) {
|
|
||||||
alert('Por favor completa tu nombre, celular y dirección para el despacho.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const orderPayload = {
|
const orderPayload = {
|
||||||
customerName,
|
customerName,
|
||||||
@ -158,14 +155,90 @@ export default function CheckoutPage() {
|
|||||||
spread: 70,
|
spread: 70,
|
||||||
origin: { y: 0.6 },
|
origin: { y: 0.6 },
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
alert(data.error || 'Error al registrar pedido.');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error creando orden:', err);
|
console.error('Error creando orden:', err);
|
||||||
|
alert('Error de conexión al registrar pedido.');
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSubmitOrder = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!customerName || !customerPhone || !address) {
|
||||||
|
alert('Por favor completa tu nombre, celular y dirección para el despacho.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const cleanPhone = customerPhone.replace(/\D/g, '');
|
||||||
|
if (cleanPhone.length < 10) {
|
||||||
|
alert('Por favor ingresa un número de celular/WhatsApp válido a 10 dígitos.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user?.phoneVerified && user.phone === cleanPhone) {
|
||||||
|
await processOrderSubmission();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Solicitar OTP por WhatsApp
|
||||||
|
setOtpLoading(true);
|
||||||
|
setOtpError('');
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/auth/send-otp', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ identifier: cleanPhone, name: customerName }),
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.success) {
|
||||||
|
setShowOtpModal(true);
|
||||||
|
} else {
|
||||||
|
alert(data.error || 'No se pudo enviar el código OTP a WhatsApp.');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
alert('Error al solicitar código OTP por WhatsApp.');
|
||||||
|
} finally {
|
||||||
|
setOtpLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleVerifyOtpAndPlaceOrder = async () => {
|
||||||
|
if (otpCode.length < 6) {
|
||||||
|
setOtpError('Ingresa el código de 6 dígitos enviado a tu WhatsApp.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setOtpLoading(true);
|
||||||
|
setOtpError('');
|
||||||
|
try {
|
||||||
|
const cleanPhone = customerPhone.replace(/\D/g, '');
|
||||||
|
const res = await fetch('/api/auth/verify-otp', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
identifier: cleanPhone,
|
||||||
|
code: otpCode,
|
||||||
|
name: customerName,
|
||||||
|
city: activeCity.name,
|
||||||
|
address: `${address} - Barrio: ${neighborhood || 'Centro'}`,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.success) {
|
||||||
|
setShowOtpModal(false);
|
||||||
|
await processOrderSubmission();
|
||||||
|
} else {
|
||||||
|
setOtpError(data.error || 'Código incorrecto o expirado.');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setOtpError('Error verificando código.');
|
||||||
|
} finally {
|
||||||
|
setOtpLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleOpenWhatsAppOrder = (order: Order) => {
|
const handleOpenWhatsAppOrder = (order: Order) => {
|
||||||
const itemsSummary = order.items
|
const itemsSummary = order.items
|
||||||
.map((i) => `• ${i.productName} (Talla ${i.size}) x${i.quantity} = ${formatCOP(i.price * i.quantity)}`)
|
.map((i) => `• ${i.productName} (Talla ${i.size}) x${i.quantity} = ${formatCOP(i.price * i.quantity)}`)
|
||||||
@ -540,6 +613,49 @@ export default function CheckoutPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
{showOtpModal && (
|
||||||
|
<div className="fixed inset-0 z-50 bg-black/80 backdrop-blur-sm flex items-center justify-center p-4 animate-in fade-in">
|
||||||
|
<div className="bg-neutral-900 border border-purple-500/40 rounded-3xl max-w-md w-full p-6 text-white space-y-4 shadow-2xl">
|
||||||
|
<div className="flex justify-between items-center border-b border-neutral-800 pb-3">
|
||||||
|
<h3 className="text-base font-black flex items-center gap-2">
|
||||||
|
<ShieldCheck className="w-5 h-5 text-purple-400" />
|
||||||
|
Verificación de WhatsApp (OTP)
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowOtpModal(false)}
|
||||||
|
className="text-neutral-400 hover:text-white text-sm cursor-pointer p-1"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-neutral-300 leading-relaxed">
|
||||||
|
Para validar tu cuenta y registrar tu orden contraentrega en la plataforma, te enviamos un código de seguridad de 6 dígitos a tu WhatsApp <strong>+57 {customerPhone}</strong>:
|
||||||
|
</p>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
maxLength={6}
|
||||||
|
value={otpCode}
|
||||||
|
onChange={(e) => setOtpCode(e.target.value.replace(/\D/g, ''))}
|
||||||
|
placeholder="• • • • • •"
|
||||||
|
className="w-full bg-neutral-950 border border-neutral-700 focus:border-purple-500 rounded-xl px-4 py-3 text-center text-2xl font-mono tracking-widest text-white outline-none"
|
||||||
|
/>
|
||||||
|
{otpError && <p className="text-xs text-rose-400 text-center">{otpError}</p>}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={otpLoading || otpCode.length < 6}
|
||||||
|
onClick={handleVerifyOtpAndPlaceOrder}
|
||||||
|
className="w-full bg-gradient-to-r from-purple-600 to-fuchsia-600 text-white font-bold py-3.5 rounded-xl hover:opacity-95 disabled:opacity-50 text-sm shadow-md transition-all cursor-pointer"
|
||||||
|
>
|
||||||
|
{otpLoading ? 'Verificando...' : 'Confirmar Código y Registrar Pedido'}
|
||||||
|
</button>
|
||||||
|
<p className="text-[11px] text-neutral-500 text-center">
|
||||||
|
El código expira en 5 minutos.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -46,6 +46,10 @@ export const CustomRequestModal: React.FC<CustomRequestModalProps> = ({
|
|||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isSuccess, setIsSuccess] = useState(false);
|
const [isSuccess, setIsSuccess] = useState(false);
|
||||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||||
|
const [showOtp, setShowOtp] = useState(false);
|
||||||
|
const [otpCode, setOtpCode] = useState('');
|
||||||
|
const [otpLoading, setOtpLoading] = useState(false);
|
||||||
|
const [otpError, setOtpError] = useState('');
|
||||||
|
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
@ -68,20 +72,12 @@ export const CustomRequestModal: React.FC<CustomRequestModalProps> = ({
|
|||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const processCustomRequest = async () => {
|
||||||
e.preventDefault();
|
setIsLoading(true);
|
||||||
setErrorMessage(null);
|
setErrorMessage(null);
|
||||||
|
|
||||||
const effectiveBrand = brand === 'Otra' && customBrand.trim() ? customBrand.trim() : brand;
|
const effectiveBrand = brand === 'Otra' && customBrand.trim() ? customBrand.trim() : brand;
|
||||||
const effectiveSize = size === 'custom' && customSize.trim() ? customSize.trim() : size;
|
const effectiveSize = size === 'custom' && customSize.trim() ? customSize.trim() : size;
|
||||||
|
|
||||||
if (!customerName.trim() || !customerPhone.trim() || !modelName.trim()) {
|
|
||||||
setErrorMessage('Por favor completa tu nombre, WhatsApp y el modelo de las zapatillas.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsLoading(true);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/custom-requests', {
|
const res = await fetch('/api/custom-requests', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@ -113,6 +109,72 @@ export const CustomRequestModal: React.FC<CustomRequestModalProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setErrorMessage(null);
|
||||||
|
|
||||||
|
const cleanPhone = customerPhone.replace(/\D/g, '');
|
||||||
|
if (!customerName.trim() || cleanPhone.length < 10 || !modelName.trim()) {
|
||||||
|
setErrorMessage('Por favor completa tu nombre, número de WhatsApp a 10 dígitos y el modelo deseado.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Solicitar código OTP por WhatsApp
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/auth/send-otp', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ identifier: cleanPhone, name: customerName.trim() }),
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.success) {
|
||||||
|
setShowOtp(true);
|
||||||
|
} else {
|
||||||
|
setErrorMessage(data.error || 'No se pudo enviar el código OTP a WhatsApp.');
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
setErrorMessage('Error al solicitar código OTP por WhatsApp.');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleVerifyOtpAndSubmitRequest = async () => {
|
||||||
|
if (otpCode.length < 6) {
|
||||||
|
setOtpError('Ingresa el código de 6 dígitos que enviamos a tu WhatsApp.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setOtpLoading(true);
|
||||||
|
setOtpError('');
|
||||||
|
try {
|
||||||
|
const cleanPhone = customerPhone.replace(/\D/g, '');
|
||||||
|
const verifyRes = await fetch('/api/auth/verify-otp', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
identifier: cleanPhone,
|
||||||
|
code: otpCode,
|
||||||
|
name: customerName.trim(),
|
||||||
|
city: customerCity.trim() || 'Colombia',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const verifyData = await verifyRes.json();
|
||||||
|
if (!verifyData.success) {
|
||||||
|
setOtpError(verifyData.error || 'Código incorrecto o expirado.');
|
||||||
|
setOtpLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setShowOtp(false);
|
||||||
|
await processCustomRequest();
|
||||||
|
} catch (err) {
|
||||||
|
setOtpError('Error verificando código.');
|
||||||
|
} finally {
|
||||||
|
setOtpLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleResetAndClose = () => {
|
const handleResetAndClose = () => {
|
||||||
setIsSuccess(false);
|
setIsSuccess(false);
|
||||||
setImageBase64(null);
|
setImageBase64(null);
|
||||||
@ -426,6 +488,49 @@ export const CustomRequestModal: React.FC<CustomRequestModalProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{showOtp && (
|
||||||
|
<div className="fixed inset-0 z-60 bg-black/85 backdrop-blur-sm flex items-center justify-center p-4 animate-in fade-in">
|
||||||
|
<div className="bg-neutral-900 border border-purple-500/40 rounded-3xl max-w-md w-full p-6 text-white space-y-4 shadow-2xl">
|
||||||
|
<div className="flex justify-between items-center border-b border-neutral-800 pb-3">
|
||||||
|
<h3 className="text-base font-black flex items-center gap-2">
|
||||||
|
<ShieldCheck className="w-5 h-5 text-purple-400" />
|
||||||
|
Verificación de WhatsApp (OTP)
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowOtp(false)}
|
||||||
|
className="text-neutral-400 hover:text-white text-sm cursor-pointer p-1"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-neutral-300 leading-relaxed">
|
||||||
|
Para validar tu cuenta y registrar tu encargo en la plataforma, enviamos un código de 6 dígitos a tu WhatsApp <strong>+57 {customerPhone}</strong>:
|
||||||
|
</p>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
maxLength={6}
|
||||||
|
value={otpCode}
|
||||||
|
onChange={(e) => setOtpCode(e.target.value.replace(/\D/g, ''))}
|
||||||
|
placeholder="• • • • • •"
|
||||||
|
className="w-full bg-neutral-950 border border-neutral-700 focus:border-purple-500 rounded-xl px-4 py-3 text-center text-2xl font-mono tracking-widest text-white outline-none"
|
||||||
|
/>
|
||||||
|
{otpError && <p className="text-xs text-rose-400 text-center">{otpError}</p>}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={otpLoading || otpCode.length < 6}
|
||||||
|
onClick={handleVerifyOtpAndSubmitRequest}
|
||||||
|
className="w-full bg-gradient-to-r from-purple-600 to-fuchsia-600 text-white font-bold py-3.5 rounded-xl hover:opacity-95 disabled:opacity-50 text-sm shadow-md transition-all cursor-pointer"
|
||||||
|
>
|
||||||
|
{otpLoading ? 'Verificando...' : 'Confirmar Código y Registrar Encargo'}
|
||||||
|
</button>
|
||||||
|
<p className="text-[11px] text-neutral-500 text-center">
|
||||||
|
El código expira en 5 minutos.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -247,13 +247,14 @@ export const CartDrawer: React.FC<CartDrawerProps> = ({
|
|||||||
<ArrowRight className="w-4 h-4" />
|
<ArrowRight className="w-4 h-4" />
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<button
|
<Link
|
||||||
onClick={handleWhatsAppCheckout}
|
href="/checkout"
|
||||||
|
onClick={onClose}
|
||||||
className="w-full bg-emerald-600 hover:bg-emerald-700 text-white font-bold text-xs py-2.5 px-3 rounded-xl flex items-center justify-center space-x-1.5 transition-all cursor-pointer"
|
className="w-full bg-emerald-600 hover:bg-emerald-700 text-white font-bold text-xs py-2.5 px-3 rounded-xl flex items-center justify-center space-x-1.5 transition-all cursor-pointer"
|
||||||
>
|
>
|
||||||
<MessageCircle className="w-4 h-4 fill-white" />
|
<MessageCircle className="w-4 h-4 fill-white" />
|
||||||
<span>Completar Pedido por WhatsApp</span>
|
<span>Completar Pedido con Verificación WhatsApp</span>
|
||||||
</button>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -178,3 +178,71 @@ export function buildOrderConfirmationMessage(order: any): string {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifica inmediatamente al administrador Carlos (+57 316 745 5877)
|
||||||
|
* y al canal de notificaciones cuando entra un nuevo pedido en la plataforma (web o app móvil).
|
||||||
|
*/
|
||||||
|
export async function notifyAdminNewOrder(order: any): Promise<boolean> {
|
||||||
|
const CARLOS_PHONE = '573167455877';
|
||||||
|
const NOTIFICATION_GROUP = '120363428012905103@g.us';
|
||||||
|
|
||||||
|
const itemsSummary = (order.items || [])
|
||||||
|
.map((item: any) => `• 👟 *${item.productName || item.name}*\n 📏 Talla: *${item.size || 'Única'}* | Cantidad: *${item.quantity || 1}* | Valor: *${formatCOP((item.price || item.retailPrice || 0) * (item.quantity || 1))}*`)
|
||||||
|
.join('\n\n');
|
||||||
|
|
||||||
|
const paymentCondition = (order.depositAmount && order.depositAmount > 0)
|
||||||
|
? `⚡ Separa con *${formatCOP(order.depositAmount)}* • Saldo Contraentrega: *${formatCOP(order.balanceDue || (order.totalAmount - order.depositAmount))}*`
|
||||||
|
: `🤝 100% Pago Contraentrega (*${formatCOP(order.totalAmount || 0)}*)`;
|
||||||
|
|
||||||
|
const orderId = order.id || `P-${Date.now().toString().slice(-6)}`;
|
||||||
|
const dateStr = new Date().toLocaleString('es-CO', { timeZone: 'America/Bogota' });
|
||||||
|
|
||||||
|
const adminMessage = (
|
||||||
|
`🚨 *¡NUEVO PEDIDO REGISTRADO EN DRIPZONE!* 🛍️📦\n` +
|
||||||
|
`━━━━━━━━━━━━━━━━━━━━━━\n` +
|
||||||
|
`🆔 *Orden #:* ${orderId}\n` +
|
||||||
|
`📅 *Fecha:* ${dateStr}\n\n` +
|
||||||
|
`👤 *DATOS DEL CLIENTE:*\n` +
|
||||||
|
`• *Nombre:* ${order.customerName || 'Cliente DripZone'}\n` +
|
||||||
|
`• *WhatsApp:* ${order.customerPhone || 'Sin registrar'}\n` +
|
||||||
|
`• *Ciudad Destino:* ${order.cityName || 'Colombia'}\n` +
|
||||||
|
`• *Dirección:* ${order.address || 'Por coordinar'}\n` +
|
||||||
|
(order.notes ? `• *Notas:* ${order.notes}\n` : '') +
|
||||||
|
`\n🛒 *PRODUCTOS SOLICITADOS:*\n` +
|
||||||
|
`${itemsSummary || '1x Zapatillas DripZone'}\n\n` +
|
||||||
|
`💰 *LIQUIDACIÓN ECONÓMICA:*\n` +
|
||||||
|
`• *Subtotal:* ${formatCOP(order.subtotal || order.totalAmount || 0)}\n` +
|
||||||
|
`• *Envío:* ${order.shippingCost === 0 ? 'GRATIS' : formatCOP(order.shippingCost || 0)}\n` +
|
||||||
|
`• *TOTAL PEDIDO:* *${formatCOP(order.totalAmount || 0)}*\n` +
|
||||||
|
`• *Condición:* ${paymentCondition}\n` +
|
||||||
|
`• *Saldo a Cobrar Contraentrega:* *${formatCOP(order.balanceDue || order.totalAmount || 0)}*\n` +
|
||||||
|
`━━━━━━━━━━━━━━━━━━━━━━\n` +
|
||||||
|
`⚡ *Gestionar en el Panel de Administración:*\n` +
|
||||||
|
`👉 https://dripzone.almeira.com.co/admin/pedidos`
|
||||||
|
);
|
||||||
|
|
||||||
|
let success = false;
|
||||||
|
try {
|
||||||
|
// 1. Notificación principal al WhatsApp personal de Carlos
|
||||||
|
const resCarlos = await sendWhatsAppMessage(CARLOS_PHONE, adminMessage);
|
||||||
|
if (resCarlos) success = true;
|
||||||
|
|
||||||
|
// 2. Si el producto principal tiene imagen web, enviar foto como soporte
|
||||||
|
const firstImage = order.items?.[0]?.image || (Array.isArray(order.items?.[0]?.images) ? order.items[0].images[0] : null);
|
||||||
|
if (firstImage && (firstImage.startsWith('http://') || firstImage.startsWith('https://'))) {
|
||||||
|
await sendWhatsAppMedia(
|
||||||
|
CARLOS_PHONE,
|
||||||
|
firstImage,
|
||||||
|
`👟 Foto de referencia: ${order.items[0]?.productName || order.items[0]?.name || 'Producto DripZone'}`
|
||||||
|
).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Notificación al canal de alertas
|
||||||
|
await sendWhatsAppMessage(NOTIFICATION_GROUP, adminMessage).catch(() => {});
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('Error enviando notificación de pedido a Carlos:', err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
10
scripts/check_vps.py
Normal file
10
scripts/check_vps.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import paramiko
|
||||||
|
|
||||||
|
client = paramiko.SSHClient()
|
||||||
|
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
client.connect('161.97.93.156', username='carlos', password='Innova1978+*?', timeout=20)
|
||||||
|
stdin, stdout, stderr = client.exec_command('docker inspect dripzone-production --format "{{.Config.WorkingDir}} - {{.Config.Cmd}}"')
|
||||||
|
print("dripzone-production cmd:", stdout.read().decode('utf-8'))
|
||||||
|
stdin, stdout, stderr = client.exec_command('docker inspect bfmnoga3lb0ckomhdmxe0rco-021053217798 --format "{{.Config.WorkingDir}} - {{.Config.Cmd}}"')
|
||||||
|
print("coolify dripzone cmd:", stdout.read().decode('utf-8'))
|
||||||
|
client.close()
|
||||||
@ -64,28 +64,23 @@ def send_apk_whatsapp():
|
|||||||
print("[3/3] Enviando APK y notificación oficial al WhatsApp de Carlos (+57 316 745 5877)...")
|
print("[3/3] Enviando APK y notificación oficial al WhatsApp de Carlos (+57 316 745 5877)...")
|
||||||
|
|
||||||
message_text = (
|
message_text = (
|
||||||
"📲 *DripZone - App Nativa Android v1.2 (100% Funcionalidad Web)*\n\n"
|
"📲 *DripZone - App Nativa Android v1.3 (OTP WhatsApp + Registro Plataforma)*\n\n"
|
||||||
"🔥 *¡Actualización Desplegada! Toda la funcionalidad y estética de la Web ahora en la App Nativa:*\n\n"
|
"🔥 *¡Actualización Desplegada! Corrección de Splash Screen y Sistema de Validación OTP:*\n\n"
|
||||||
"🎨 *1. Estilo Visual, Splash Screen y Paleta Web Idénticos:*\n"
|
"✨ *1. Corrección Total de Splash Screen:*\n"
|
||||||
"• *Splash Screen:* Integrado con el recurso oficial solicitado `dripzone_splash_clean_1787718656035` con transición fluida.\n"
|
"• Solucionado el desbordamiento de pantalla: ahora se ajusta perfectamente con escalado proporcional (`fitCenter` / `adjustViewBounds`) y fondo `#0A0A0A` sin distorsión ni recortes en ningún modelo de celular.\n\n"
|
||||||
"• *Paleta Dark Streetwear DripZone:* Fondo `#0A0A0A`, detalles en púrpura `#9333EA` / `#A855F7`, fucsia neón `#D946EF`, rosa `#EC4899`, badges violeta `#2E1065` y glow borders.\n"
|
"🔐 *2. Registro de Usuario y Validación de WhatsApp con OTP:*\n"
|
||||||
"• *Banner Web Superior:* Degradado en vivo 'ENVÍOS CONTRAENTREGA EN TODO COLOMBIA'.\n\n"
|
"• Al solicitar cualquier pedido o encargo, el usuario registra su nombre, celular y dirección.\n"
|
||||||
"⚡ *2. Misma Funcionalidad que la Versión Web:*\n"
|
"• La plataforma envía un código de seguridad de 6 dígitos al WhatsApp del cliente vía Evolution API.\n"
|
||||||
"• *Selector de Ciudad:* Modal interactivo con ciudades colombianas (Cali, Bogotá, Medellín, Barranquilla, Bucaramanga, etc.), tiempos de entrega y garantía de 1er cambio gratis.\n"
|
"• El cliente valida su número introduciendo el código OTP en la app o web, quedando registrado y verificado en la plataforma.\n\n"
|
||||||
"• *Carrito de Compras Multi-Producto:* Drawer interactivo (`CartBottomSheet`), sumatoria automática, desglose de separación ('Separa con $10.000 COP y paga saldo al recibir') y checkout multi-item a WhatsApp.\n"
|
"📦 *3. Registro en Plataforma y Notificación Automática a Carlos:*\n"
|
||||||
"• *Detalle de Producto:* Selector interactivo de tallas en chips, botón 'Agregar al Carrito' y 'Comprar Directo por WhatsApp' con foto y formato de orden contraentrega.\n"
|
"• Los pedidos (`/api/orders`) y encargos (`/api/custom-requests`) se guardan directamente en la base de datos de la plataforma.\n"
|
||||||
"• *Dock de Navegación Móvil (5 Pestañas):* Inicio, Buscar, Carrito con badge dinámico en tiempo real, Encargos y Asesor WhatsApp.\n"
|
"• El servidor de la plataforma es el encargado de enviarte la información completa de inmediato a tu WhatsApp (+57 316 745 5877) con datos del cliente, ciudad, productos, tallas, liquidación contraentrega y foto de soporte.\n"
|
||||||
"• *Módulo de Encargos / Pedidos Especiales:* Formulario para cotizar modelos no listados directamente con proveedores mayoristas.\n"
|
"• El cliente recibe su confirmación oficial por WhatsApp.\n\n"
|
||||||
"• *Filtros y Búsqueda en Vivo:* Categorías dinámicas (Sneakers, Chanclas, Bolsos, Gorras) y filtrado instantáneo por texto.\n"
|
"🌐 *4. Misma Funcionalidad en Web y App Móvil:*\n"
|
||||||
"• *Pull-to-Refresh:* Sincronización instantánea con los drops de WhatsApp.\n\n"
|
"• Tanto la tienda web (`checkout/page.tsx`, `CustomRequestModal.tsx`) como la app nativa en Kotlin comparten exactamente los mismos endpoints, reglas de verificación OTP y plantillas de Evolution API.\n\n"
|
||||||
"🛡️ *3. Seguridad de Nivel Corporativo:*\n"
|
f"⬇️ *Descarga directa del APK actualizado:* {APK_URL}\n"
|
||||||
"• *Network Security Config:* Tráfico en texto claro (HTTP) bloqueado, TLS 1.2+ estricto.\n"
|
|
||||||
"• *Cifrado de Datos:* `EncryptedSharedPreferences` respaldado por Android KeyStore (AES-256 GCM).\n"
|
|
||||||
"• *Protección Anti-Tapjacking:* `filterTouchesWhenObscured=\"true\"` en botones clave.\n"
|
|
||||||
"• *Lanzamiento Seguro de Intents:* Validación rigurosa de URLs y esquemas de WhatsApp.\n\n"
|
|
||||||
f"⬇️ *Descarga directa del APK:* {APK_URL}\n"
|
|
||||||
f"⬇️ *Enlace alternativo:* https://dripzone.almeira.com.co/DripZone.apk\n\n"
|
f"⬇️ *Enlace alternativo:* https://dripzone.almeira.com.co/DripZone.apk\n\n"
|
||||||
"Adjunto el instalador APK para su instalación y prueba inmediata."
|
"Adjunto el archivo APK instalador actualizado para prueba inmediata en tu dispositivo."
|
||||||
)
|
)
|
||||||
|
|
||||||
req_text = urllib.request.Request(
|
req_text = urllib.request.Request(
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user