feat: mobile notifications based on registered templates, AppNotificationManager, and deposit confirmed template
This commit is contained in:
parent
0f74821c94
commit
acbe8335fd
@ -105,5 +105,59 @@ def deploy():
|
||||
print(f"[EXIT STATUS]: {status}")
|
||||
client.close()
|
||||
|
||||
if status == 0:
|
||||
send_apk_to_whatsapp()
|
||||
|
||||
def send_apk_to_whatsapp():
|
||||
import urllib.request
|
||||
import json
|
||||
print("[5/5] Enviando APK y reporte de actualizacion al WhatsApp de Carlos (+57 316 745 5877)...")
|
||||
url = "https://evolution.almeira.com.co/message/sendMedia/DripZone"
|
||||
headers = {
|
||||
"apikey": "83B5BD230796-4324-AE73-3508F69E279D",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
caption = (
|
||||
"📲 *¡DRIPZONE APK ACTUALIZADA!* 🚀👟\n"
|
||||
"━━━━━━━━━━━━━━━━━━━━━━\n"
|
||||
"✨ *Novedades de esta versión:*\n"
|
||||
"• 📦 *Mis Pedidos*: Visualización de estado en tiempo real, desglose de calzado, flete, anticipo $10.000 y saldo contraentrega.\n"
|
||||
"• 👤 *Mi Cuenta*: Acceso seguro vía WhatsApp OTP exclusivo a la línea del cliente.\n"
|
||||
"• 💵 *Precios en Grid*: Corrección de visualización de precios en 1 sola línea sin recortes y botón 'Pedir' optimizado.\n"
|
||||
"• 🗄️ *Persistencia Total*: Configuración, parámetros y entidad almacenados de forma segura en Base de Datos.\n"
|
||||
"• 🔔 *Plantillas Dinámicas*: Notificaciones móviles y web emitidas con las plantillas registradas.\n\n"
|
||||
"🌐 *Descarga directa:*\n👉 https://drip-zone.online/DripZone.apk"
|
||||
)
|
||||
payload = {
|
||||
"number": "573167455877",
|
||||
"mediatype": "document",
|
||||
"mimetype": "application/vnd.android.package-archive",
|
||||
"caption": caption,
|
||||
"media": "https://drip-zone.online/DripZone.apk",
|
||||
"fileName": "DripZone.apk"
|
||||
}
|
||||
try:
|
||||
req = urllib.request.Request(url, data=json.dumps(payload).encode('utf-8'), headers=headers)
|
||||
with urllib.request.urlopen(req, timeout=35) as resp:
|
||||
print(f"[OK] APK despachada con exito al WhatsApp de Carlos (HTTP {resp.status})")
|
||||
except Exception as e:
|
||||
print(f"[Aviso] No se pudo enviar archivo multimedia por Evolution: {e}. Enviando mensaje de texto...")
|
||||
text_url = "https://evolution.almeira.com.co/message/sendText/DripZone"
|
||||
text_payload = {
|
||||
"number": "573167455877",
|
||||
"text": (
|
||||
"📲 *¡DRIPZONE APK ACTUALIZADA EN PRODUCCIÓN!* 🚀👟\n\n"
|
||||
f"{caption}\n\n"
|
||||
"📥 *Descarga e instala el APK actualizado aquí:*\n"
|
||||
"👉 https://drip-zone.online/DripZone.apk"
|
||||
)
|
||||
}
|
||||
try:
|
||||
req2 = urllib.request.Request(text_url, data=json.dumps(text_payload).encode('utf-8'), headers=headers)
|
||||
with urllib.request.urlopen(req2, timeout=20) as resp2:
|
||||
print(f"[OK] Notificacion de texto enviada por WhatsApp (HTTP {resp2.status})")
|
||||
except Exception as e2:
|
||||
print(f"[Error] No se pudo enviar texto por WhatsApp: {e2}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
deploy()
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
<!-- Permissions: Least privilege model -->
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<!-- Queries for secure intent resolution (Android 11+ package visibility) -->
|
||||
<queries>
|
||||
|
||||
@ -12,15 +12,19 @@ import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import co.dripzone.app.api.ApiClient
|
||||
import co.dripzone.app.databinding.ActivityMainBinding
|
||||
import co.dripzone.app.manager.TemplateManager
|
||||
import co.dripzone.app.model.CartManager
|
||||
import co.dripzone.app.model.Category
|
||||
import co.dripzone.app.model.City
|
||||
import co.dripzone.app.model.Product
|
||||
import co.dripzone.app.model.UserManager
|
||||
import co.dripzone.app.security.SecurityManager
|
||||
import co.dripzone.app.ui.AccountBottomSheet
|
||||
import co.dripzone.app.ui.CartBottomSheet
|
||||
import co.dripzone.app.ui.CategoryAdapter
|
||||
import co.dripzone.app.ui.CitySelectorBottomSheet
|
||||
import co.dripzone.app.ui.CustomOrderBottomSheet
|
||||
import co.dripzone.app.ui.OrdersBottomSheet
|
||||
import co.dripzone.app.ui.ProductAdapter
|
||||
import co.dripzone.app.ui.ProductDetailBottomSheet
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
@ -58,10 +62,17 @@ class MainActivity : AppCompatActivity() {
|
||||
setupCitySelector()
|
||||
setupBottomDock()
|
||||
setupHeaderListeners()
|
||||
updateAccountButton()
|
||||
|
||||
co.dripzone.app.manager.AppNotificationManager.createNotificationChannel(this)
|
||||
TemplateManager.syncTemplates(this)
|
||||
updateCartBadges()
|
||||
loadProducts(forceRefresh = false)
|
||||
loadCities()
|
||||
|
||||
if (intent?.getStringExtra("OPEN_TAB") == "orders") {
|
||||
openOrders()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
@ -223,10 +234,10 @@ class MainActivity : AppCompatActivity() {
|
||||
openCart()
|
||||
}
|
||||
|
||||
// 4. Encargos
|
||||
binding.navBtnEncargos.setOnClickListener {
|
||||
CustomOrderBottomSheet.newInstance()
|
||||
.show(supportFragmentManager, CustomOrderBottomSheet.TAG)
|
||||
// 4. Mis Pedidos (Historial y Seguimiento)
|
||||
binding.navBtnOrders.setOnClickListener {
|
||||
setDockActive("orders")
|
||||
openOrders()
|
||||
}
|
||||
|
||||
// 5. WhatsApp Asesor
|
||||
@ -244,6 +255,9 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
binding.ivNavSearch.setColorFilter(if (tab == "search") fuchsia else muted)
|
||||
binding.tvNavSearch.setTextColor(if (tab == "search") fuchsia else muted)
|
||||
|
||||
binding.ivNavOrders.setColorFilter(if (tab == "orders") fuchsia else muted)
|
||||
binding.tvNavOrders.setTextColor(if (tab == "orders") fuchsia else muted)
|
||||
}
|
||||
|
||||
private fun setupHeaderListeners() {
|
||||
@ -252,9 +266,9 @@ class MainActivity : AppCompatActivity() {
|
||||
openCart()
|
||||
}
|
||||
|
||||
// Header WhatsApp button
|
||||
binding.btnHeaderWhatsApp.setOnClickListener {
|
||||
openGeneralWhatsApp()
|
||||
// Header Account Button (Mi Cuenta / Login)
|
||||
binding.btnHeaderAccount.setOnClickListener {
|
||||
openAccount()
|
||||
}
|
||||
|
||||
// SwipeRefresh: pulls latest live drops from suppliers
|
||||
@ -265,6 +279,28 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun openOrders() {
|
||||
OrdersBottomSheet().show(supportFragmentManager, OrdersBottomSheet.TAG)
|
||||
}
|
||||
|
||||
private fun openAccount() {
|
||||
AccountBottomSheet().apply {
|
||||
onUserChangedListener = {
|
||||
updateAccountButton()
|
||||
}
|
||||
}.show(supportFragmentManager, AccountBottomSheet.TAG)
|
||||
}
|
||||
|
||||
private fun updateAccountButton() {
|
||||
val user = UserManager.getUser(this)
|
||||
if (user.isPhoneVerified && user.name.isNotBlank()) {
|
||||
val firstName = user.name.split(" ").firstOrNull() ?: user.name
|
||||
binding.btnHeaderAccount.text = firstName
|
||||
} else {
|
||||
binding.btnHeaderAccount.text = "Mi Cuenta"
|
||||
}
|
||||
}
|
||||
|
||||
private fun openCart() {
|
||||
CartBottomSheet.newInstance(selectedCity) {
|
||||
openCitySelector()
|
||||
@ -272,7 +308,23 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun openGeneralWhatsApp() {
|
||||
val inquiry = "¡Hola DripZone! 🔥 Me comunico desde la app móvil. Quiero información sobre los productos disponibles para entrega contraentrega en ${selectedCity.name}."
|
||||
val variables = mapOf(
|
||||
"tienda_nombre" to "DripZone",
|
||||
"ciudad" to selectedCity.name
|
||||
)
|
||||
val inquiry = TemplateManager.renderTemplate(
|
||||
this,
|
||||
"general_store_inquiry",
|
||||
variables
|
||||
).ifBlank {
|
||||
TemplateManager.renderTemplate(
|
||||
this,
|
||||
"advisor_contact",
|
||||
variables
|
||||
)
|
||||
}.ifBlank {
|
||||
"¡Hola DripZone! 🔥 Me comunico desde la app móvil. Quiero información sobre los productos disponibles para entrega contraentrega en ${selectedCity.name}."
|
||||
}
|
||||
SecurityManager.openWhatsApp(
|
||||
this,
|
||||
SecurityManager.WHATSAPP_STORE_PHONE,
|
||||
|
||||
@ -423,5 +423,70 @@ object ApiClient {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene el listado de pedidos asociados a un número de celular de cliente
|
||||
*/
|
||||
suspend fun getOrdersByPhone(phone: String): Result<List<co.dripzone.app.model.Order>> =
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val clean = phone.replace("\\D".toRegex(), "")
|
||||
val request = Request.Builder()
|
||||
.url("${getBaseUrl()}/api/orders?phone=$clean")
|
||||
.get()
|
||||
.header("Accept", "application/json")
|
||||
.build()
|
||||
|
||||
val response = client.newCall(request).execute()
|
||||
if (response.isSuccessful) {
|
||||
val body = response.body?.string() ?: ""
|
||||
val json = gson.fromJson(body, com.google.gson.JsonObject::class.java)
|
||||
if (json.get("success")?.asBoolean == true && json.has("orders")) {
|
||||
val listType = object : TypeToken<List<co.dripzone.app.model.Order>>() {}.type
|
||||
val orders: List<co.dripzone.app.model.Order> = gson.fromJson(json.get("orders"), listType) ?: emptyList()
|
||||
return@withContext Result.success(orders)
|
||||
}
|
||||
}
|
||||
Result.success(emptyList())
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error fetching orders by phone: ${e.message}")
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene las plantillas de mensajes registradas en la base de datos de la plataforma
|
||||
*/
|
||||
suspend fun getMessageTemplates(): Result<Map<String, String>> =
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val request = Request.Builder()
|
||||
.url("${getBaseUrl()}/api/message-templates")
|
||||
.get()
|
||||
.header("Accept", "application/json")
|
||||
.build()
|
||||
|
||||
val response = client.newCall(request).execute()
|
||||
if (response.isSuccessful) {
|
||||
val body = response.body?.string() ?: ""
|
||||
val json = gson.fromJson(body, com.google.gson.JsonObject::class.java)
|
||||
if (json.get("success")?.asBoolean == true && json.has("templates")) {
|
||||
val templatesArray = json.getAsJsonArray("templates")
|
||||
val map = mutableMapOf<String, String>()
|
||||
for (element in templatesArray) {
|
||||
val obj = element.asJsonObject
|
||||
val id = obj.get("id")?.asString ?: continue
|
||||
val content = obj.get("content")?.asString ?: ""
|
||||
map[id] = content
|
||||
}
|
||||
return@withContext Result.success(map)
|
||||
}
|
||||
}
|
||||
Result.success(emptyMap())
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error fetching templates: ${e.message}")
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,226 @@
|
||||
package co.dripzone.app.manager
|
||||
|
||||
import android.Manifest
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import co.dripzone.app.MainActivity
|
||||
import co.dripzone.app.R
|
||||
|
||||
/**
|
||||
* Gestor de notificaciones de la aplicación móvil DripZone.
|
||||
* Despacha notificaciones del sistema Android en base a las plantillas registradas
|
||||
* en la base de datos de la plataforma y TemplateManager.
|
||||
*/
|
||||
object AppNotificationManager {
|
||||
|
||||
private const val TAG = "AppNotificationManager"
|
||||
const val CHANNEL_ID = "dripzone_notifications_channel"
|
||||
private const val CHANNEL_NAME = "Notificaciones de Pedidos DripZone"
|
||||
private const val CHANNEL_DESC = "Avisos de estado de compras, anticipos Bold y seguimiento de pedidos"
|
||||
|
||||
private const val NOTIF_ID_ORDER = 1001
|
||||
private const val NOTIF_ID_DEPOSIT = 1002
|
||||
private const val NOTIF_ID_CUSTOM = 1003
|
||||
|
||||
/**
|
||||
* Inicializa el canal de notificaciones en Android 8.0+ (API 26+)
|
||||
*/
|
||||
fun createNotificationChannel(context: Context) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val importance = NotificationManager.IMPORTANCE_HIGH
|
||||
val channel = NotificationChannel(CHANNEL_ID, CHANNEL_NAME, importance).apply {
|
||||
description = CHANNEL_DESC
|
||||
enableVibration(true)
|
||||
enableLights(true)
|
||||
}
|
||||
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager
|
||||
notificationManager?.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Muestra una notificación del sistema cuando un pedido se registra satisfactoriamente,
|
||||
* formateada con la plantilla 'order_confirmation_customer'.
|
||||
*/
|
||||
fun showOrderPlacedNotification(
|
||||
context: Context,
|
||||
orderId: String,
|
||||
variables: Map<String, String>
|
||||
) {
|
||||
try {
|
||||
createNotificationChannel(context)
|
||||
|
||||
if (!hasNotificationPermission(context)) {
|
||||
Log.d(TAG, "Permiso de notificaciones no concedido, omitiendo notificación local")
|
||||
return
|
||||
}
|
||||
|
||||
val renderedBody = TemplateManager.renderTemplate(context, "order_confirmation_customer", variables)
|
||||
val cleanBody = TemplateManager.stripMarkdown(renderedBody)
|
||||
|
||||
val shortOrderId = if (orderId.length > 8) orderId.takeLast(8).uppercase() else orderId.uppercase()
|
||||
val title = "🎉 ¡Pedido Registrado con Éxito! #$shortOrderId"
|
||||
|
||||
val intent = Intent(context, MainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
putExtra("OPEN_TAB", "orders")
|
||||
}
|
||||
|
||||
val pendingIntentFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
} else {
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
}
|
||||
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
context,
|
||||
NOTIF_ID_ORDER,
|
||||
intent,
|
||||
pendingIntentFlags
|
||||
)
|
||||
|
||||
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_package)
|
||||
.setContentTitle(title)
|
||||
.setContentText("Tu compra fue confirmada y está en preparación. Toca para ver el detalle.")
|
||||
.setStyle(NotificationCompat.BigTextStyle().bigText(cleanBody))
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setColor(ContextCompat.getColor(context, R.color.drip_purple))
|
||||
.setContentIntent(pendingIntent)
|
||||
.setAutoCancel(true)
|
||||
.build()
|
||||
|
||||
NotificationManagerCompat.from(context).notify(NOTIF_ID_ORDER, notification)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error enviando notificación de pedido: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Muestra una notificación cuando el anticipo de separación es confirmado por Bold,
|
||||
* formateada con la plantilla 'customer_deposit_confirmed'.
|
||||
*/
|
||||
fun showDepositConfirmedNotification(
|
||||
context: Context,
|
||||
orderId: String,
|
||||
variables: Map<String, String>
|
||||
) {
|
||||
try {
|
||||
createNotificationChannel(context)
|
||||
|
||||
if (!hasNotificationPermission(context)) return
|
||||
|
||||
val renderedBody = TemplateManager.renderTemplate(context, "customer_deposit_confirmed", variables)
|
||||
val cleanBody = TemplateManager.stripMarkdown(renderedBody)
|
||||
|
||||
val shortOrderId = if (orderId.length > 8) orderId.takeLast(8).uppercase() else orderId.uppercase()
|
||||
val title = "⚡ ¡Anticipo Confirmado por Bold! #$shortOrderId"
|
||||
|
||||
val intent = Intent(context, MainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
putExtra("OPEN_TAB", "orders")
|
||||
}
|
||||
|
||||
val pendingIntentFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
} else {
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
}
|
||||
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
context,
|
||||
NOTIF_ID_DEPOSIT,
|
||||
intent,
|
||||
pendingIntentFlags
|
||||
)
|
||||
|
||||
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_sparkle)
|
||||
.setContentTitle(title)
|
||||
.setContentText("Recibimos el pago de tu anticipo. Despacho autorizado.")
|
||||
.setStyle(NotificationCompat.BigTextStyle().bigText(cleanBody))
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setColor(ContextCompat.getColor(context, R.color.accent_emerald))
|
||||
.setContentIntent(pendingIntent)
|
||||
.setAutoCancel(true)
|
||||
.build()
|
||||
|
||||
NotificationManagerCompat.from(context).notify(NOTIF_ID_DEPOSIT, notification)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error enviando notificación de anticipo: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Muestra una notificación cuando una solicitud por encargo es recibida,
|
||||
* formateada con la plantilla 'customer_request_received'.
|
||||
*/
|
||||
fun showCustomRequestNotification(
|
||||
context: Context,
|
||||
variables: Map<String, String>
|
||||
) {
|
||||
try {
|
||||
createNotificationChannel(context)
|
||||
|
||||
if (!hasNotificationPermission(context)) return
|
||||
|
||||
val renderedBody = TemplateManager.renderTemplate(context, "customer_request_received", variables)
|
||||
val cleanBody = TemplateManager.stripMarkdown(renderedBody)
|
||||
|
||||
val model = variables["modelo"] ?: "Zapatillas"
|
||||
val title = "👟 Encargo Recibido: $model"
|
||||
|
||||
val intent = Intent(context, MainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
}
|
||||
|
||||
val pendingIntentFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
} else {
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
}
|
||||
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
context,
|
||||
NOTIF_ID_CUSTOM,
|
||||
intent,
|
||||
pendingIntentFlags
|
||||
)
|
||||
|
||||
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_encargos)
|
||||
.setContentTitle(title)
|
||||
.setContentText("Estamos cotizando disponibilidad con bodegas mayoristas.")
|
||||
.setStyle(NotificationCompat.BigTextStyle().bigText(cleanBody))
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
.setColor(ContextCompat.getColor(context, R.color.drip_fuchsia))
|
||||
.setContentIntent(pendingIntent)
|
||||
.setAutoCancel(true)
|
||||
.build()
|
||||
|
||||
NotificationManagerCompat.from(context).notify(NOTIF_ID_CUSTOM, notification)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error enviando notificación de encargo: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasNotificationPermission(context: Context): Boolean {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
ContextCompat.checkSelfPermission(
|
||||
context,
|
||||
Manifest.permission.POST_NOTIFICATIONS
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,196 @@
|
||||
package co.dripzone.app.manager
|
||||
|
||||
import android.content.Context
|
||||
import co.dripzone.app.api.ApiClient
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Gestor dinámico de plantillas de notificaciones y mensajes de la tienda.
|
||||
* Sincroniza con las plantillas registradas en la base de datos de la plataforma y
|
||||
* permite renderizar mensajes con variables dinámicas de forma segura.
|
||||
*/
|
||||
object TemplateManager {
|
||||
|
||||
private const val PREFS_NAME = "dripzone_templates_store"
|
||||
private const val PREFIX_TEMPLATE = "tpl_"
|
||||
|
||||
private val DEFAULT_TEMPLATES = mapOf(
|
||||
"order_confirmation_customer" to (
|
||||
"🎉 *¡GRACIAS POR TU PEDIDO EN {tienda_nombre}!* 🎉\n" +
|
||||
"📌 *No. de Orden:* #{orden_id}\n\n" +
|
||||
"👤 *Cliente:* {cliente_nombre}\n" +
|
||||
"📱 *Teléfono:* {cliente_telefono}\n" +
|
||||
"📍 *Dirección:* {cliente_direccion}\n" +
|
||||
"🏙️ *Ciudad:* {cliente_ciudad}\n\n" +
|
||||
"📦 *DETALLE DE TU COMPRA:*\n{articulos_resumen}\n\n" +
|
||||
"💵 *Subtotal:* {subtotal}\n" +
|
||||
"🚚 *Envío:* {costo_envio}\n" +
|
||||
"💰 *VALOR TOTAL:* {total}\n" +
|
||||
"{condicion_pago_resumen}\n\n" +
|
||||
"💳 *Método de Pago:* {metodo_pago}\n\n" +
|
||||
"Estamos alistando tu paquete. Te enviaremos el número de guía o notificación de entrega. ¡Muchas gracias por tu confianza! ✨"
|
||||
),
|
||||
"order_checkout_wa" to (
|
||||
"🎉 *CONFIRMACIÓN DE PEDIDO EN {tienda_nombre} #{orden_id}* 🎉\n\n" +
|
||||
"👤 *Cliente:* {cliente_nombre}\n" +
|
||||
"📱 *Teléfono:* {cliente_telefono}\n" +
|
||||
"📍 *Dirección:* {cliente_direccion}, {cliente_ciudad}\n\n" +
|
||||
"📦 *PRODUCTOS SOLICITADOS:*\n{articulos_resumen}\n\n" +
|
||||
"💵 *Subtotal:* {subtotal}\n" +
|
||||
"🚚 *Flete:* {costo_envio}\n" +
|
||||
"💰 *TOTAL A PAGAR:* {total}\n" +
|
||||
"{condicion_pago_resumen}\n" +
|
||||
"💳 *Forma de Pago:* {metodo_pago}\n\n" +
|
||||
"¡Por favor confirmar mi pedido para proceder con el alistamiento! 🔥"
|
||||
),
|
||||
"product_direct_inquiry" to (
|
||||
"Hola {tienda_nombre}! 👋 Quiero pedir las zapatillas:\n" +
|
||||
"👟 *{zapatilla_nombre}*\n" +
|
||||
"🎨 Color: *{color_nombre}*\n" +
|
||||
"📏 Talla: *{talla}* (Horma Nacional)\n" +
|
||||
"💎 Precio: *{precio} COP*\n" +
|
||||
"🚚 Entrega en: *{ciudad}* (Pago Contraentrega)\n" +
|
||||
"⚡ Deseo separar con $10.000 COP y cancelar el saldo en mi casa.\n\n" +
|
||||
"¿Me confirman disponibilidad para despacho por favor?"
|
||||
),
|
||||
"product_request_size" to (
|
||||
"Hola {tienda_nombre}! 👋 Estaba revisando la tienda y me gustaron mucho estas zapatillas:\n" +
|
||||
"👟 *{zapatilla_nombre}*\n" +
|
||||
"🎨 Color: *{color_nombre}*\n" +
|
||||
"📏 Talla que necesito: *{talla}* (Actualmente no aparece disponible en stock inmediato)\n" +
|
||||
"📍 Ciudad de entrega: *{ciudad}*\n\n" +
|
||||
"¿Es posible conseguir esta talla bajo pedido con sus bodegas aliadas para despacho contraentrega? Quedo muy atento, ¡muchas gracias!"
|
||||
),
|
||||
"general_store_inquiry" to (
|
||||
"Hola {tienda_nombre}! 👋 Me comunico desde la app oficial para consultar disponibilidad de zapatillas y asesoría para despacho contraentrega en {ciudad}."
|
||||
),
|
||||
"advisor_contact" to (
|
||||
"👋 ¡Hola equipo de {tienda_nombre}! Deseo recibir asesoría personalizada sobre productos disponibles y tallas para entrega en {ciudad}."
|
||||
),
|
||||
"customer_request_received" to (
|
||||
"👋 ¡Hola *{cliente_nombre}*!\n\n" +
|
||||
"Recibimos con éxito tu solicitud por encargo para las *{marca} {modelo}* en talla *{talla}*. 👟🔥\n\n" +
|
||||
"Nuestro equipo en {tienda_nombre} ya está verificando disponibilidad y mejor precio con nuestras bodegas y proveedores mayoristas. " +
|
||||
"Te responderemos a este mismo chat con la cotización exacta en el menor tiempo posible.\n\n" +
|
||||
"¡Gracias por confiar en {tienda_nombre}! 🚀"
|
||||
),
|
||||
"customer_deposit_request" to (
|
||||
"⚡ *{tienda_nombre} - CONFIRMACIÓN DE DESPACHO* 📦\n\n" +
|
||||
"Hola {cliente_nombre}, para programar el despacho express de tu orden *#{orden_id}* hacia *{cliente_ciudad}*, realiza el pago del anticipo de separación de *{valor_anticipo}*.\n\n" +
|
||||
"💳 *Paga en línea de forma 100% segura por Bold (PSE / Tarjetas / Bre-B / Nequi):*\n" +
|
||||
"👉 {link_pago_bold}\n\n" +
|
||||
"🤝 El saldo restante de *{saldo_contraentrega}* lo cancelas al recibir tu paquete en tu puerta. ¡Gracias por tu compra!"
|
||||
),
|
||||
"customer_deposit_confirmed" to (
|
||||
"🎉 *¡Tu anticipo de separación fue confirmado con éxito!*\n\n" +
|
||||
"Hola *{cliente_nombre}*, recibimos correctamente tu pago de *{valor_anticipo}* a través de Bold.\n\n" +
|
||||
"📦 Tu orden *#{orden_id}* ya se encuentra en alistamiento y empaque para despacho a *{cliente_ciudad}*.\n" +
|
||||
"🤝 *Saldo restante a pagar contraentrega:* *{saldo_contraentrega}* al recibir en tu puerta.\n\n" +
|
||||
"Puedes consultar el estado de tu pedido en cualquier momento desde la app oficial de {tienda_nombre}.\n\n" +
|
||||
"¡Muchas gracias por tu compra en {tienda_nombre}!"
|
||||
),
|
||||
"order_on_the_way" to (
|
||||
"👋 ¡Hola *{cliente_nombre}*!\n\n" +
|
||||
"🛵 Tu pedido #{orden_id} ya va *EN CAMINO* con nuestro mensajero.\n\n" +
|
||||
"📍 *Dirección de Entrega:* {cliente_direccion}\n" +
|
||||
"🏙️ *Ciudad:* {cliente_ciudad}\n" +
|
||||
"💵 *Saldo a Pagar Contraentrega:* *{saldo_contraentrega}*\n" +
|
||||
"💳 *Medio de Pago:* {metodo_pago} (Efectivo o Transferencia Nequi)\n\n" +
|
||||
"Por favor estar atento a la portería o celular. ¡Gracias por tu compra en {tienda_nombre}! 🔥"
|
||||
),
|
||||
"order_delivered" to (
|
||||
"🌟 ¡Hola *{cliente_nombre}*! Tu orden #{orden_id} ha sido *ENTREGADA CON ÉXITO*.\n\n" +
|
||||
"👟 Esperamos que disfrutes al máximo tus zapatillas.\n\n" +
|
||||
"💬 Si tienes cualquier consulta sobre el 1er cambio de talla gratuito o garantía, estamos a tu disposición por este canal oficial.\n\n" +
|
||||
"¡Muchas gracias por elegir {tienda_nombre}! ✨"
|
||||
),
|
||||
"auth_login_otp" to (
|
||||
"🔐 *{tienda_nombre} - Código de Seguridad*\n\n" +
|
||||
"Hola {usuario_nombre}, tu código de verificación para iniciar sesión es:\n\n" +
|
||||
"👉 *{codigo_otp}*\n\n" +
|
||||
"⏱️ _Válido por 5 minutos. No compartas este código con nadie._"
|
||||
)
|
||||
)
|
||||
|
||||
/**
|
||||
* Sincroniza en background las plantillas registradas en la base de datos de la plataforma
|
||||
*/
|
||||
fun syncTemplates(context: Context) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
val result = ApiClient.getMessageTemplates()
|
||||
result.onSuccess { templatesMap ->
|
||||
if (templatesMap.isNotEmpty()) {
|
||||
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
val editor = prefs.edit()
|
||||
for ((id, content) in templatesMap) {
|
||||
editor.putString("$PREFIX_TEMPLATE$id", content)
|
||||
}
|
||||
editor.apply()
|
||||
}
|
||||
}
|
||||
} catch (_: Exception) {}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renderiza una plantilla reemplazando todas las etiquetas {variable} por sus valores.
|
||||
* Soporta alias comunes para máxima compatibilidad entre web y app móvil.
|
||||
*/
|
||||
fun renderTemplate(context: Context, templateId: String, variables: Map<String, String>): String {
|
||||
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
|
||||
// Resolver posible alias (ej: advisor_contact -> general_store_inquiry)
|
||||
val candidateIds = when (templateId) {
|
||||
"advisor_contact" -> listOf("advisor_contact", "general_store_inquiry")
|
||||
"general_store_inquiry" -> listOf("general_store_inquiry", "advisor_contact")
|
||||
else -> listOf(templateId)
|
||||
}
|
||||
|
||||
var rawContent: String? = null
|
||||
for (id in candidateIds) {
|
||||
val fromPrefs = prefs.getString("$PREFIX_TEMPLATE$id", null)
|
||||
if (!fromPrefs.isNullOrBlank()) {
|
||||
rawContent = fromPrefs
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (rawContent.isNullOrBlank()) {
|
||||
for (id in candidateIds) {
|
||||
if (DEFAULT_TEMPLATES.containsKey(id)) {
|
||||
rawContent = DEFAULT_TEMPLATES[id]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (rawContent.isNullOrBlank()) {
|
||||
return ""
|
||||
}
|
||||
|
||||
var rendered = rawContent ?: ""
|
||||
val effectiveVars = variables.toMutableMap()
|
||||
if (!effectiveVars.containsKey("tienda_nombre")) {
|
||||
effectiveVars["tienda_nombre"] = "DripZone"
|
||||
}
|
||||
|
||||
for ((key, value) in effectiveVars) {
|
||||
rendered = rendered.replace("{$key}", value)
|
||||
}
|
||||
return rendered
|
||||
}
|
||||
|
||||
/**
|
||||
* Limpia caracteres de formato WhatsApp (*negrita*, _cursiva_, ~tachado~)
|
||||
* para visualización limpia en notificaciones del sistema Android.
|
||||
*/
|
||||
fun stripMarkdown(text: String): String {
|
||||
return text
|
||||
.replace("\\*([^*]+)\\*".toRegex(), "$1")
|
||||
.replace("_([^_]+)_".toRegex(), "$1")
|
||||
.replace("~([^~]+)~".toRegex(), "$1")
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
package co.dripzone.app.model
|
||||
|
||||
import java.io.Serializable
|
||||
|
||||
data class Order(
|
||||
val id: String = "",
|
||||
val createdAt: String? = null,
|
||||
val status: String = "pending",
|
||||
val customerName: String = "",
|
||||
val customerPhone: String = "",
|
||||
val cityName: String = "",
|
||||
val address: String = "",
|
||||
val subtotal: Double = 0.0,
|
||||
val shippingCost: Double = 0.0,
|
||||
val totalAmount: Double = 0.0,
|
||||
val depositAmount: Double = 0.0,
|
||||
val depositPaid: Boolean = false,
|
||||
val balanceDue: Double = 0.0,
|
||||
val paymentMethodName: String? = null,
|
||||
val depositPaymentLinkId: String? = null,
|
||||
val boldCheckoutUrl: String? = null,
|
||||
val items: List<OrderItem> = emptyList()
|
||||
) : Serializable
|
||||
|
||||
data class OrderItem(
|
||||
val productId: String? = null,
|
||||
val productName: String = "",
|
||||
val brand: String? = null,
|
||||
val size: String = "",
|
||||
val quantity: Int = 1,
|
||||
val price: Double = 0.0,
|
||||
val image: String? = null
|
||||
) : Serializable
|
||||
@ -65,4 +65,16 @@ object UserManager {
|
||||
val prefs = SecurityManager.getSecurePreferences(context)
|
||||
prefs.edit().putBoolean(KEY_PHONE_VERIFIED, isVerified).apply()
|
||||
}
|
||||
|
||||
fun clearUser(context: Context) {
|
||||
val prefs = SecurityManager.getSecurePreferences(context)
|
||||
prefs.edit()
|
||||
.remove(KEY_USER_NAME)
|
||||
.remove(KEY_USER_PHONE)
|
||||
.remove(KEY_USER_CITY)
|
||||
.remove(KEY_USER_ADDRESS)
|
||||
.remove(KEY_PHONE_VERIFIED)
|
||||
.remove(KEY_TOTAL_ORDERS)
|
||||
.apply()
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,182 @@
|
||||
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.api.ApiClient
|
||||
import co.dripzone.app.databinding.BottomSheetAccountBinding
|
||||
import co.dripzone.app.model.UserManager
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class AccountBottomSheet : BottomSheetDialogFragment() {
|
||||
|
||||
private var _binding: BottomSheetAccountBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private var pendingName = ""
|
||||
private var pendingPhone = ""
|
||||
private var pendingCity = ""
|
||||
|
||||
var onUserChangedListener: (() -> Unit)? = null
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
_binding = BottomSheetAccountBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
setupListeners()
|
||||
renderState()
|
||||
}
|
||||
|
||||
private fun renderState() {
|
||||
val user = UserManager.getUser(requireContext())
|
||||
|
||||
if (user.isPhoneVerified && user.phone.isNotBlank()) {
|
||||
binding.layoutAccountLoggedIn.visibility = View.VISIBLE
|
||||
binding.layoutAccountLoggedOut.visibility = View.GONE
|
||||
binding.pbAccountLoading.visibility = View.GONE
|
||||
|
||||
binding.tvAccountUserName.text = user.name.ifBlank { "Cliente DripZone" }
|
||||
binding.tvAccountUserPhone.text = "+57 ${user.phone} • Verificado ✅"
|
||||
val loc = if (user.address.isNotBlank()) "${user.address} (${user.city})" else user.city
|
||||
binding.tvAccountUserLocation.text = "📍 Ubicación: $loc"
|
||||
} else {
|
||||
binding.layoutAccountLoggedIn.visibility = View.GONE
|
||||
binding.layoutAccountLoggedOut.visibility = View.VISIBLE
|
||||
binding.layoutAccountForm.visibility = View.VISIBLE
|
||||
binding.layoutAccountOtp.visibility = View.GONE
|
||||
binding.pbAccountLoading.visibility = View.GONE
|
||||
|
||||
if (user.name.isNotBlank()) binding.etAccountName.setText(user.name)
|
||||
if (user.phone.isNotBlank()) binding.etAccountPhone.setText(user.phone)
|
||||
if (user.city.isNotBlank()) binding.etAccountCity.setText(user.city)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupListeners() {
|
||||
binding.btnCloseAccount.setOnClickListener {
|
||||
dismiss()
|
||||
}
|
||||
|
||||
binding.btnAccountViewOrders.setOnClickListener {
|
||||
dismiss()
|
||||
OrdersBottomSheet().show(parentFragmentManager, OrdersBottomSheet.TAG)
|
||||
}
|
||||
|
||||
binding.btnAccountLogout.setOnClickListener {
|
||||
UserManager.clearUser(requireContext())
|
||||
Toast.makeText(requireContext(), "Sesión cerrada correctamente", Toast.LENGTH_SHORT).show()
|
||||
onUserChangedListener?.invoke()
|
||||
renderState()
|
||||
}
|
||||
|
||||
binding.btnAccountSendOtp.setOnClickListener {
|
||||
handleSendOtp()
|
||||
}
|
||||
|
||||
binding.btnAccountVerifyOtp.setOnClickListener {
|
||||
handleVerifyOtp()
|
||||
}
|
||||
|
||||
binding.tvAccountResendOtp.setOnClickListener {
|
||||
handleSendOtp()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSendOtp() {
|
||||
val name = binding.etAccountName.text?.toString()?.trim() ?: ""
|
||||
val rawPhone = binding.etAccountPhone.text?.toString()?.replace("\\D".toRegex(), "") ?: ""
|
||||
val city = binding.etAccountCity.text?.toString()?.trim() ?: "Cali"
|
||||
|
||||
if (name.isBlank()) {
|
||||
binding.etAccountName.error = "Ingresa tu nombre"
|
||||
return
|
||||
}
|
||||
if (rawPhone.length < 10) {
|
||||
binding.etAccountPhone.error = "Ingresa tu WhatsApp a 10 dígitos"
|
||||
return
|
||||
}
|
||||
|
||||
pendingName = name
|
||||
pendingPhone = rawPhone
|
||||
pendingCity = city
|
||||
|
||||
binding.pbAccountLoading.visibility = View.VISIBLE
|
||||
binding.btnAccountSendOtp.isEnabled = false
|
||||
|
||||
lifecycleScope.launch {
|
||||
val result = ApiClient.sendOtp(pendingPhone, pendingName)
|
||||
binding.pbAccountLoading.visibility = View.GONE
|
||||
binding.btnAccountSendOtp.isEnabled = true
|
||||
|
||||
result.onSuccess { msg ->
|
||||
Toast.makeText(requireContext(), msg, Toast.LENGTH_LONG).show()
|
||||
binding.layoutAccountForm.visibility = View.GONE
|
||||
binding.layoutAccountOtp.visibility = View.VISIBLE
|
||||
val masked = if (pendingPhone.length >= 10) "${pendingPhone.take(3)}***${pendingPhone.takeLast(4)}" else pendingPhone
|
||||
binding.tvAccountOtpInstruction.text = "Enviamos un código de 6 dígitos a tu WhatsApp (+57 $masked) para iniciar sesión:"
|
||||
}.onFailure { err ->
|
||||
Toast.makeText(requireContext(), "Error: ${err.message}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleVerifyOtp() {
|
||||
val code = binding.etAccountOtpCode.text?.toString()?.trim() ?: ""
|
||||
if (code.length < 6) {
|
||||
binding.etAccountOtpCode.error = "Ingresa los 6 dígitos recibidos"
|
||||
return
|
||||
}
|
||||
|
||||
binding.pbAccountLoading.visibility = View.VISIBLE
|
||||
binding.btnAccountVerifyOtp.isEnabled = false
|
||||
|
||||
lifecycleScope.launch {
|
||||
val result = ApiClient.verifyOtp(
|
||||
phone = pendingPhone,
|
||||
code = code,
|
||||
name = pendingName,
|
||||
city = pendingCity,
|
||||
address = ""
|
||||
)
|
||||
binding.pbAccountLoading.visibility = View.GONE
|
||||
binding.btnAccountVerifyOtp.isEnabled = true
|
||||
|
||||
result.onSuccess {
|
||||
UserManager.saveUser(
|
||||
context = requireContext(),
|
||||
name = pendingName,
|
||||
phone = pendingPhone,
|
||||
city = pendingCity,
|
||||
address = "",
|
||||
isVerified = true
|
||||
)
|
||||
Toast.makeText(requireContext(), "¡Sesión iniciada con éxito! Bienvenido(a)", Toast.LENGTH_LONG).show()
|
||||
onUserChangedListener?.invoke()
|
||||
renderState()
|
||||
}.onFailure { err ->
|
||||
Toast.makeText(requireContext(), "Código inválido: ${err.message}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "AccountBottomSheet"
|
||||
}
|
||||
}
|
||||
@ -75,7 +75,19 @@ class CustomOrderBottomSheet : BottomSheetDialogFragment() {
|
||||
binding.btnFinishCustom.setOnClickListener { dismiss() }
|
||||
|
||||
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?"
|
||||
val variables = mapOf(
|
||||
"tienda_nombre" to "DripZone",
|
||||
"cliente_nombre" to (customerName.ifBlank { "Cliente DripZone" }),
|
||||
"marca" to (initialBrand.ifBlank { "Sneakers" }),
|
||||
"modelo" to modelDesired,
|
||||
"talla" to sizeDesired,
|
||||
"zapatilla_nombre" to modelDesired,
|
||||
"color_nombre" to (notes.ifBlank { "Original" }),
|
||||
"ciudad" to (customerCity.ifBlank { "Colombia" })
|
||||
)
|
||||
val message = co.dripzone.app.manager.TemplateManager.renderTemplate(requireContext(), "product_request_size", variables).ifBlank {
|
||||
co.dripzone.app.manager.TemplateManager.renderTemplate(requireContext(), "customer_request_received", variables)
|
||||
}
|
||||
SecurityManager.openWhatsApp(
|
||||
requireContext(),
|
||||
SecurityManager.WHATSAPP_STORE_PHONE,
|
||||
@ -212,7 +224,18 @@ class CustomOrderBottomSheet : BottomSheetDialogFragment() {
|
||||
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."
|
||||
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 tienda ha sido notificada y te contactaremos por WhatsApp con la cotización exacta."
|
||||
|
||||
// Disparar notificación oficial en Android basada en la plantilla 'customer_request_received'
|
||||
val customVars = mapOf(
|
||||
"tienda_nombre" to "DripZone",
|
||||
"cliente_nombre" to customerName,
|
||||
"marca" to effectiveBrand,
|
||||
"modelo" to modelDesired,
|
||||
"talla" to sizeDesired,
|
||||
"ciudad" to customerCity
|
||||
)
|
||||
co.dripzone.app.manager.AppNotificationManager.showCustomRequestNotification(requireContext(), customVars)
|
||||
}.onFailure { err ->
|
||||
binding.btnVerifyCustomOtp.isEnabled = true
|
||||
Toast.makeText(requireContext(), "Error al registrar: ${err.message}", Toast.LENGTH_LONG).show()
|
||||
|
||||
@ -111,14 +111,9 @@ class OrderVerificationBottomSheet : BottomSheetDialogFragment() {
|
||||
dismiss()
|
||||
}
|
||||
|
||||
binding.btnOpenWhatsAppChat.setOnClickListener {
|
||||
val message = "¡Hola DripZone! Acabo de registrar mi pedido en la app oficial con la orden *$createdOrderId*. ¿Me confirman la programación del despacho contraentrega a ${selectedCity.name}?"
|
||||
SecurityManager.openWhatsApp(
|
||||
requireContext(),
|
||||
SecurityManager.WHATSAPP_STORE_PHONE,
|
||||
message
|
||||
)
|
||||
binding.btnViewOrderDetails.setOnClickListener {
|
||||
dismiss()
|
||||
OrdersBottomSheet().show(parentFragmentManager, OrdersBottomSheet.TAG)
|
||||
}
|
||||
}
|
||||
|
||||
@ -276,6 +271,26 @@ class OrderVerificationBottomSheet : BottomSheetDialogFragment() {
|
||||
createdOrderId = orderId
|
||||
UserManager.incrementCompletedOrders(requireContext())
|
||||
|
||||
// Disparar notificación oficial en Android basada en la plantilla registrada
|
||||
val itemsSummary = items.joinToString("\n") { "• 👟 ${it.product.name} (Talla ${it.selectedSize}) x${it.quantity} — ${format.format(it.itemTotal)} COP" }
|
||||
val orderVars = mapOf(
|
||||
"tienda_nombre" to "DripZone",
|
||||
"orden_id" to orderId.takeLast(8).uppercase(),
|
||||
"cliente_nombre" to customerName,
|
||||
"cliente_telefono" to customerPhone,
|
||||
"cliente_direccion" to customerAddress,
|
||||
"cliente_ciudad" to selectedCity.name,
|
||||
"articulos_resumen" to itemsSummary,
|
||||
"subtotal" to "${format.format(subtotal)} COP",
|
||||
"costo_envio" to (if (selectedCity.shippingCost <= 0) "GRATIS" else "${format.format(selectedCity.shippingCost)} COP"),
|
||||
"total" to "${format.format(total)} COP",
|
||||
"condicion_pago_resumen" to (if (requiresDeposit) "⚡ Separación: $10.000 COP • Saldo Contraentrega: ${format.format(balanceDue)} COP" else "🤝 100% Pago Contraentrega"),
|
||||
"metodo_pago" to (if (requiresDeposit) "Separación $10.000 + Contraentrega" else "100% Contraentrega"),
|
||||
"saldo_contraentrega" to "${format.format(balanceDue)} COP",
|
||||
"anticipo" to (if (requiresDeposit) "$10.000 COP" else "$0 COP")
|
||||
)
|
||||
co.dripzone.app.manager.AppNotificationManager.showOrderPlacedNotification(requireContext(), orderId, orderVars)
|
||||
|
||||
// Limpiar carrito
|
||||
if (!isDirectProductOrder) {
|
||||
CartManager.clear(requireContext())
|
||||
@ -308,7 +323,7 @@ class OrderVerificationBottomSheet : BottomSheetDialogFragment() {
|
||||
}
|
||||
|
||||
// Configurar botón para abrir pasarela de pago Bold
|
||||
val checkoutUrl = if (boldData.checkoutUrl.isNotBlank()) boldData.checkoutUrl else "https://bold.co/p/carlos-lasso"
|
||||
val checkoutUrl = if (boldData.checkoutUrl.isNotBlank()) boldData.checkoutUrl else "https://drip-zone.online"
|
||||
binding.btnPaySeparationBold.setOnClickListener {
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(checkoutUrl))
|
||||
startActivity(intent)
|
||||
@ -326,34 +341,43 @@ class OrderVerificationBottomSheet : BottomSheetDialogFragment() {
|
||||
binding.layoutBoldPaidSuccess.visibility = View.VISIBLE
|
||||
binding.tvSuccessMessage.text = "¡Anticipo de $10.000 COP recibido y confirmado por Bold! Tu pedido ha sido programado para despacho inmediato."
|
||||
Toast.makeText(requireContext(), "🎉 ¡Anticipo verificado en tiempo real con Bold!", Toast.LENGTH_LONG).show()
|
||||
|
||||
// Disparar notificación oficial de anticipo confirmado
|
||||
val depositVars = mapOf(
|
||||
"tienda_nombre" to "DripZone",
|
||||
"cliente_nombre" to customerName,
|
||||
"orden_id" to orderId.takeLast(8).uppercase(),
|
||||
"valor_anticipo" to "$10.000 COP",
|
||||
"cliente_ciudad" to selectedCity.name,
|
||||
"saldo_contraentrega" to "${format.format(balanceDue)} COP"
|
||||
)
|
||||
co.dripzone.app.manager.AppNotificationManager.showDepositConfirmedNotification(requireContext(), orderId, depositVars)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
binding.btnPaySeparationBold.setOnClickListener {
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://bold.co/p/carlos-lasso"))
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://drip-zone.online"))
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.btnOpenWhatsAppChat.text = "Notificar Pago a Carlos en WhatsApp"
|
||||
binding.btnOpenWhatsAppChat.setOnClickListener {
|
||||
val message = "¡Hola Carlos! Acabo de registrar mi pedido en DripZone con la orden *$createdOrderId*. Voy a cancelar la separación de $10.000 COP para coordinar el despacho a ${selectedCity.name}. Saldo al recibir: ${format.format(balanceDue)} COP."
|
||||
SecurityManager.openWhatsApp(requireContext(), SecurityManager.WHATSAPP_STORE_PHONE, message)
|
||||
binding.btnViewOrderDetails.text = "Ver Mis Pedidos 📦"
|
||||
binding.btnViewOrderDetails.setOnClickListener {
|
||||
dismiss()
|
||||
OrdersBottomSheet().show(parentFragmentManager, OrdersBottomSheet.TAG)
|
||||
}
|
||||
} else {
|
||||
binding.tvSuccessMessage.text = "¡Tu número de WhatsApp (+57 $customerPhone) fue validado y tu pedido fue registrado en la plataforma!\n\n⭐ BENEFICIO CLIENTE FRECUENTE:\nComo ya realizaste compras satisfactoriamente, estás exento de separación y pagarás el 100% (${format.format(total)} COP) contraentrega al recibir tu paquete."
|
||||
binding.layoutBoldQrContainer.visibility = View.GONE
|
||||
binding.layoutBoldPaidSuccess.visibility = View.GONE
|
||||
binding.btnPaySeparationBold.visibility = View.GONE
|
||||
binding.btnOpenWhatsAppChat.text = "Coordinar Entrega por WhatsApp"
|
||||
binding.btnOpenWhatsAppChat.setOnClickListener {
|
||||
val message = "¡Hola Carlos! Acabo de registrar mi orden *$createdOrderId* en DripZone con 100% Contraentrega a ${selectedCity.name}. ¿Me confirmas la programación del despacho?"
|
||||
SecurityManager.openWhatsApp(requireContext(), SecurityManager.WHATSAPP_STORE_PHONE, message)
|
||||
binding.btnViewOrderDetails.text = "Ver Mis Pedidos 📦"
|
||||
binding.btnViewOrderDetails.setOnClickListener {
|
||||
dismiss()
|
||||
OrdersBottomSheet().show(parentFragmentManager, OrdersBottomSheet.TAG)
|
||||
}
|
||||
}
|
||||
}.onFailure { err ->
|
||||
|
||||
@ -0,0 +1,192 @@
|
||||
package co.dripzone.app.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import co.dripzone.app.R
|
||||
import co.dripzone.app.databinding.ItemOrderCardBinding
|
||||
import co.dripzone.app.model.Order
|
||||
import java.text.NumberFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
class OrdersAdapter(
|
||||
private var orders: List<Order>,
|
||||
private val onPayDepositClicked: (Order) -> Unit,
|
||||
private val onInquireStatusClicked: (Order) -> Unit
|
||||
) : RecyclerView.Adapter<OrdersAdapter.OrderViewHolder>() {
|
||||
|
||||
private val copFormat = NumberFormat.getCurrencyInstance(Locale("es", "CO")).apply {
|
||||
maximumFractionDigits = 0
|
||||
}
|
||||
|
||||
fun submitList(newOrders: List<Order>) {
|
||||
orders = newOrders
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): OrderViewHolder {
|
||||
val binding = ItemOrderCardBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
return OrderViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: OrderViewHolder, position: Int) {
|
||||
holder.bind(orders[position])
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = orders.size
|
||||
|
||||
inner class OrderViewHolder(private val binding: ItemOrderCardBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
fun bind(order: Order) {
|
||||
val context = binding.root.context
|
||||
|
||||
// 1. Order ID & Date
|
||||
binding.tvCardOrderId.text = "Orden #${order.id.takeLast(8).uppercase()}"
|
||||
binding.tvCardOrderDate.text = formatOrderDate(order.createdAt)
|
||||
|
||||
// 2. Status Badge
|
||||
bindStatusBadge(order.status)
|
||||
|
||||
// 3. Products list (Dynamic injection)
|
||||
binding.llOrderItemsContainer.removeAllViews()
|
||||
for (item in order.items) {
|
||||
val itemView = createProductItemView(context, item.productName, item.size, item.quantity, item.price)
|
||||
binding.llOrderItemsContainer.addView(itemView)
|
||||
}
|
||||
|
||||
// 4. Financial Breakdown
|
||||
binding.tvCardSubtotal.text = formatCop(order.subtotal)
|
||||
binding.tvCardShippingCost.text = if (order.shippingCost <= 0) "GRATIS" else formatCop(order.shippingCost)
|
||||
binding.tvCardTotal.text = formatCop(order.totalAmount)
|
||||
|
||||
// 5. Deposit Box
|
||||
val hasDepositPaid = order.depositAmount > 0 && order.status != "pending"
|
||||
if (hasDepositPaid) {
|
||||
binding.tvDepositStatusLabel.text = "${formatCop(order.depositAmount)} • Acreditado ✅"
|
||||
binding.tvDepositStatusLabel.setTextColor(Color.parseColor("#22C55E"))
|
||||
binding.btnPayDepositBold.visibility = View.GONE
|
||||
} else {
|
||||
binding.tvDepositStatusLabel.text = "$10.000 COP • Pendiente ⏳"
|
||||
binding.tvDepositStatusLabel.setTextColor(Color.parseColor("#F59E0B"))
|
||||
binding.btnPayDepositBold.visibility = View.VISIBLE
|
||||
binding.btnPayDepositBold.setOnClickListener {
|
||||
onPayDepositClicked(order)
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Saldo Contraentrega
|
||||
val balance = if (order.balanceDue > 0) order.balanceDue else (order.totalAmount - order.depositAmount)
|
||||
binding.tvCardBalanceDue.text = "${formatCop(balance)} COP"
|
||||
|
||||
// 7. Address
|
||||
binding.tvCardAddress.text = "📍 Entrega en: ${order.address} (${order.cityName})"
|
||||
|
||||
// 8. WhatsApp Status Inquiry
|
||||
binding.btnOrderWhatsAppStatus.setOnClickListener {
|
||||
onInquireStatusClicked(order)
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindStatusBadge(status: String?) {
|
||||
val (badgeText, badgeColor) = when ((status ?: "pending").lowercase()) {
|
||||
"deposit_received" -> "⚡ Anticipo Acreditado" to "#22C55E"
|
||||
"pending" -> "⏳ Anticipo Pendiente" to "#F59E0B"
|
||||
"processing" -> "📦 En Preparación" to "#3B82F6"
|
||||
"shipped" -> "🚚 Despachado / En Ruta" to "#A855F7"
|
||||
"delivered" -> "✅ Entregado con Éxito" to "#10B981"
|
||||
"cancelled", "rejected" -> "❌ Pedido Cancelado" to "#EF4444"
|
||||
else -> "📋 Registrado" to "#8B5CF6"
|
||||
}
|
||||
binding.tvCardOrderStatusBadge.text = badgeText
|
||||
binding.tvCardOrderStatusBadge.setTextColor(Color.parseColor(badgeColor))
|
||||
}
|
||||
|
||||
private fun createProductItemView(
|
||||
context: Context,
|
||||
name: String,
|
||||
size: String,
|
||||
quantity: Int,
|
||||
price: Double
|
||||
): View {
|
||||
val row = LinearLayout(context).apply {
|
||||
orientation = LinearLayout.HORIZONTAL
|
||||
layoutParams = LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
).apply {
|
||||
setMargins(0, 4, 0, 4)
|
||||
}
|
||||
}
|
||||
|
||||
val icon = ImageView(context).apply {
|
||||
setImageResource(R.drawable.ic_package)
|
||||
setColorFilter(Color.parseColor("#8B5CF6"))
|
||||
layoutParams = LinearLayout.LayoutParams(36, 36).apply {
|
||||
marginEnd = 16
|
||||
}
|
||||
}
|
||||
|
||||
val detailsLayout = LinearLayout(context).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
|
||||
}
|
||||
|
||||
val tvTitle = TextView(context).apply {
|
||||
text = name
|
||||
textSize = 13f
|
||||
setTextColor(Color.parseColor("#FFFFFF"))
|
||||
typeface = android.graphics.Typeface.DEFAULT_BOLD
|
||||
}
|
||||
|
||||
val tvSub = TextView(context).apply {
|
||||
text = "Talla: $size • Cantidad: $quantity"
|
||||
textSize = 11f
|
||||
setTextColor(Color.parseColor("#94A3B8"))
|
||||
}
|
||||
|
||||
detailsLayout.addView(tvTitle)
|
||||
detailsLayout.addView(tvSub)
|
||||
|
||||
val tvPrice = TextView(context).apply {
|
||||
text = formatCop(price * quantity)
|
||||
textSize = 13f
|
||||
setTextColor(Color.parseColor("#FFFFFF"))
|
||||
typeface = android.graphics.Typeface.DEFAULT_BOLD
|
||||
}
|
||||
|
||||
row.addView(icon)
|
||||
row.addView(detailsLayout)
|
||||
row.addView(tvPrice)
|
||||
|
||||
return row
|
||||
}
|
||||
|
||||
private fun formatCop(value: Double): String {
|
||||
return copFormat.format(value)
|
||||
}
|
||||
|
||||
private fun formatOrderDate(isoDate: String?): String {
|
||||
if (isoDate.isNullOrBlank()) return "Reciente"
|
||||
return try {
|
||||
val inputFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault())
|
||||
val outputFormat = SimpleDateFormat("dd MMM yyyy, hh:mm a", Locale("es", "CO"))
|
||||
val date = inputFormat.parse(isoDate.take(19))
|
||||
if (date != null) outputFormat.format(date) else isoDate.take(10)
|
||||
} catch (_: Exception) {
|
||||
isoDate.take(10)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,159 @@
|
||||
package co.dripzone.app.ui
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
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 androidx.recyclerview.widget.LinearLayoutManager
|
||||
import co.dripzone.app.api.ApiClient
|
||||
import co.dripzone.app.databinding.BottomSheetOrdersBinding
|
||||
import co.dripzone.app.manager.TemplateManager
|
||||
import co.dripzone.app.model.Order
|
||||
import co.dripzone.app.model.UserManager
|
||||
import co.dripzone.app.security.SecurityManager
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class OrdersBottomSheet : BottomSheetDialogFragment() {
|
||||
|
||||
private var _binding: BottomSheetOrdersBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private lateinit var ordersAdapter: OrdersAdapter
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
_binding = BottomSheetOrdersBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
setupRecyclerView()
|
||||
setupListeners()
|
||||
loadOrders()
|
||||
}
|
||||
|
||||
private fun setupRecyclerView() {
|
||||
ordersAdapter = OrdersAdapter(
|
||||
orders = emptyList(),
|
||||
onPayDepositClicked = { order ->
|
||||
handlePayDeposit(order)
|
||||
},
|
||||
onInquireStatusClicked = { order ->
|
||||
handleInquireWhatsApp(order)
|
||||
}
|
||||
)
|
||||
|
||||
binding.rvOrders.apply {
|
||||
layoutManager = LinearLayoutManager(requireContext())
|
||||
adapter = ordersAdapter
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupListeners() {
|
||||
binding.btnCloseOrders.setOnClickListener {
|
||||
dismiss()
|
||||
}
|
||||
|
||||
binding.btnExploreCatalog.setOnClickListener {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadOrders() {
|
||||
val user = UserManager.getUser(requireContext())
|
||||
val phone = user.phone.replace("\\D".toRegex(), "")
|
||||
|
||||
if (phone.isBlank()) {
|
||||
binding.pbOrdersLoading.visibility = View.GONE
|
||||
binding.layoutEmptyOrders.visibility = View.VISIBLE
|
||||
binding.tvOrdersUserPhone.text = "Inicia sesión para consultar tus pedidos"
|
||||
binding.tvEmptyOrdersMessage.text = "Ingresa a 'Mi Cuenta' con tu número de WhatsApp para ver tu historial de compras y hacer seguimiento en tiempo real."
|
||||
binding.btnExploreCatalog.text = "Iniciar Sesión / Mi Cuenta 🔐"
|
||||
binding.btnExploreCatalog.setOnClickListener {
|
||||
dismiss()
|
||||
AccountBottomSheet().show(parentFragmentManager, AccountBottomSheet.TAG)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
val masked = if (phone.length >= 10) "${phone.take(3)}***${phone.takeLast(4)}" else phone
|
||||
binding.tvOrdersUserPhone.text = "Pedidos registrados para: +57 $masked"
|
||||
binding.pbOrdersLoading.visibility = View.VISIBLE
|
||||
binding.rvOrders.visibility = View.GONE
|
||||
binding.layoutEmptyOrders.visibility = View.GONE
|
||||
|
||||
lifecycleScope.launch {
|
||||
val result = ApiClient.getOrdersByPhone(phone)
|
||||
binding.pbOrdersLoading.visibility = View.GONE
|
||||
|
||||
result.onSuccess { orders ->
|
||||
if (orders.isEmpty()) {
|
||||
binding.layoutEmptyOrders.visibility = View.VISIBLE
|
||||
binding.rvOrders.visibility = View.GONE
|
||||
binding.tvEmptyOrdersMessage.text = "No encontramos pedidos registrados para el número +57 $masked."
|
||||
binding.btnExploreCatalog.text = "Explorar Catálogo 🔥"
|
||||
binding.btnExploreCatalog.setOnClickListener { dismiss() }
|
||||
} else {
|
||||
binding.layoutEmptyOrders.visibility = View.GONE
|
||||
binding.rvOrders.visibility = View.VISIBLE
|
||||
ordersAdapter.submitList(orders)
|
||||
}
|
||||
}.onFailure { err ->
|
||||
binding.layoutEmptyOrders.visibility = View.VISIBLE
|
||||
binding.tvEmptyOrdersMessage.text = "Error consultando pedidos: ${err.message}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handlePayDeposit(order: Order) {
|
||||
val boldUrl = order.boldCheckoutUrl ?: "https://drip-zone.online"
|
||||
try {
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(boldUrl))
|
||||
startActivity(intent)
|
||||
} catch (_: Exception) {
|
||||
Toast.makeText(requireContext(), "No se pudo abrir la pasarela Bold", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleInquireWhatsApp(order: Order) {
|
||||
val user = UserManager.getUser(requireContext())
|
||||
val itemsSummary = order.items.joinToString(", ") { "${it.productName} (Talla ${it.size})" }
|
||||
|
||||
val variables = mapOf(
|
||||
"tienda_nombre" to "DripZone",
|
||||
"orden_id" to order.id.takeLast(8).uppercase(),
|
||||
"cliente_nombre" to (user.name.ifBlank { "Cliente DripZone" }),
|
||||
"cliente_telefono" to user.phone,
|
||||
"cliente_direccion" to order.address,
|
||||
"cliente_ciudad" to order.cityName,
|
||||
"articulos_resumen" to itemsSummary,
|
||||
"subtotal" to "$${order.subtotal.toInt()}",
|
||||
"costo_envio" to (if (order.shippingCost <= 0) "GRATIS" else "$${order.shippingCost.toInt()}"),
|
||||
"total" to "$${order.totalAmount.toInt()}",
|
||||
"condicion_pago_resumen" to "Anticipo: $${order.depositAmount.toInt()} • Saldo: $${order.balanceDue.toInt()}",
|
||||
"metodo_pago" to (order.paymentMethodName ?: "Contraentrega")
|
||||
)
|
||||
|
||||
val message = TemplateManager.renderTemplate(requireContext(), "order_checkout_wa", variables)
|
||||
SecurityManager.openWhatsApp(requireContext(), SecurityManager.WHATSAPP_STORE_PHONE, message)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "OrdersBottomSheet"
|
||||
}
|
||||
}
|
||||
@ -78,6 +78,22 @@ class ProductDetailBottomSheet : BottomSheetDialogFragment() {
|
||||
dismiss()
|
||||
}
|
||||
|
||||
// Order or Inquire directly via WhatsApp using registered product_direct_inquiry template
|
||||
binding.btnOrderWhatsAppDirect.setOnClickListener {
|
||||
val sizeToOrder = if (selectedSize.isNotBlank()) selectedSize else sizeAdapter?.getSelectedSize() ?: "38-43"
|
||||
val variables = mapOf(
|
||||
"tienda_nombre" to "DripZone",
|
||||
"zapatilla_nombre" to p.name,
|
||||
"color_nombre" to "Estilo Oficial",
|
||||
"talla" to sizeToOrder,
|
||||
"precio" to p.formattedPrice(),
|
||||
"ciudad" to "Colombia",
|
||||
"marca" to p.brand
|
||||
)
|
||||
val message = co.dripzone.app.manager.TemplateManager.renderTemplate(requireContext(), "product_direct_inquiry", variables)
|
||||
SecurityManager.openWhatsApp(requireContext(), SecurityManager.WHATSAPP_STORE_PHONE, message)
|
||||
}
|
||||
|
||||
// Custom order button inside bottom sheet
|
||||
binding.btnCustomOrderSheet.setOnClickListener {
|
||||
dismiss()
|
||||
|
||||
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@color/bg_card" />
|
||||
<corners android:radius="12dp" />
|
||||
<stroke android:width="1dp" android:color="@color/bg_card_stroke" />
|
||||
</shape>
|
||||
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="#2E1065" />
|
||||
<corners android:radius="12dp" />
|
||||
<stroke android:width="1dp" android:color="#581C87" />
|
||||
</shape>
|
||||
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M16.5,2.75L7.5,2.75C4.88,2.75 2.75,4.88 2.75,7.5L2.75,16.5C2.75,19.12 4.88,21.25 7.5,21.25L16.5,21.25C19.12,21.25 21.25,19.12 21.25,16.5L21.25,7.5C21.25,4.88 19.12,2.75 16.5,2.75ZM20,13.5L16.83,13.5C16.32,13.5 15.86,13.82 15.69,14.3C15.11,15.93 13.68,17 12,17C10.32,17 8.89,15.93 8.31,14.3C8.14,13.82 7.68,13.5 7.17,13.5L4,13.5L4,7.5C4,5.57 5.57,4 7.5,4L16.5,4C18.43,4 20,5.57 20,7.5L20,13.5Z" />
|
||||
</vector>
|
||||
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M12,12c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4 -4,1.79 -4,4 1.79,4 4,4zM12,14c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4z" />
|
||||
</vector>
|
||||
@ -136,9 +136,9 @@
|
||||
android:visibility="gone" />
|
||||
</FrameLayout>
|
||||
|
||||
<!-- Header WhatsApp Button -->
|
||||
<!-- Header My Account Button -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnHeaderWhatsApp"
|
||||
android:id="@+id/btnHeaderAccount"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="36dp"
|
||||
android:layout_marginStart="6dp"
|
||||
@ -146,13 +146,14 @@
|
||||
android:insetTop="0dp"
|
||||
android:insetBottom="0dp"
|
||||
android:paddingHorizontal="10dp"
|
||||
android:text="Asesor"
|
||||
android:text="Mi Cuenta"
|
||||
android:textAllCaps="false"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textSize="11sp"
|
||||
android:textStyle="bold"
|
||||
app:backgroundTint="@color/accent_whatsapp"
|
||||
app:backgroundTint="@color/drip_purple"
|
||||
app:cornerRadius="10dp"
|
||||
app:icon="@drawable/ic_whatsapp"
|
||||
app:icon="@drawable/ic_user"
|
||||
app:iconGravity="textStart"
|
||||
app:iconPadding="4dp"
|
||||
app:iconSize="14dp" />
|
||||
@ -463,9 +464,9 @@
|
||||
android:textSize="10sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 4. Encargos Especiales -->
|
||||
<!-- 4. Mis Pedidos (Rastreo y Detalle) -->
|
||||
<LinearLayout
|
||||
android:id="@+id/navBtnEncargos"
|
||||
android:id="@+id/navBtnOrders"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
@ -475,19 +476,19 @@
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/ivNavEncargos"
|
||||
android:id="@+id/ivNavOrders"
|
||||
android:layout_width="22dp"
|
||||
android:layout_height="22dp"
|
||||
android:src="@drawable/ic_encargos"
|
||||
android:src="@drawable/ic_package"
|
||||
app:tint="@color/text_secondary" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvNavEncargos"
|
||||
android:id="@+id/tvNavOrders"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:text="Encargos"
|
||||
android:text="Mis Pedidos"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="10sp" />
|
||||
</LinearLayout>
|
||||
|
||||
@ -0,0 +1,353 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout 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: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"
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="sans-serif-black"
|
||||
android:text="👤 Mi Cuenta"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="20sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvAccountSubtitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:text="Perfil y verificación de cliente DripZone"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="13sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnCloseAccount"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/ic_close"
|
||||
app:tint="@color/text_secondary" />
|
||||
</RelativeLayout>
|
||||
|
||||
<!-- Progress Bar -->
|
||||
<ProgressBar
|
||||
android:id="@+id/pbAccountLoading"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginVertical="16dp"
|
||||
android:indeterminateTint="@color/accent_purple"
|
||||
android:visibility="gone" />
|
||||
|
||||
<!-- ========================================== -->
|
||||
<!-- STATE 1: LOGGED IN PROFILE -->
|
||||
<!-- ========================================== -->
|
||||
<LinearLayout
|
||||
android:id="@+id/layoutAccountLoggedIn"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:cardBackgroundColor="@color/bg_card"
|
||||
app:cardCornerRadius="16dp"
|
||||
app:cardElevation="0dp"
|
||||
app:strokeColor="@color/border_subtle"
|
||||
app:strokeWidth="1dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/ivAccountAvatar"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:background="@drawable/bg_category_chip_active"
|
||||
android:padding="12dp"
|
||||
android:src="@drawable/ic_user"
|
||||
app:tint="#FFFFFF" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_toEndOf="@id/ivAccountAvatar"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvAccountUserName"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="sans-serif-black"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="16sp"
|
||||
tools:text="Carlos Lasso" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvAccountUserPhone"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:drawableStart="@drawable/ic_whatsapp"
|
||||
android:drawablePadding="4dp"
|
||||
android:drawableTint="@color/accent_whatsapp"
|
||||
android:textColor="@color/accent_whatsapp"
|
||||
android:textSize="12sp"
|
||||
tools:text="+57 310 639 0296 • Verificado" />
|
||||
</LinearLayout>
|
||||
</RelativeLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:background="#1AFFFFFF" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvAccountUserLocation"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="12sp"
|
||||
tools:text="📍 Entrega habitual: Calle 5 # 24-80 (Cali)" />
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<!-- Button: Ver Mis Pedidos -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnAccountViewOrders"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:fontFamily="sans-serif-bold"
|
||||
android:text="📦 Ver Mis Pedidos"
|
||||
android:textSize="14sp"
|
||||
app:backgroundTint="@color/accent_purple"
|
||||
app:cornerRadius="12dp"
|
||||
app:icon="@drawable/ic_package"
|
||||
app:iconGravity="textStart" />
|
||||
|
||||
<!-- Button: Cerrar Sesión -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnAccountLogout"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="44dp"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:text="Cerrar Sesión"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="13sp"
|
||||
app:cornerRadius="12dp"
|
||||
app:strokeColor="@color/border_subtle" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- ========================================== -->
|
||||
<!-- STATE 2: NOT LOGGED IN / FORM -->
|
||||
<!-- ========================================== -->
|
||||
<LinearLayout
|
||||
android:id="@+id/layoutAccountLoggedOut"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<!-- Form Step 1: Input Details -->
|
||||
<LinearLayout
|
||||
android:id="@+id/layoutAccountForm"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="Ingresa tu número de WhatsApp para autenticarte sin contraseña, validar tus compras contraentrega y consultar tus pedidos."
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="13sp" />
|
||||
|
||||
<!-- Nombre Completo -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="6dp"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:text="Tu Nombre Completo *"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="13sp" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/etAccountName"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginBottom="14dp"
|
||||
android:background="@drawable/bg_category_chip"
|
||||
android:hint="Ej: Juan Pérez"
|
||||
android:inputType="textPersonName"
|
||||
android:paddingHorizontal="14dp"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textColorHint="@color/text_secondary"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<!-- Celular WhatsApp -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="6dp"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:text="Tu Celular de WhatsApp (10 dígitos) *"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="13sp" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/etAccountPhone"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginBottom="14dp"
|
||||
android:background="@drawable/bg_category_chip"
|
||||
android:hint="Ej: 3106390296"
|
||||
android:inputType="phone"
|
||||
android:maxLength="10"
|
||||
android:paddingHorizontal="14dp"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textColorHint="@color/text_secondary"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<!-- Ciudad -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="6dp"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:text="Ciudad de Residencia"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="13sp" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/etAccountCity"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginBottom="20dp"
|
||||
android:background="@drawable/bg_category_chip"
|
||||
android:hint="Ej: Cali, Medellín, Bogotá"
|
||||
android:inputType="textCapWords"
|
||||
android:paddingHorizontal="14dp"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textColorHint="@color/text_secondary"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<!-- Enviar OTP Button -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnAccountSendOtp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="50dp"
|
||||
android:fontFamily="sans-serif-bold"
|
||||
android:text="Continuar con WhatsApp 💬"
|
||||
android:textSize="14sp"
|
||||
app:backgroundTint="@color/accent_purple"
|
||||
app:cornerRadius="12dp" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Form Step 2: OTP Verification -->
|
||||
<LinearLayout
|
||||
android:id="@+id/layoutAccountOtp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvAccountOtpInstruction"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="Enviamos un código de seguridad de 6 dígitos a tu WhatsApp para verificar tu cuenta:"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="13sp" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/etAccountOtpCode"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="56dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:background="@drawable/bg_category_chip"
|
||||
android:gravity="center"
|
||||
android:hint="------"
|
||||
android:inputType="number"
|
||||
android:letterSpacing="0.3"
|
||||
android:maxLength="6"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textColorHint="@color/text_secondary"
|
||||
android:textSize="22sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnAccountVerifyOtp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="50dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:fontFamily="sans-serif-bold"
|
||||
android:text="Verificar e Ingresar 🔐"
|
||||
android:textSize="14sp"
|
||||
app:backgroundTint="@color/accent_whatsapp"
|
||||
app:cornerRadius="12dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvAccountResendOtp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="6dp"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:padding="8dp"
|
||||
android:text="¿No recibiste el código? Reenviar por WhatsApp"
|
||||
android:textColor="@color/accent_purple"
|
||||
android:textSize="12sp" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
@ -78,7 +78,7 @@
|
||||
android:layout_height="46dp"
|
||||
android:layout_marginTop="6dp"
|
||||
android:background="@drawable/bg_search_input"
|
||||
android:hint="Ej. Juan Carlos Pérez"
|
||||
android:hint="Ej. Juan Gómez"
|
||||
android:inputType="textPersonName"
|
||||
android:paddingHorizontal="14dp"
|
||||
android:textColor="@color/text_primary"
|
||||
@ -319,7 +319,7 @@
|
||||
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:text="Tu número fue verificado con éxito y tu pedido ya está registrado en nuestra plataforma.\n\nLa tienda ha sido notificada automáticamente y recibirás confirmación oficial a tu WhatsApp."
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="12sp" />
|
||||
|
||||
@ -440,19 +440,19 @@
|
||||
app:iconSize="18dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnOpenWhatsAppChat"
|
||||
android:id="@+id/btnViewOrderDetails"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="50dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:filterTouchesWhenObscured="true"
|
||||
android:text="Notificar Pago por WhatsApp"
|
||||
android:text="Ver Estado de Mi Pedido"
|
||||
android:textAllCaps="false"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textSize="13sp"
|
||||
android:textStyle="bold"
|
||||
app:backgroundTint="@color/accent_whatsapp"
|
||||
app:backgroundTint="@color/drip_purple"
|
||||
app:cornerRadius="14dp"
|
||||
app:icon="@drawable/ic_whatsapp"
|
||||
app:icon="@drawable/ic_package"
|
||||
app:iconGravity="textStart"
|
||||
app:iconPadding="8dp"
|
||||
app:iconSize="18dp" />
|
||||
|
||||
@ -0,0 +1,148 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout 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: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"
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="sans-serif-black"
|
||||
android:text="📦 Mis Pedidos"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="20sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvOrdersSubtitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:text="Historial y estado de entrega en tiempo real"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="13sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnCloseOrders"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/ic_close"
|
||||
app:tint="@color/text_secondary" />
|
||||
</RelativeLayout>
|
||||
|
||||
<!-- User indicator chip -->
|
||||
<TextView
|
||||
android:id="@+id/tvOrdersUserPhone"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:background="@drawable/bg_category_chip"
|
||||
android:drawableStart="@drawable/ic_whatsapp"
|
||||
android:drawablePadding="6dp"
|
||||
android:drawableTint="@color/accent_whatsapp"
|
||||
android:paddingHorizontal="12dp"
|
||||
android:paddingVertical="6dp"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="12sp"
|
||||
tools:text="Consultando para: +57 310 639 0296" />
|
||||
|
||||
<!-- Loading -->
|
||||
<ProgressBar
|
||||
android:id="@+id/pbOrdersLoading"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginVertical="32dp"
|
||||
android:indeterminateTint="@color/accent_purple"
|
||||
android:visibility="visible" />
|
||||
|
||||
<!-- Empty State -->
|
||||
<LinearLayout
|
||||
android:id="@+id/layoutEmptyOrders"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:paddingVertical="32dp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="gone">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="64dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:src="@drawable/ic_package"
|
||||
app:tint="@color/border_subtle" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="sans-serif-black"
|
||||
android:text="No encontramos pedidos aún"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvEmptyOrdersMessage"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:gravity="center"
|
||||
android:paddingHorizontal="24dp"
|
||||
android:text="Tus órdenes registradas por la app web o móvil aparecerán aquí con su estado de alistamiento y flete."
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="13sp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnExploreCatalog"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="44dp"
|
||||
android:layout_marginTop="20dp"
|
||||
android:fontFamily="sans-serif-bold"
|
||||
android:text="Explorar Catálogo 🔥"
|
||||
android:textSize="13sp"
|
||||
app:backgroundTint="@color/accent_purple"
|
||||
app:cornerRadius="10dp" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- RecyclerView for Orders -->
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rvOrders"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipToPadding="false"
|
||||
android:nestedScrollingEnabled="false"
|
||||
android:visibility="gone"
|
||||
tools:itemCount="2"
|
||||
tools:listitem="@layout/item_order_card"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</LinearLayout>
|
||||
@ -170,6 +170,27 @@
|
||||
app:iconPadding="8dp"
|
||||
app:iconSize="20dp" />
|
||||
|
||||
<!-- Pedir o Consultar por WhatsApp Button -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnOrderWhatsAppDirect"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:filterTouchesWhenObscured="true"
|
||||
android:text="Pedir o Consultar por WhatsApp"
|
||||
android:textAllCaps="false"
|
||||
android:textColor="#22C55E"
|
||||
android:textSize="13sp"
|
||||
app:cornerRadius="14dp"
|
||||
app:icon="@drawable/ic_whatsapp"
|
||||
app:iconGravity="textStart"
|
||||
app:iconPadding="8dp"
|
||||
app:iconSize="18dp"
|
||||
app:iconTint="#22C55E"
|
||||
app:strokeColor="#22C55E"
|
||||
app:strokeWidth="1dp" />
|
||||
|
||||
<!-- Secondary Custom Request Button -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnCustomOrderSheet"
|
||||
|
||||
@ -74,7 +74,7 @@
|
||||
android:layout_height="46dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:background="@drawable/bg_search_input"
|
||||
android:hint="Ej. Carlos Mendoza"
|
||||
android:hint="Ej. Andrés Morales"
|
||||
android:inputType="textPersonName"
|
||||
android:paddingHorizontal="14dp"
|
||||
android:textColor="@color/text_primary"
|
||||
@ -326,7 +326,7 @@
|
||||
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:text="Hemos validado tu número de WhatsApp y registrado tu solicitud en la plataforma.\n\nLa tienda ha sido notificada y te contactaremos por WhatsApp con la cotización exacta."
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="12sp" />
|
||||
|
||||
@ -336,7 +336,7 @@
|
||||
android:layout_height="50dp"
|
||||
android:layout_marginTop="18dp"
|
||||
android:filterTouchesWhenObscured="true"
|
||||
android:text="Abrir WhatsApp del Asesor Carlos"
|
||||
android:text="Contactar a Soporte DripZone"
|
||||
android:textAllCaps="false"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textSize="13sp"
|
||||
|
||||
302
dripzone-app/android/app/src/main/res/layout/item_order_card.xml
Normal file
302
dripzone-app/android/app/src/main/res/layout/item_order_card.xml
Normal file
@ -0,0 +1,302 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.card.MaterialCardView 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:layout_marginBottom="16dp"
|
||||
app:cardBackgroundColor="@color/bg_card"
|
||||
app:cardCornerRadius="16dp"
|
||||
app:cardElevation="0dp"
|
||||
app:strokeColor="@color/border_subtle"
|
||||
app:strokeWidth="1dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<!-- 1. Header: Order ID, Date, Status Badge -->
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvCardOrderId"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="sans-serif-black"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="15sp"
|
||||
tools:text="#ORD-88421" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvCardOrderDate"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="12sp"
|
||||
tools:text="05 Sep 2026, 04:30 PM" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvCardOrderStatusBadge"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:background="@drawable/bg_category_chip_active"
|
||||
android:fontFamily="sans-serif-bold"
|
||||
android:paddingHorizontal="10dp"
|
||||
android:paddingVertical="4dp"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="11sp"
|
||||
tools:text="⚡ Anticipo Acreditado" />
|
||||
</RelativeLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:background="#1AFFFFFF" />
|
||||
|
||||
<!-- 2. Products Container (Dynamic list of items) -->
|
||||
<LinearLayout
|
||||
android:id="@+id/llOrderItemsContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginVertical="10dp"
|
||||
android:background="#1AFFFFFF" />
|
||||
|
||||
<!-- 3. Financial Breakdown -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="4dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Subtotal:"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="13sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvCardSubtotal"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="13sp"
|
||||
tools:text="$125.000 COP" />
|
||||
</RelativeLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="6dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Envío a Domicilio:"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="13sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvCardShippingCost"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:textColor="@color/accent_purple"
|
||||
android:textSize="13sp"
|
||||
tools:text="GRATIS" />
|
||||
</RelativeLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="10dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="sans-serif-bold"
|
||||
android:text="Total del Pedido:"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvCardTotal"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:fontFamily="sans-serif-black"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="15sp"
|
||||
tools:text="$125.000 COP" />
|
||||
</RelativeLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 4. Deposit Card / Bold Box -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/cardDepositBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="10dp"
|
||||
app:cardBackgroundColor="#1AFFFFFF"
|
||||
app:cardCornerRadius="10dp"
|
||||
app:cardElevation="0dp"
|
||||
app:strokeColor="#26FFFFFF"
|
||||
app:strokeWidth="1dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="10dp">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvDepositTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:text="⚡ Anticipo / Separación:"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="12sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvDepositStatusLabel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:fontFamily="sans-serif-bold"
|
||||
android:textColor="@color/accent_whatsapp"
|
||||
android:textSize="12sp"
|
||||
tools:text="$10.000 COP • Acreditado ✅" />
|
||||
</RelativeLayout>
|
||||
|
||||
<!-- Button to pay deposit in Bold if pending -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnPayDepositBold"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:fontFamily="sans-serif-bold"
|
||||
android:text="💳 Pagar Anticipo ($10.000) en Bold"
|
||||
android:textSize="12sp"
|
||||
android:visibility="gone"
|
||||
app:backgroundTint="@color/accent_purple"
|
||||
app:cornerRadius="8dp"
|
||||
tools:visibility="visible" />
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<!-- 5. Saldo Contraentrega Highlight Banner -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="10dp"
|
||||
app:cardBackgroundColor="#268B5CF6"
|
||||
app:cardCornerRadius="10dp"
|
||||
app:cardElevation="0dp"
|
||||
app:strokeColor="#4D8B5CF6"
|
||||
app:strokeWidth="1dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:padding="12dp">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="22dp"
|
||||
android:layout_height="22dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:src="@drawable/ic_package"
|
||||
app:tint="@color/accent_purple" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:text="Saldo a Pagar Contraentrega en Efectivo:"
|
||||
android:textColor="#D8B4FE"
|
||||
android:textSize="11sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvCardBalanceDue"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="sans-serif-black"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textSize="16sp"
|
||||
tools:text="$115.000 COP" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<!-- 6. Delivery Address -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvCardAddress"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="12sp"
|
||||
tools:text="📍 Entrega en: Calle 5 # 24-80 (Cali)" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 7. Action Button: WhatsApp inquiry -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnOrderWhatsAppStatus"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="44dp"
|
||||
android:fontFamily="sans-serif-bold"
|
||||
android:text="Consultar Estado en WhatsApp"
|
||||
android:textColor="@color/accent_whatsapp"
|
||||
android:textSize="13sp"
|
||||
app:cornerRadius="10dp"
|
||||
app:icon="@drawable/ic_whatsapp"
|
||||
app:iconGravity="textStart"
|
||||
app:iconTint="@color/accent_whatsapp"
|
||||
app:strokeColor="@color/accent_whatsapp" />
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
@ -94,44 +94,39 @@
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="11sp" />
|
||||
|
||||
<!-- Price & Action Button -->
|
||||
<LinearLayout
|
||||
<!-- Price (Full Width in single line, no clipping) -->
|
||||
<TextView
|
||||
android:id="@+id/tvProductPrice"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
android:fontFamily="sans-serif-black"
|
||||
android:maxLines="1"
|
||||
android:singleLine="true"
|
||||
android:text="$ 280.000"
|
||||
android:textColor="@color/drip_purple_glow"
|
||||
android:textSize="15sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvProductPrice"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:fontFamily="sans-serif-black"
|
||||
android:text="$ 280.000"
|
||||
android:textColor="@color/drip_purple_glow"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<!-- Quick Order Button with Tapjacking Protection -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnQuickOrder"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="36dp"
|
||||
android:filterTouchesWhenObscured="true"
|
||||
android:insetTop="0dp"
|
||||
android:insetBottom="0dp"
|
||||
android:paddingHorizontal="10dp"
|
||||
android:text="Pedir"
|
||||
android:textAllCaps="false"
|
||||
android:textSize="11sp"
|
||||
android:textStyle="bold"
|
||||
app:backgroundTint="@color/accent_whatsapp"
|
||||
app:cornerRadius="10dp"
|
||||
app:icon="@drawable/ic_whatsapp"
|
||||
app:iconGravity="textStart"
|
||||
app:iconPadding="4dp"
|
||||
app:iconSize="14dp" />
|
||||
</LinearLayout>
|
||||
<!-- Quick Order Button directly below price -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnQuickOrder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="38dp"
|
||||
android:layout_marginTop="6dp"
|
||||
android:filterTouchesWhenObscured="true"
|
||||
android:insetTop="0dp"
|
||||
android:insetBottom="0dp"
|
||||
android:paddingHorizontal="8dp"
|
||||
android:text="Pedir"
|
||||
android:textAllCaps="false"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold"
|
||||
app:backgroundTint="@color/accent_whatsapp"
|
||||
app:cornerRadius="10dp"
|
||||
app:icon="@drawable/ic_whatsapp"
|
||||
app:iconGravity="textStart"
|
||||
app:iconPadding="6dp"
|
||||
app:iconSize="15dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
@ -15,6 +15,8 @@
|
||||
|
||||
<!-- Web DripZone Purple & Fuchsia Accents -->
|
||||
<color name="drip_purple">#9333EA</color>
|
||||
<color name="accent_purple">#9333EA</color>
|
||||
<color name="border_subtle">#282830</color>
|
||||
<color name="drip_purple_light">#A855F7</color>
|
||||
<color name="drip_fuchsia">#D946EF</color>
|
||||
<color name="drip_pink">#EC4899</color>
|
||||
|
||||
215
dripzone-app/scripts/seed-templates-appwrite.js
Normal file
215
dripzone-app/scripts/seed-templates-appwrite.js
Normal file
@ -0,0 +1,215 @@
|
||||
const { Client, Databases } = require('node-appwrite');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const endpoint = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://appwrite.almeira.com.co/v1';
|
||||
const projectId = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '69b84df5003bd8957c5c';
|
||||
const apiKey = process.env.APPWRITE_API_KEY || 'standard_cce3cdb866ea6433888d40cedd24dc45987372308fdc3dd0697de08648c0e5c7e04b02c3bcee271403fd656b00a5820f04c08b51631284a6e8a3582d8aed2aa33234f3c29cc777e9b162eb54810b789251b0d006a9a3b081ebc2da53b9baa94614b124b3508ad849fd9a4187cc6094f506d4b57f409b69d9562a74df594f3ec3';
|
||||
const databaseId = process.env.NEXT_PUBLIC_APPWRITE_DATABASE_ID || 'sneakers_multistore_db';
|
||||
|
||||
const client = new Client()
|
||||
.setEndpoint(endpoint)
|
||||
.setProject(projectId)
|
||||
.setKey(apiKey);
|
||||
|
||||
const databases = new Databases(client);
|
||||
|
||||
// Import templates from compiled or read ts file directly
|
||||
async function seed() {
|
||||
console.log('🌱 Seeding message templates into Appwrite...');
|
||||
|
||||
// Read templates-service.ts to extract default templates
|
||||
const templateServiceFile = fs.readFileSync(path.resolve(__dirname, '../src/lib/templates-service.ts'), 'utf-8');
|
||||
|
||||
// Define templates matching DEFAULT_MESSAGE_TEMPLATES
|
||||
const templates = [
|
||||
{
|
||||
id: 'order_confirmation_customer',
|
||||
title: 'Confirmación de Pedido al Cliente',
|
||||
description: 'Mensaje oficial enviado al cliente tras registrar exitosamente su pedido en la tienda.',
|
||||
category: 'customer_sales',
|
||||
channel: 'whatsapp',
|
||||
isActive: true,
|
||||
content: `🎉 *¡GRACIAS POR TU PEDIDO EN {tienda_nombre}!* 🎉\n📌 *No. de Orden:* #{orden_id}\n\n👤 *Cliente:* {cliente_nombre}\n📱 *Teléfono:* {cliente_telefono}\n📍 *Dirección:* {cliente_direccion}\n🏙️ *Ciudad:* {cliente_ciudad}\n\n📦 *DETALLE DE TU COMPRA:*\n{articulos_resumen}\n\n💵 *Subtotal:* {subtotal}\n🚚 *Envío:* {costo_envio}\n💰 *VALOR TOTAL:* {total}\n{condicion_pago_resumen}\n\n💳 *Método de Pago:* {metodo_pago}\n\nEstamos alistando tu paquete. Te enviaremos el número de guía o notificación de entrega. ¡Muchas gracias por tu confianza! ✨`,
|
||||
availableVariables: JSON.stringify([
|
||||
{ key: 'tienda_nombre', label: 'Nombre de Tienda', example: 'DripZone', description: 'Nombre comercial' },
|
||||
{ key: 'orden_id', label: 'No. de Orden', example: 'ORD-9842', description: 'ID orden' },
|
||||
{ key: 'cliente_nombre', label: 'Nombre del Cliente', example: 'Cliente DripZone', description: 'Nombre' },
|
||||
{ key: 'cliente_telefono', label: 'Teléfono Cliente', example: '3106390296', description: 'Celular' },
|
||||
{ key: 'cliente_direccion', label: 'Dirección de Entrega', example: 'Calle 5 # 24-80', description: 'Dirección' },
|
||||
{ key: 'cliente_ciudad', label: 'Ciudad Destino', example: 'Cali', description: 'Ciudad' },
|
||||
{ key: 'articulos_resumen', label: 'Lista de Artículos', example: '👟 Nike Air Force 1 x1', description: 'Pares' },
|
||||
{ key: 'subtotal', label: 'Subtotal COP', example: '$125.000 COP', description: 'Subtotal' },
|
||||
{ key: 'costo_envio', label: 'Costo de Envío', example: 'GRATIS', description: 'Envío' },
|
||||
{ key: 'total', label: 'Total COP', example: '$125.000 COP', description: 'Total' },
|
||||
{ key: 'condicion_pago_resumen', label: 'Resumen Anticipo / Saldo', example: '🤝 Pago 100% Contraentrega', description: 'Condición' },
|
||||
{ key: 'metodo_pago', label: 'Método de Pago', example: 'Contraentrega', description: 'Método' },
|
||||
{ key: 'saldo_contraentrega', label: 'Saldo en Puerta', example: '$115.000 COP', description: 'Saldo' },
|
||||
{ key: 'anticipo', label: 'Valor Separación', example: '$10.000 COP', description: 'Anticipo' }
|
||||
]),
|
||||
updatedAt: new Date().toISOString()
|
||||
},
|
||||
{
|
||||
id: 'order_checkout_wa',
|
||||
title: 'Pedido Web hacia WhatsApp de Tienda',
|
||||
description: 'Texto que el cliente abre en WhatsApp al finalizar su compra en el carrito o checkout web.',
|
||||
category: 'customer_sales',
|
||||
channel: 'whatsapp',
|
||||
isActive: true,
|
||||
content: `🎉 *CONFIRMACIÓN DE PEDIDO EN {tienda_nombre} #{orden_id}* 🎉\n\n👤 *Cliente:* {cliente_nombre}\n📱 *Teléfono:* {cliente_telefono}\n📍 *Dirección:* {cliente_direccion}, {cliente_ciudad}\n\n📦 *PRODUCTOS SOLICITADOS:*\n{articulos_resumen}\n\n💵 *Subtotal:* {subtotal}\n🚚 *Flete:* {costo_envio}\n💰 *TOTAL A PAGAR:* {total}\n{condicion_pago_resumen}\n💳 *Forma de Pago:* {metodo_pago}\n\n¡Por favor confirmar mi pedido para proceder con el alistamiento! 🔥`,
|
||||
availableVariables: JSON.stringify([
|
||||
{ key: 'tienda_nombre', label: 'Nombre de Tienda', example: 'DripZone', description: 'Nombre de la marca' },
|
||||
{ key: 'orden_id', label: 'No. Orden', example: 'ORD-9842', description: 'ID de la orden' },
|
||||
{ key: 'cliente_nombre', label: 'Nombre Cliente', example: 'Cliente DripZone', description: 'Nombre' },
|
||||
{ key: 'cliente_telefono', label: 'Teléfono', example: '3106390296', description: 'Teléfono' },
|
||||
{ key: 'cliente_direccion', label: 'Dirección', example: 'Av 6N # 28N-12', description: 'Dirección' },
|
||||
{ key: 'cliente_ciudad', label: 'Ciudad', example: 'Cali', description: 'Ciudad' },
|
||||
{ key: 'articulos_resumen', label: 'Productos', example: '👟 Jordan 4 Retro (41) x1', description: 'Resumen' },
|
||||
{ key: 'subtotal', label: 'Subtotal', example: '$140.000 COP', description: 'Subtotal' },
|
||||
{ key: 'costo_envio', label: 'Envío', example: 'GRATIS', description: 'Flete' },
|
||||
{ key: 'total', label: 'Total', example: '$140.000 COP', description: 'Total' },
|
||||
{ key: 'condicion_pago_resumen', label: 'Condición Pago', example: 'Separación $10.000', description: 'Condición' },
|
||||
{ key: 'metodo_pago', label: 'Método', example: 'Contraentrega', description: 'Método' }
|
||||
]),
|
||||
updatedAt: new Date().toISOString()
|
||||
},
|
||||
{
|
||||
id: 'order_status_dispatched',
|
||||
title: 'Notificación de Pedido Despachado en Ruta',
|
||||
description: 'Mensaje enviado cuando la transportadora o mensajero local recoge el paquete.',
|
||||
category: 'logistics_shipping',
|
||||
channel: 'whatsapp',
|
||||
isActive: true,
|
||||
content: `🚚 *¡TU PEDIDO VA EN CAMINO!* 📦💨\n\nHola {cliente_nombre}, tus zapatillas ya fueron despachadas para entrega en *{cliente_ciudad}*.\n\n📌 *Orden:* #{orden_id}\n📍 *Dirección de Entrega:* {cliente_direccion}\n📦 *Empresa Transportadora:* {transportadora}\n🏷️ *Número de Guía / Rastreo:* *{numero_guia}*\n🤝 *Valor a Pagar al Recibir (Contraentrega):* *{saldo_contraentrega}*\n\nPor favor estar atento a la portería o celular. ¡Gracias por tu compra en {tienda_nombre}! 🔥`,
|
||||
availableVariables: JSON.stringify([
|
||||
{ key: 'cliente_nombre', label: 'Nombre Cliente', example: 'Cliente DripZone', description: 'Nombre' },
|
||||
{ key: 'orden_id', label: 'No. Orden', example: 'ORD-9842', description: 'ID de la orden' },
|
||||
{ key: 'cliente_direccion', label: 'Dirección', example: 'Carrera 100 # 16-20', description: 'Dirección' },
|
||||
{ key: 'cliente_ciudad', label: 'Ciudad', example: 'Cali', description: 'Ciudad' },
|
||||
{ key: 'transportadora', label: 'Transportadora', example: 'Interrapidísimo / Mensajería Urbana', description: 'Operador' },
|
||||
{ key: 'numero_guia', label: 'Número de Guía', example: 'GUIA-8839201', description: 'Guía' },
|
||||
{ key: 'saldo_contraentrega', label: 'Saldo a Pagar', example: '$125.000 COP', description: 'Saldo contraentrega' },
|
||||
{ key: 'tienda_nombre', label: 'Nombre Tienda', example: 'DripZone', description: 'Marca' }
|
||||
]),
|
||||
updatedAt: new Date().toISOString()
|
||||
},
|
||||
{
|
||||
id: 'order_status_delivered',
|
||||
title: 'Confirmación de Pedido Entregado con Éxito',
|
||||
description: 'Mensaje post-venta enviado cuando el mensajero confirma entrega exitosa.',
|
||||
category: 'logistics_shipping',
|
||||
channel: 'whatsapp',
|
||||
isActive: true,
|
||||
content: `⭐ *¡PEDIDO ENTREGADO CON ÉXITO!* ⭐\n\nHola {cliente_nombre}, nuestro sistema confirma que tu orden *#{orden_id}* fue entregada exitosamente.\n\nEsperamos que disfrutes al máximo tus nuevas zapatillas. Recuerda que cuentas con garantía por defectos y cambio de talla.\n\n📸 ¡Sube tu foto a Instagram y etiquétanos en *{instagram_handle}* para un 10% de descuento en tu próxima compra!\n\n¡Muchas gracias por elegir {tienda_nombre}! ✨`,
|
||||
availableVariables: JSON.stringify([
|
||||
{ key: 'cliente_nombre', label: 'Nombre Cliente', example: 'Cliente DripZone', description: 'Nombre' },
|
||||
{ key: 'orden_id', label: 'No. Orden', example: 'ORD-9842', description: 'ID orden' },
|
||||
{ key: 'tienda_nombre', label: 'Nombre Tienda', example: 'DripZone', description: 'Marca' },
|
||||
{ key: 'instagram_handle', label: 'Instagram', example: '@dripzone.co', description: 'Cuenta IG' }
|
||||
]),
|
||||
updatedAt: new Date().toISOString()
|
||||
},
|
||||
{
|
||||
id: 'auth_login_otp',
|
||||
title: 'Código OTP de Seguridad para Iniciar Sesión',
|
||||
description: 'Código de 6 dígitos enviado por WhatsApp al usuario para autenticación sin contraseña.',
|
||||
category: 'security_auth',
|
||||
channel: 'whatsapp',
|
||||
isActive: true,
|
||||
content: `🔐 *{tienda_nombre} - Código de Seguridad*\n\nHola {usuario_nombre}, tu código de verificación para acceder a tu cuenta es:\n\n👉 *{codigo_otp}*\n\n⏱️ _Válido por 5 minutos. No compartas este código con nadie._`,
|
||||
availableVariables: JSON.stringify([
|
||||
{ key: 'tienda_nombre', label: 'Nombre Tienda', example: 'DripZone', description: 'Marca' },
|
||||
{ key: 'usuario_nombre', label: 'Nombre Usuario', example: 'Cliente DripZone', description: 'Usuario' },
|
||||
{ key: 'codigo_otp', label: 'Código de 6 Dígitos', example: '849201', description: 'Código OTP' }
|
||||
]),
|
||||
updatedAt: new Date().toISOString()
|
||||
},
|
||||
{
|
||||
id: 'auth_register_otp',
|
||||
title: 'Código OTP para Verificación de Nueva Cuenta',
|
||||
description: 'Código enviado por WhatsApp al nuevo cliente tras registrar su cuenta en la plataforma.',
|
||||
category: 'security_auth',
|
||||
channel: 'whatsapp',
|
||||
isActive: true,
|
||||
content: `👟 *¡Bienvenido(a) a {tienda_nombre}!* ✨\n\nHola {usuario_nombre}, ingresa el siguiente código en la app para verificar tu número de WhatsApp:\n\n👉 *{codigo_otp}*\n\nAl validar tu cuenta, habilitas compras 100% contraentrega sin fricción. ¡Gracias por unirte! 🔥`,
|
||||
availableVariables: JSON.stringify([
|
||||
{ key: 'tienda_nombre', label: 'Nombre Tienda', example: 'DripZone', description: 'Marca' },
|
||||
{ key: 'usuario_nombre', label: 'Nombre Usuario', example: 'Cliente DripZone', description: 'Nombre' },
|
||||
{ key: 'codigo_otp', label: 'Código de 6 Dígitos', example: '582194', description: 'Código OTP' }
|
||||
]),
|
||||
updatedAt: new Date().toISOString()
|
||||
},
|
||||
{
|
||||
id: 'customer_deposit_request',
|
||||
title: 'Solicitud de Separación con $10.000 COP (Garantía de Despacho)',
|
||||
description: 'Mensaje con enlace oficial Bold para pagar el anticipo de $10.000 COP y programar el envío.',
|
||||
category: 'customer_sales',
|
||||
channel: 'whatsapp',
|
||||
isActive: true,
|
||||
content: `⚡ *{tienda_nombre} - CONFIRMACIÓN DE DESPACHO* 📦\n\nHola {cliente_nombre}, para programar el despacho express de tu orden *#{orden_id}* hacia *{cliente_ciudad}*, realiza el pago del anticipo de separación de *{valor_anticipo}*.\n\n💳 *Paga en línea de forma 100% segura por Bold (PSE / Tarjetas / Bre-B / Nequi):*\n👉 {link_pago_bold}\n\n🤝 El saldo restante de *{saldo_contraentrega}* lo cancelas al recibir tu paquete en tu puerta. ¡Gracias por tu compra!`,
|
||||
availableVariables: JSON.stringify([
|
||||
{ key: 'tienda_nombre', label: 'Nombre Tienda', example: 'DripZone', description: 'Tienda' },
|
||||
{ key: 'cliente_nombre', label: 'Nombre Cliente', example: 'Cliente DripZone', description: 'Cliente' },
|
||||
{ key: 'orden_id', label: 'No. Orden', example: 'ORD-9842', description: 'ID orden' },
|
||||
{ key: 'cliente_ciudad', label: 'Ciudad', example: 'Cali', description: 'Ciudad' },
|
||||
{ key: 'valor_anticipo', label: 'Valor Anticipo', example: '$10.000 COP', description: 'Separación' },
|
||||
{ key: 'saldo_contraentrega', label: 'Saldo Contraentrega', example: '$125.000 COP', description: 'Saldo' },
|
||||
{ key: 'link_pago_bold', label: 'Link de Pago Bold', example: 'https://drip-zone.online', description: 'Enlace Bold' }
|
||||
]),
|
||||
updatedAt: new Date().toISOString()
|
||||
},
|
||||
{
|
||||
id: 'customer_deposit_confirmed',
|
||||
title: 'Confirmación de Anticipo Bold al Cliente',
|
||||
description: 'Notificación enviada al cliente por WhatsApp cuando su pago de anticipo de separación es acreditado por Bold.',
|
||||
category: 'customer_sales',
|
||||
channel: 'whatsapp',
|
||||
isActive: true,
|
||||
content: `🎉 *¡Tu anticipo de separación fue confirmado con éxito!*\n\nHola *{cliente_nombre}*, recibimos correctamente tu pago de *{valor_anticipo}* a través de Bold.\n\n📦 Tu orden *#{orden_id}* ya se encuentra en alistamiento y empaque para despacho a *{cliente_ciudad}*.\n🤝 *Saldo restante a pagar contraentrega:* *{saldo_contraentrega}* al recibir en tu puerta.\n\nPuedes consultar el estado de tu pedido en cualquier momento desde la app oficial de {tienda_nombre}.\n\n¡Muchas gracias por tu compra en {tienda_nombre}!`,
|
||||
availableVariables: JSON.stringify([
|
||||
{ key: 'tienda_nombre', label: 'Nombre Tienda', example: 'DripZone', description: 'Nombre de la tienda' },
|
||||
{ key: 'cliente_nombre', label: 'Nombre Cliente', example: 'Cliente DripZone', description: 'Nombre del comprador' },
|
||||
{ key: 'orden_id', label: 'No. de Orden', example: 'ORD-9842', description: 'ID de la orden' },
|
||||
{ key: 'valor_anticipo', label: 'Valor Anticipo', example: '$10.000 COP', description: 'Monto anticipo pagado' },
|
||||
{ key: 'cliente_ciudad', label: 'Ciudad Destino', example: 'Cali', description: 'Municipio de entrega' },
|
||||
{ key: 'saldo_contraentrega', label: 'Saldo Contraentrega', example: '$115.000 COP', description: 'Saldo en puerta' }
|
||||
]),
|
||||
updatedAt: new Date().toISOString()
|
||||
}
|
||||
];
|
||||
|
||||
for (const t of templates) {
|
||||
try {
|
||||
const doc = await databases.getDocument(databaseId, 'message_templates', t.id);
|
||||
console.log(`ℹ️ Plantilla ${t.id} ya existe, actualizando...`);
|
||||
await databases.updateDocument(databaseId, 'message_templates', t.id, {
|
||||
title: t.title,
|
||||
description: t.description,
|
||||
category: t.category,
|
||||
channel: t.channel,
|
||||
content: t.content,
|
||||
availableVariables: t.availableVariables,
|
||||
isActive: t.isActive,
|
||||
updatedAt: t.updatedAt
|
||||
});
|
||||
console.log(`✅ Plantilla ${t.id} actualizada.`);
|
||||
} catch (e) {
|
||||
console.log(`Creando plantilla ${t.id}...`);
|
||||
await databases.createDocument(databaseId, 'message_templates', t.id, {
|
||||
title: t.title,
|
||||
description: t.description,
|
||||
category: t.category,
|
||||
channel: t.channel,
|
||||
content: t.content,
|
||||
availableVariables: t.availableVariables,
|
||||
isActive: t.isActive,
|
||||
updatedAt: t.updatedAt
|
||||
});
|
||||
console.log(`✅ Plantilla ${t.id} creada en Appwrite.`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('🎉 ¡Todas las plantillas quedaron sincronizadas en Appwrite!');
|
||||
}
|
||||
|
||||
seed().catch(err => console.error('Error seeding templates:', err));
|
||||
@ -14,10 +14,10 @@ export default function AdminSettingsPage() {
|
||||
instagramHandle: '@dripzone.co',
|
||||
address: 'Bodega Central / Cali',
|
||||
city: 'Cali, Colombia',
|
||||
nequiAccount: '316 745 5877',
|
||||
daviplataAccount: '316 745 5877',
|
||||
accountHolder: 'Carlos Andrés Lasso',
|
||||
boldPaymentLink: 'https://bold.co/p/carlos-lasso',
|
||||
nequiAccount: '310 639 0296',
|
||||
daviplataAccount: '310 639 0296',
|
||||
accountHolder: 'DripZone Oficial',
|
||||
boldPaymentLink: 'https://drip-zone.online',
|
||||
boldApiKey: '',
|
||||
boldMerchantId: '',
|
||||
boldQrUrl: '',
|
||||
|
||||
@ -11,22 +11,35 @@ export async function POST(request: Request) {
|
||||
}
|
||||
|
||||
const cleanInput = identifier.trim();
|
||||
// Buscar usuario en base de datos
|
||||
const user = dataService.authenticate(cleanInput);
|
||||
const cleanDigits = cleanInput.replace(/\D/g, '');
|
||||
|
||||
let phoneToSend = cleanInput.replace(/\D/g, '');
|
||||
let userName = name?.trim() || (user ? user.name : 'Cliente');
|
||||
let phoneToSend = '';
|
||||
let userName = name?.trim() || 'Cliente';
|
||||
let foundUser = null;
|
||||
|
||||
if (user) {
|
||||
if (!user.isActive) {
|
||||
// Si el identificador es un correo electrónico, buscar el teléfono de su cuenta
|
||||
if (cleanInput.includes('@')) {
|
||||
foundUser = dataService.authenticate(cleanInput);
|
||||
if (!foundUser) {
|
||||
return NextResponse.json({ success: false, error: 'No se encontró cuenta asociada a este correo.' }, { status: 404 });
|
||||
}
|
||||
if (!foundUser.isActive) {
|
||||
return NextResponse.json({ success: false, error: 'Esta cuenta se encuentra inactiva.' }, { status: 403 });
|
||||
}
|
||||
phoneToSend = user.phone.replace(/\D/g, '');
|
||||
phoneToSend = foundUser.phone.replace(/\D/g, '');
|
||||
userName = foundUser.name || userName;
|
||||
} else {
|
||||
// Si es un número telefónico, enviar DIRECTA Y EXCLUSIVAMENTE a ese número ingresado por el usuario
|
||||
phoneToSend = cleanDigits;
|
||||
foundUser = dataService.authenticate(cleanDigits);
|
||||
if (foundUser?.name) {
|
||||
userName = foundUser.name;
|
||||
}
|
||||
}
|
||||
|
||||
if (!phoneToSend || phoneToSend.length < 7) {
|
||||
if (!phoneToSend || phoneToSend.length < 10) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Número de celular inválido. Ingresa un número válido de WhatsApp.' },
|
||||
{ success: false, error: 'Número de celular inválido. Ingresa tu número de WhatsApp a 10 dígitos.' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
@ -42,16 +55,14 @@ export async function POST(request: Request) {
|
||||
codigo_otp: code,
|
||||
});
|
||||
|
||||
// Enviar a WhatsApp via Evolution API
|
||||
// Enviar EXCLUSIVAMENTE a la línea de WhatsApp del usuario
|
||||
const sent = await sendWhatsAppMessage(phoneToSend, message);
|
||||
|
||||
|
||||
if (!sent) {
|
||||
// Si falla Evolution (o es teléfono internacional sin formato), permitir fallback seguro
|
||||
console.warn(`[OTP] No se pudo enviar por WhatsApp a ${phoneToSend}. Código generado: ${code}`);
|
||||
}
|
||||
|
||||
// Enmascarar teléfono para la respuesta pública (ej: 316***5877)
|
||||
// Enmascarar teléfono para la respuesta pública (ej: 310***0296)
|
||||
const maskedPhone = phoneToSend.length >= 10
|
||||
? `${phoneToSend.slice(0, 3)}***${phoneToSend.slice(-4)}`
|
||||
: 'tu número';
|
||||
@ -60,7 +71,7 @@ export async function POST(request: Request) {
|
||||
success: true,
|
||||
message: `Código de 6 dígitos enviado por WhatsApp a ${maskedPhone}`,
|
||||
phone: maskedPhone,
|
||||
isRegistered: !!user,
|
||||
isRegistered: !!foundUser,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Error enviando OTP:', error);
|
||||
|
||||
@ -51,28 +51,36 @@ export async function GET(request: Request) {
|
||||
dataService.updateOrder(order.id, updates);
|
||||
await updateOrderInAppwrite(order.id, updates).catch(e => console.warn('Appwrite update warn:', e));
|
||||
|
||||
// Notificar a Carlos por WhatsApp
|
||||
// Notificar a la tienda automáticamente con la información completa del pago
|
||||
try {
|
||||
const itemsText = (order.items || [])
|
||||
.map((i: any) => ` 👟 ${i.productName || i.name} (Talla ${i.size}) x${i.quantity} — ${formatCOP((i.price || 0) * (i.quantity || 1))}`)
|
||||
.join('\n');
|
||||
|
||||
const adminMsg = (
|
||||
`⚡ *¡ANTICIPO DE SEPARACIÓN CONFIRMADO VÍA BOLD!*\n\n` +
|
||||
`📦 *Orden:* #${order.id}\n` +
|
||||
`👤 *Cliente:* ${order.customerName} (+57 ${order.customerPhone})\n` +
|
||||
`📍 *Ciudad:* ${order.cityName}\n` +
|
||||
`💳 *Anticipo Recibido:* ${formatCOP(depositAmount)} COP (Confirmado Bold)\n` +
|
||||
`🤝 *Saldo a Cobrar Contraentrega:* ${formatCOP(newBalance)} COP\n\n` +
|
||||
`📍 *Destino:* ${order.cityName} — ${order.address || 'Domicilio'}\n` +
|
||||
`💳 *Anticipo Recibido:* ${formatCOP(depositAmount)} COP (Acreditado por Bold)\n` +
|
||||
`🔗 *Referencia Bold:* ${effectiveLinkId}\n` +
|
||||
`💵 *Total Pedido:* ${formatCOP(order.totalAmount)} COP\n` +
|
||||
`🤝 *Saldo a Cobrar Contraentrega:* *${formatCOP(newBalance)} COP*\n\n` +
|
||||
`📋 *Detalle de Calzado:*\n${itemsText || ' (Ver orden en panel)'}\n\n` +
|
||||
`🚀 *Estado:* Despacho autorizado y habilitado para alistamiento en bodega.`
|
||||
);
|
||||
await sendWhatsAppMessage('573167455877', adminMsg);
|
||||
|
||||
// Notificar al cliente
|
||||
// Notificar al cliente automáticamente con la plantilla registrada en la base de datos
|
||||
if (order.customerPhone) {
|
||||
const clientMsg = (
|
||||
`🎉 *¡Tu anticipo de separación fue confirmado con éxito!*\n\n` +
|
||||
`Hola *${order.customerName}*, recibimos correctamente tu pago de *${formatCOP(depositAmount)} COP* a través de Bold.\n\n` +
|
||||
`📦 Tu orden *#${order.id}* ya se encuentra en alistamiento y empaque para despacho a *${order.cityName}*.\n` +
|
||||
`🤝 *Saldo restante a pagar contraentrega:* *${formatCOP(newBalance)} COP* al recibir en tu puerta.\n\n` +
|
||||
`¡Muchas gracias por tu compra en DripZone!`
|
||||
);
|
||||
const clientMsg = dataService.renderMessageTemplate('customer_deposit_confirmed', {
|
||||
tienda_nombre: 'DripZone',
|
||||
cliente_nombre: order.customerName || 'Cliente',
|
||||
orden_id: order.id,
|
||||
valor_anticipo: formatCOP(depositAmount),
|
||||
cliente_ciudad: order.cityName || 'Colombia',
|
||||
saldo_contraentrega: formatCOP(newBalance),
|
||||
});
|
||||
await sendWhatsAppMessage(order.customerPhone, clientMsg);
|
||||
}
|
||||
} catch (waErr) {
|
||||
|
||||
@ -4,9 +4,7 @@ import { CustomProductRequest } from '@/types';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const EVOLUTION_API_URL = process.env.EVOLUTION_API_URL || 'https://evolution.almeira.com.co';
|
||||
const EVOLUTION_API_KEY = process.env.EVOLUTION_API_KEY || 'EE49029EEB85-4568-8F81-C9FDFC6E2943';
|
||||
const EVOLUTION_INSTANCE = process.env.EVOLUTION_INSTANCE_NAME || 'AutomataAlmeira';
|
||||
import { sendWhatsAppMessage } from '@/lib/evolution-api';
|
||||
const CARLOS_PHONE = '573167455877';
|
||||
|
||||
// Carpeta local para almacenar fotos de encargos especiales
|
||||
@ -18,30 +16,6 @@ function ensureUploadsDir() {
|
||||
}
|
||||
}
|
||||
|
||||
// Función auxiliar para enviar mensajes de WhatsApp vía Evolution API
|
||||
async function sendWhatsAppMessage(number: string, text: string) {
|
||||
try {
|
||||
const cleanNumber = number.replace(/\D/g, '');
|
||||
const fullNumber = cleanNumber.length === 10 ? `57${cleanNumber}` : cleanNumber;
|
||||
|
||||
const res = await fetch(`${EVOLUTION_API_URL}/message/sendText/${EVOLUTION_INSTANCE}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
apikey: EVOLUTION_API_KEY,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
number: fullNumber,
|
||||
text: text,
|
||||
}),
|
||||
});
|
||||
return await res.json();
|
||||
} catch (err) {
|
||||
console.warn('Error enviando WhatsApp a ' + number + ':', err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
@ -142,13 +116,15 @@ ${customerEmail ? `✉️ *Correo:* ${customerEmail}\n` : ''}
|
||||
sendWhatsAppMessage(CARLOS_PHONE, adminMessage).catch(() => {});
|
||||
sendWhatsAppMessage('120363428012905103@g.us', adminMessage).catch(() => {});
|
||||
|
||||
// 2. Confirmación automática al cliente por WhatsApp
|
||||
const clientMessage = `👋 ¡Hola *${customerName}*!
|
||||
Recibimos con éxito tu solicitud por encargo para las *${brand} ${modelName}* en talla *${size}*. 👟🔥
|
||||
|
||||
Nuestro equipo ya está verificando disponibilidad y mejor precio con nuestras bodegas y proveedores mayoristas. Te responderemos a este mismo chat con la cotización exacta en el menor tiempo posible.
|
||||
|
||||
¡Gracias por confiar en DripZone! 🚀`;
|
||||
// 2. Confirmación automática al cliente por WhatsApp usando la plantilla registrada
|
||||
const settings = dataService.getSettings();
|
||||
const clientMessage = dataService.renderMessageTemplate('customer_request_received', {
|
||||
tienda_nombre: settings?.storeName || 'DripZone Sneakers',
|
||||
cliente_nombre: customerName,
|
||||
marca: brand,
|
||||
modelo: modelName,
|
||||
talla: size,
|
||||
});
|
||||
|
||||
sendWhatsAppMessage(customerPhone, clientMessage).catch(() => {});
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { dataService } from '@/lib/data-service';
|
||||
import { TEMPLATE_CATEGORIES } from '@/lib/templates-service';
|
||||
import { getMessageTemplatesFromAppwrite, updateMessageTemplateInAppwrite } from '@/lib/server-appwrite';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
@ -8,6 +9,16 @@ export async function GET(request: Request) {
|
||||
const id = searchParams.get('id');
|
||||
const category = searchParams.get('category');
|
||||
|
||||
// Intentar sincronizar con Appwrite si hay plantillas en BD
|
||||
try {
|
||||
const appwriteTemplates = await getMessageTemplatesFromAppwrite();
|
||||
if (appwriteTemplates && appwriteTemplates.length > 0) {
|
||||
for (const t of appwriteTemplates) {
|
||||
dataService.updateMessageTemplate(t.id, t);
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
if (id) {
|
||||
const template = dataService.getMessageTemplateById(id);
|
||||
if (!template) {
|
||||
@ -37,12 +48,18 @@ export async function PUT(request: Request) {
|
||||
return NextResponse.json({ success: false, error: 'ID de la plantilla es obligatorio' }, { status: 400 });
|
||||
}
|
||||
|
||||
const updated = dataService.updateMessageTemplate(id, {
|
||||
const updates = {
|
||||
...(content !== undefined && { content }),
|
||||
...(isActive !== undefined && { isActive }),
|
||||
...(title !== undefined && { title }),
|
||||
...(description !== undefined && { description }),
|
||||
});
|
||||
};
|
||||
|
||||
// Actualizar en Appwrite
|
||||
const updatedInDb = await updateMessageTemplateInAppwrite(id, updates);
|
||||
|
||||
// Actualizar en memoria / dataService
|
||||
const updated = dataService.updateMessageTemplate(id, updates);
|
||||
|
||||
if (!updated) {
|
||||
return NextResponse.json({ success: false, error: 'Plantilla no encontrada' }, { status: 404 });
|
||||
@ -50,8 +67,8 @@ export async function PUT(request: Request) {
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
template: updated,
|
||||
message: `Plantilla "${updated.title}" actualizada correctamente.`,
|
||||
template: updatedInDb || updated,
|
||||
message: `Plantilla "${updated.title}" actualizada correctamente en la base de datos.`,
|
||||
});
|
||||
} catch (error: any) {
|
||||
return NextResponse.json({ success: false, error: error.message }, { status: 400 });
|
||||
|
||||
@ -12,12 +12,38 @@ import {
|
||||
} from '@/lib/evolution-api';
|
||||
import { dataService } from '@/lib/data-service';
|
||||
|
||||
export async function GET() {
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const orders = await getOrdersFromAppwrite();
|
||||
const { searchParams } = new URL(request.url);
|
||||
const phone = searchParams.get('phone');
|
||||
|
||||
let orders = await getOrdersFromAppwrite();
|
||||
if (!orders || orders.length === 0) {
|
||||
orders = dataService.getOrders();
|
||||
}
|
||||
|
||||
if (phone) {
|
||||
const cleanPhone = phone.replace(/\D/g, '');
|
||||
orders = orders.filter((o: any) => {
|
||||
const oPhone = (o.customerPhone || '').replace(/\D/g, '');
|
||||
return oPhone.endsWith(cleanPhone) || cleanPhone.endsWith(oPhone);
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true, orders });
|
||||
} catch (error: any) {
|
||||
const fallback = dataService.getOrders();
|
||||
let fallback = dataService.getOrders();
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const phone = searchParams.get('phone');
|
||||
if (phone) {
|
||||
const cleanPhone = phone.replace(/\D/g, '');
|
||||
fallback = fallback.filter((o: any) => {
|
||||
const oPhone = (o.customerPhone || '').replace(/\D/g, '');
|
||||
return oPhone.endsWith(cleanPhone) || cleanPhone.endsWith(oPhone);
|
||||
});
|
||||
}
|
||||
} catch (_) {}
|
||||
return NextResponse.json({ success: true, orders: fallback });
|
||||
}
|
||||
}
|
||||
|
||||
@ -123,8 +123,8 @@ export default function CheckoutPage() {
|
||||
const depositAmount = isMandatoryDeposit ? 10000 : 0;
|
||||
const balanceDue = Math.max(0, totalAmount - depositAmount);
|
||||
|
||||
const officialWhatsapp = storeSettings?.whatsappNumber || '573167455877';
|
||||
const boldLink = storeSettings?.boldPaymentLink || 'https://bold.co/p/carlos-lasso';
|
||||
const officialWhatsapp = storeSettings?.whatsappNumber || '573106390296';
|
||||
const boldLink = storeSettings?.boldPaymentLink || 'https://drip-zone.online';
|
||||
|
||||
const processOrderSubmission = async () => {
|
||||
setIsSubmitting(true);
|
||||
@ -407,7 +407,7 @@ export default function CheckoutPage() {
|
||||
|
||||
<div className="bg-neutral-900/90 p-2.5 rounded-xl border border-neutral-800 text-xs font-mono text-neutral-300 flex justify-between items-center">
|
||||
<span>📱 Transferencia Directa:</span>
|
||||
<strong className="text-white">Nequi: {storeSettings?.nequiAccount || '316 745 5877'}</strong>
|
||||
<strong className="text-white">Nequi: {storeSettings?.nequiAccount || '310 639 0296'}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -128,8 +128,8 @@ export const INITIAL_PAYMENT_METHODS: PaymentMethod[] = [
|
||||
description: 'Paga de forma rápida y 100% segura con tu cuenta Bold. Acepta PSE, Tarjetas Visa/Mastercard y QR de cualquier banco.',
|
||||
requiresDeposit: false,
|
||||
depositAmount: 0,
|
||||
boldPaymentLink: 'https://bold.co/p/carlos-lasso',
|
||||
accountInfo: 'Pasarela Oficial Bold.co • PSE / Tarjetas / QR',
|
||||
boldPaymentLink: 'https://drip-zone.online',
|
||||
accountInfo: 'Pasarela Oficial Bold.co • PSE / Tarjetas / QR Bre-B',
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
@ -139,8 +139,8 @@ export const INITIAL_PAYMENT_METHODS: PaymentMethod[] = [
|
||||
description: 'Asegura tu talla con solo $10.000 COP vía Bold/Nequi/Daviplata y cancelas el saldo restante en efectivo o transferencia cuando el mensajero llegue a tu casa.',
|
||||
requiresDeposit: true,
|
||||
depositAmount: 10000,
|
||||
accountInfo: 'Nequi / Daviplata Oficial: 316 745 5877 (Carlos Andrés Lasso)',
|
||||
boldPaymentLink: 'https://bold.co/p/carlos-lasso',
|
||||
accountInfo: 'Nequi / Daviplata Oficial DripZone: 310 639 0296',
|
||||
boldPaymentLink: 'https://drip-zone.online',
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
@ -159,7 +159,7 @@ export const INITIAL_PAYMENT_METHODS: PaymentMethod[] = [
|
||||
description: 'Paga el 100% anticipado y recibe prioridad de despacho express en 24 horas.',
|
||||
requiresDeposit: false,
|
||||
depositAmount: 0,
|
||||
accountInfo: 'Nequi / Daviplata: 316 745 5877 (Carlos Andrés Lasso)',
|
||||
accountInfo: 'Nequi / Daviplata Oficial DripZone: 310 639 0296',
|
||||
isActive: true,
|
||||
},
|
||||
];
|
||||
@ -172,10 +172,10 @@ export const INITIAL_ORDERS: Order[] = [];
|
||||
|
||||
export const INITIAL_USERS: User[] = [
|
||||
{
|
||||
id: 'usr-admin-carlos',
|
||||
name: 'Carlos Lasso',
|
||||
email: 'carlos.andres.lasso@gmail.com',
|
||||
phone: '573167455877',
|
||||
id: 'usr-admin-dripzone',
|
||||
name: 'Administrador DripZone',
|
||||
email: 'info@drip-zone.online',
|
||||
phone: '573106390296',
|
||||
city: 'Cali',
|
||||
address: 'Sede Principal / Cali',
|
||||
role: 'admin',
|
||||
@ -210,53 +210,84 @@ function getPathModule() {
|
||||
return null;
|
||||
}
|
||||
|
||||
function getPersistedFilePath(): string | null {
|
||||
function getPersistedFilePath(fileName: string = 'persisted_products.json'): string | null {
|
||||
const pathMod = getPathModule();
|
||||
if (!pathMod) return null;
|
||||
return pathMod.join(process.cwd(), 'data', 'persisted_products.json');
|
||||
return pathMod.join(process.cwd(), 'data', fileName);
|
||||
}
|
||||
|
||||
function loadPersistedProducts(): Product[] {
|
||||
function loadPersistedData<T>(fileName: string, defaultValue: T): T {
|
||||
const fsMod = getFsModule();
|
||||
const filePath = getPersistedFilePath();
|
||||
const filePath = getPersistedFilePath(fileName);
|
||||
if (fsMod && filePath && fsMod.existsSync(filePath)) {
|
||||
try {
|
||||
const content = fsMod.readFileSync(filePath, 'utf-8');
|
||||
const parsed = JSON.parse(content);
|
||||
if (Array.isArray(parsed) && parsed.length > 0) {
|
||||
if (parsed !== undefined && parsed !== null) {
|
||||
return parsed;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[DataService] Error leyendo archivo de productos persistidos:', e);
|
||||
console.warn(`[DataService] Error leyendo ${fileName}:`, e);
|
||||
}
|
||||
}
|
||||
return [...INITIAL_PRODUCTS];
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
function savePersistedProducts(products: Product[]): void {
|
||||
function savePersistedData<T>(fileName: string, data: T): void {
|
||||
const fsMod = getFsModule();
|
||||
const pathMod = getPathModule();
|
||||
const filePath = getPersistedFilePath();
|
||||
const filePath = getPersistedFilePath(fileName);
|
||||
if (fsMod && pathMod && filePath) {
|
||||
try {
|
||||
const dir = pathMod.dirname(filePath);
|
||||
if (!fsMod.existsSync(dir)) {
|
||||
fsMod.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
fsMod.writeFileSync(filePath, JSON.stringify(products, null, 2), 'utf-8');
|
||||
fsMod.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
|
||||
} catch (e) {
|
||||
console.warn('[DataService] Error guardando productos en archivo persistente:', e);
|
||||
console.warn(`[DataService] Error guardando ${fileName}:`, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function loadPersistedProducts(): Product[] {
|
||||
return loadPersistedData<Product[]>('persisted_products.json', [...INITIAL_PRODUCTS]);
|
||||
}
|
||||
|
||||
function savePersistedProducts(products: Product[]): void {
|
||||
savePersistedData('persisted_products.json', products);
|
||||
}
|
||||
|
||||
export const DEFAULT_STORE_SETTINGS: StoreSettings = {
|
||||
storeName: 'DripZone',
|
||||
slogan: 'Exclusive Streetwear & Sneakers Oficial',
|
||||
email: 'info@drip-zone.online',
|
||||
phone: '3106390296',
|
||||
whatsappNumber: '573106390296',
|
||||
instagramHandle: '@dripzone.co',
|
||||
address: 'Bodega Central / Cali',
|
||||
city: 'Cali, Colombia',
|
||||
nequiAccount: '310 639 0296',
|
||||
daviplataAccount: '310 639 0296',
|
||||
accountHolder: 'DripZone Oficial',
|
||||
boldPaymentLink: 'https://drip-zone.online',
|
||||
boldApiKey: '',
|
||||
boldMerchantId: '',
|
||||
boldQrUrl: '',
|
||||
boldEnabled: true,
|
||||
defaultProfitMargin: 35,
|
||||
defaultDepositAmount: 10000,
|
||||
};
|
||||
|
||||
class DataService {
|
||||
private products: Product[] = loadPersistedProducts();
|
||||
private cities: City[] = [...INITIAL_CITIES];
|
||||
private paymentMethods: PaymentMethod[] = [...INITIAL_PAYMENT_METHODS];
|
||||
private suppliers: Supplier[] = [...INITIAL_SUPPLIERS];
|
||||
private orders: Order[] = [...INITIAL_ORDERS];
|
||||
private cities: City[] = loadPersistedData<City[]>('persisted_cities.json', [...INITIAL_CITIES]);
|
||||
private paymentMethods: PaymentMethod[] = loadPersistedData<PaymentMethod[]>('persisted_payment_methods.json', [...INITIAL_PAYMENT_METHODS]);
|
||||
private suppliers: Supplier[] = loadPersistedData<Supplier[]>('persisted_suppliers.json', [...INITIAL_SUPPLIERS]);
|
||||
private orders: Order[] = loadPersistedData<Order[]>('persisted_orders.json', [...INITIAL_ORDERS]);
|
||||
private supplierPosts: SupplierPost[] = [...INITIAL_SUPPLIER_POSTS];
|
||||
private settings: StoreSettings = loadPersistedData<StoreSettings>('persisted_settings.json', DEFAULT_STORE_SETTINGS);
|
||||
private messageTemplates: MessageTemplate[] = loadPersistedData<MessageTemplate[]>('persisted_templates.json', [...DEFAULT_MESSAGE_TEMPLATES]);
|
||||
|
||||
getProducts(): Product[] {
|
||||
if (this.products.length === 0) {
|
||||
@ -661,6 +692,7 @@ class DataService {
|
||||
|
||||
addCity(city: City): City {
|
||||
this.cities.push(city);
|
||||
savePersistedData('persisted_cities.json', this.cities);
|
||||
return city;
|
||||
}
|
||||
|
||||
@ -668,12 +700,14 @@ class DataService {
|
||||
const idx = this.cities.findIndex((c) => c.id === id);
|
||||
if (idx === -1) return null;
|
||||
this.cities[idx] = { ...this.cities[idx], ...updates };
|
||||
savePersistedData('persisted_cities.json', this.cities);
|
||||
return this.cities[idx];
|
||||
}
|
||||
|
||||
deleteCity(id: string): boolean {
|
||||
const initialLen = this.cities.length;
|
||||
this.cities = this.cities.filter((c) => c.id !== id);
|
||||
savePersistedData('persisted_cities.json', this.cities);
|
||||
return this.cities.length < initialLen;
|
||||
}
|
||||
|
||||
@ -687,6 +721,7 @@ class DataService {
|
||||
|
||||
addPaymentMethod(method: PaymentMethod): PaymentMethod {
|
||||
this.paymentMethods.push(method);
|
||||
savePersistedData('persisted_payment_methods.json', this.paymentMethods);
|
||||
return method;
|
||||
}
|
||||
|
||||
@ -694,12 +729,14 @@ class DataService {
|
||||
const idx = this.paymentMethods.findIndex((p) => p.id === id);
|
||||
if (idx === -1) return null;
|
||||
this.paymentMethods[idx] = { ...this.paymentMethods[idx], ...updates };
|
||||
savePersistedData('persisted_payment_methods.json', this.paymentMethods);
|
||||
return this.paymentMethods[idx];
|
||||
}
|
||||
|
||||
deletePaymentMethod(id: string): boolean {
|
||||
const initialLen = this.paymentMethods.length;
|
||||
this.paymentMethods = this.paymentMethods.filter((p) => p.id !== id);
|
||||
savePersistedData('persisted_payment_methods.json', this.paymentMethods);
|
||||
return this.paymentMethods.length < initialLen;
|
||||
}
|
||||
|
||||
@ -713,6 +750,7 @@ class DataService {
|
||||
|
||||
addOrder(order: Order): Order {
|
||||
this.orders.unshift(order);
|
||||
savePersistedData('persisted_orders.json', this.orders);
|
||||
return order;
|
||||
}
|
||||
|
||||
@ -737,6 +775,7 @@ class DataService {
|
||||
createdAt: orderData.createdAt || new Date().toISOString(),
|
||||
};
|
||||
this.orders.unshift(newOrder);
|
||||
savePersistedData('persisted_orders.json', this.orders);
|
||||
return newOrder;
|
||||
}
|
||||
|
||||
@ -756,12 +795,14 @@ class DataService {
|
||||
depositAmount: deposit,
|
||||
balanceDue,
|
||||
};
|
||||
savePersistedData('persisted_orders.json', this.orders);
|
||||
return this.orders[idx];
|
||||
}
|
||||
|
||||
deleteOrder(id: string): boolean {
|
||||
const initialLen = this.orders.length;
|
||||
this.orders = this.orders.filter((o) => o.id !== id);
|
||||
savePersistedData('persisted_orders.json', this.orders);
|
||||
return this.orders.length < initialLen;
|
||||
}
|
||||
|
||||
@ -850,33 +891,13 @@ class DataService {
|
||||
}
|
||||
|
||||
// Configuración de la Tienda
|
||||
private settings: StoreSettings = {
|
||||
storeName: 'DripZone',
|
||||
slogan: 'Luxury Streetwear & Collectibles',
|
||||
email: 'info@drip-zone.online',
|
||||
phone: '3106390296',
|
||||
whatsappNumber: '573106390296',
|
||||
instagramHandle: '@dripzone.co',
|
||||
address: 'Bodega Central / Cali',
|
||||
city: 'Cali, Colombia',
|
||||
nequiAccount: '316 745 5877',
|
||||
daviplataAccount: '316 745 5877',
|
||||
accountHolder: 'Carlos Andrés Lasso',
|
||||
boldPaymentLink: 'https://bold.co/p/carlos-lasso',
|
||||
boldApiKey: '',
|
||||
boldMerchantId: '',
|
||||
boldQrUrl: '',
|
||||
boldEnabled: true,
|
||||
defaultProfitMargin: 35,
|
||||
defaultDepositAmount: 10000,
|
||||
};
|
||||
|
||||
getSettings(): StoreSettings {
|
||||
return this.settings;
|
||||
}
|
||||
|
||||
updateSettings(updates: Partial<StoreSettings>): StoreSettings {
|
||||
this.settings = { ...this.settings, ...updates };
|
||||
savePersistedData('persisted_settings.json', this.settings);
|
||||
return this.settings;
|
||||
}
|
||||
|
||||
@ -912,9 +933,15 @@ class DataService {
|
||||
|
||||
authenticate(emailOrPhone: string): User | null {
|
||||
const q = emailOrPhone.trim().toLowerCase();
|
||||
const user = this.users.find(
|
||||
(u) => u.email.toLowerCase() === q || u.phone === q || u.phone.includes(q)
|
||||
);
|
||||
const cleanDigits = q.replace(/\D/g, '');
|
||||
const user = this.users.find((u) => {
|
||||
if (u.email.toLowerCase() === q) return true;
|
||||
const uDigits = u.phone.replace(/\D/g, '');
|
||||
if (cleanDigits.length >= 7 && uDigits.length >= 7) {
|
||||
return uDigits === cleanDigits || uDigits.endsWith(cleanDigits) || cleanDigits.endsWith(uDigits);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return user || null;
|
||||
}
|
||||
|
||||
@ -941,8 +968,6 @@ class DataService {
|
||||
// ==========================================
|
||||
// SISTEMA DE PLANTILLAS CATEGORIZADAS
|
||||
// ==========================================
|
||||
private messageTemplates: MessageTemplate[] = [...DEFAULT_MESSAGE_TEMPLATES];
|
||||
|
||||
getMessageTemplates(category?: string): MessageTemplate[] {
|
||||
if (!category || category === 'all') {
|
||||
return this.messageTemplates;
|
||||
@ -962,6 +987,7 @@ class DataService {
|
||||
...updates,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
savePersistedData('persisted_templates.json', this.messageTemplates);
|
||||
return this.messageTemplates[idx];
|
||||
}
|
||||
|
||||
@ -971,6 +997,7 @@ class DataService {
|
||||
const idx = this.messageTemplates.findIndex((t) => t.id === id);
|
||||
if (idx === -1) {
|
||||
this.messageTemplates.push({ ...defaultTpl, updatedAt: new Date().toISOString() });
|
||||
savePersistedData('persisted_templates.json', this.messageTemplates);
|
||||
return defaultTpl;
|
||||
}
|
||||
this.messageTemplates[idx] = {
|
||||
@ -979,6 +1006,7 @@ class DataService {
|
||||
isActive: true,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
savePersistedData('persisted_templates.json', this.messageTemplates);
|
||||
return this.messageTemplates[idx];
|
||||
}
|
||||
|
||||
@ -988,6 +1016,7 @@ class DataService {
|
||||
content: t.defaultContent,
|
||||
updatedAt: new Date().toISOString(),
|
||||
}));
|
||||
savePersistedData('persisted_templates.json', this.messageTemplates);
|
||||
return this.messageTemplates;
|
||||
}
|
||||
|
||||
@ -1007,7 +1036,7 @@ class DataService {
|
||||
{
|
||||
id: 'req-1',
|
||||
customerName: 'Santiago Mejía',
|
||||
customerPhone: '573167455877',
|
||||
customerPhone: '573105559876',
|
||||
customerCity: 'Cali',
|
||||
brand: 'Jordan',
|
||||
modelName: 'Air Jordan 4 Retro Travis Scott Cactus Jack',
|
||||
|
||||
@ -31,7 +31,7 @@ export const otpStore = {
|
||||
const key = identifier.trim().toLowerCase();
|
||||
const record = otpCache.get(key);
|
||||
|
||||
// Emergency Master PIN fallback for Carlos Lasso
|
||||
// Emergency Master PIN fallback
|
||||
if (code === '1978' || code === '197824') {
|
||||
otpCache.delete(key);
|
||||
return { valid: true };
|
||||
|
||||
@ -789,3 +789,83 @@ export async function toggleMonitoredGroupInAppwrite(
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 9. PLANTILLAS DE MENSAJES & NOTIFICACIONES (CRUD APPWRITE)
|
||||
// =========================================================================
|
||||
export async function getMessageTemplatesFromAppwrite(): Promise<MessageTemplate[]> {
|
||||
try {
|
||||
const res = await serverDatabases.listDocuments(DB_ID, 'message_templates', [
|
||||
Query.limit(50),
|
||||
]);
|
||||
if (res.documents && res.documents.length > 0) {
|
||||
return res.documents.map((d: any) => {
|
||||
let vars = [];
|
||||
try {
|
||||
vars = typeof d.availableVariables === 'string' ? JSON.parse(d.availableVariables) : (d.availableVariables || []);
|
||||
} catch (_) {}
|
||||
return {
|
||||
id: d.$id,
|
||||
title: d.title || '',
|
||||
description: d.description || '',
|
||||
category: d.category || 'customer_sales',
|
||||
channel: d.channel || 'whatsapp',
|
||||
content: d.content || '',
|
||||
defaultContent: d.content || '',
|
||||
availableVariables: vars,
|
||||
isActive: d.isActive !== false,
|
||||
updatedAt: d.updatedAt || d.$updatedAt,
|
||||
};
|
||||
});
|
||||
}
|
||||
return [];
|
||||
} catch (e: any) {
|
||||
console.warn('Error getMessageTemplatesFromAppwrite:', e.message);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateMessageTemplateInAppwrite(
|
||||
id: string,
|
||||
updates: Partial<MessageTemplate>
|
||||
): Promise<MessageTemplate | null> {
|
||||
try {
|
||||
const payload: Record<string, any> = {
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
if (updates.title !== undefined) payload.title = updates.title;
|
||||
if (updates.description !== undefined) payload.description = updates.description;
|
||||
if (updates.content !== undefined) payload.content = updates.content;
|
||||
if (updates.isActive !== undefined) payload.isActive = updates.isActive;
|
||||
if (updates.category !== undefined) payload.category = updates.category;
|
||||
if (updates.channel !== undefined) payload.channel = updates.channel;
|
||||
if (updates.availableVariables !== undefined) {
|
||||
payload.availableVariables = typeof updates.availableVariables === 'string'
|
||||
? updates.availableVariables
|
||||
: JSON.stringify(updates.availableVariables);
|
||||
}
|
||||
|
||||
const doc = await serverDatabases.updateDocument(DB_ID, 'message_templates', id, payload);
|
||||
let vars = [];
|
||||
try {
|
||||
vars = typeof doc.availableVariables === 'string' ? JSON.parse(doc.availableVariables) : (doc.availableVariables || []);
|
||||
} catch (_) {}
|
||||
|
||||
return {
|
||||
id: doc.$id,
|
||||
title: doc.title,
|
||||
description: doc.description,
|
||||
category: doc.category,
|
||||
channel: doc.channel,
|
||||
content: doc.content,
|
||||
defaultContent: doc.content,
|
||||
availableVariables: vars,
|
||||
isActive: doc.isActive !== false,
|
||||
updatedAt: doc.updatedAt,
|
||||
};
|
||||
} catch (e: any) {
|
||||
console.error('Error updateMessageTemplateInAppwrite:', e.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -94,7 +94,7 @@ Estamos alistando tu paquete. Te enviaremos el número de guía o notificación
|
||||
availableVariables: [
|
||||
{ key: 'tienda_nombre', label: 'Nombre de Tienda', example: 'DripZone Luxury', description: 'Nombre comercial configurado' },
|
||||
{ key: 'orden_id', label: 'No. de Orden', example: 'ORD-9842', description: 'Identificador único del pedido' },
|
||||
{ key: 'cliente_nombre', label: 'Nombre del Cliente', example: 'Carlos Lasso', description: 'Nombre registrado' },
|
||||
{ key: 'cliente_nombre', label: 'Nombre del Cliente', example: 'Cliente DripZone', description: 'Nombre registrado' },
|
||||
{ key: 'cliente_telefono', label: 'Teléfono Cliente', example: '3167455877', description: 'Celular del comprador' },
|
||||
{ key: 'cliente_direccion', label: 'Dirección de Entrega', example: 'Calle 5 # 24-80, Apto 302', description: 'Dirección física' },
|
||||
{ key: 'cliente_ciudad', label: 'Ciudad Destino', example: 'Cali', description: 'Municipio de despacho' },
|
||||
@ -150,7 +150,7 @@ Estamos alistando tu paquete. Te enviaremos el número de guía o notificación
|
||||
availableVariables: [
|
||||
{ key: 'tienda_nombre', label: 'Nombre de Tienda', example: 'DripZone', description: 'Nombre de la marca' },
|
||||
{ key: 'orden_id', label: 'No. Orden', example: 'ORD-9842', description: 'ID de la orden' },
|
||||
{ key: 'cliente_nombre', label: 'Nombre Cliente', example: 'Carlos Lasso', description: 'Nombre' },
|
||||
{ key: 'cliente_nombre', label: 'Nombre Cliente', example: 'Cliente DripZone', description: 'Nombre' },
|
||||
{ key: 'cliente_telefono', label: 'Teléfono', example: '3167455877', description: 'Teléfono' },
|
||||
{ key: 'cliente_direccion', label: 'Dirección', example: 'Av 6N # 28N-12', description: 'Dirección' },
|
||||
{ key: 'cliente_ciudad', label: 'Ciudad', example: 'Cali', description: 'Ciudad' },
|
||||
@ -270,7 +270,7 @@ Por favor estar atento a la portería o celular. ¡Gracias por tu compra en {tie
|
||||
|
||||
Por favor estar atento a la portería o celular. ¡Gracias por tu compra en {tienda_nombre}! 🔥`,
|
||||
availableVariables: [
|
||||
{ key: 'cliente_nombre', label: 'Nombre Cliente', example: 'Carlos Lasso', description: 'Nombre' },
|
||||
{ key: 'cliente_nombre', label: 'Nombre Cliente', example: 'Cliente DripZone', description: 'Nombre' },
|
||||
{ key: 'orden_id', label: 'No. Orden', example: 'ORD-9842', description: 'ID de la orden' },
|
||||
{ key: 'cliente_direccion', label: 'Dirección', example: 'Carrera 100 # 16-20', description: 'Dirección' },
|
||||
{ key: 'cliente_ciudad', label: 'Ciudad', example: 'Cali', description: 'Ciudad' },
|
||||
@ -301,7 +301,7 @@ Por favor estar atento a la portería o celular. ¡Gracias por tu compra en {tie
|
||||
|
||||
¡Muchas gracias por elegir {tienda_nombre}! ✨`,
|
||||
availableVariables: [
|
||||
{ key: 'cliente_nombre', label: 'Nombre Cliente', example: 'Carlos Lasso', description: 'Nombre' },
|
||||
{ key: 'cliente_nombre', label: 'Nombre Cliente', example: 'Cliente DripZone', description: 'Nombre' },
|
||||
{ key: 'orden_id', label: 'No. Orden', example: 'ORD-9842', description: 'ID orden' },
|
||||
{ key: 'tienda_nombre', label: 'Nombre Tienda', example: 'DripZone', description: 'Marca' },
|
||||
],
|
||||
@ -324,7 +324,7 @@ Por favor estar atento a la portería o celular. ¡Gracias por tu compra en {tie
|
||||
|
||||
🤝 ¿Nos confirmas en qué horario o con qué persona podemos coordinar el reintento de entrega hoy? Quedamos muy atentos.`,
|
||||
availableVariables: [
|
||||
{ key: 'cliente_nombre', label: 'Nombre Cliente', example: 'Carlos Lasso', description: 'Nombre' },
|
||||
{ key: 'cliente_nombre', label: 'Nombre Cliente', example: 'Cliente DripZone', description: 'Nombre' },
|
||||
{ key: 'orden_id', label: 'No. Orden', example: 'ORD-9842', description: 'ID orden' },
|
||||
{ key: 'cliente_direccion', label: 'Dirección', example: 'Calle 9 # 44-12', description: 'Dirección' },
|
||||
{ key: 'cliente_ciudad', label: 'Ciudad', example: 'Cali', description: 'Ciudad' },
|
||||
@ -465,7 +465,7 @@ Hola {usuario_nombre}, tu código de verificación para iniciar sesión es:
|
||||
⏱️ _Válido por 5 minutos. No compartas este código con nadie._`,
|
||||
availableVariables: [
|
||||
{ key: 'tienda_nombre', label: 'Nombre Tienda', example: 'DripZone Sneakers', description: 'Marca' },
|
||||
{ key: 'usuario_nombre', label: 'Nombre Usuario', example: 'Carlos Lasso', description: 'Usuario' },
|
||||
{ key: 'usuario_nombre', label: 'Nombre Usuario', example: 'Cliente DripZone', description: 'Usuario' },
|
||||
{ key: 'codigo_otp', label: 'Código de 6 Dígitos', example: '849201', description: 'Código OTP' },
|
||||
],
|
||||
},
|
||||
@ -506,7 +506,7 @@ Hola {usuario_nombre}! Tu código de seguridad para validar tu cuenta es:
|
||||
content: `Hola {usuario_nombre}! Te escribimos del equipo de {tienda_nombre} 🟣`,
|
||||
defaultContent: `Hola {usuario_nombre}! Te escribimos del equipo de {tienda_nombre} 🟣`,
|
||||
availableVariables: [
|
||||
{ key: 'usuario_nombre', label: 'Nombre Usuario', example: 'Carlos Lasso', description: 'Nombre del usuario' },
|
||||
{ key: 'usuario_nombre', label: 'Nombre Usuario', example: 'Cliente DripZone', description: 'Nombre del usuario' },
|
||||
{ key: 'tienda_nombre', label: 'Nombre Tienda', example: 'DripZone', description: 'Nombre' },
|
||||
{ key: 'ciudad', label: 'Ciudad Usuario', example: 'Cali', description: 'Ciudad' },
|
||||
],
|
||||
@ -538,7 +538,7 @@ Hola {cliente_nombre}, para procesar tu orden #{orden_id} hacia {cliente_ciudad}
|
||||
¡Una vez confirmado tu comprobante, tu despacho saldrá de inmediato!`,
|
||||
availableVariables: [
|
||||
{ key: 'tienda_nombre', label: 'Nombre Tienda', example: 'DripZone', description: 'Tienda' },
|
||||
{ key: 'cliente_nombre', label: 'Nombre Cliente', example: 'Carlos Lasso', description: 'Cliente' },
|
||||
{ key: 'cliente_nombre', label: 'Nombre Cliente', example: 'Cliente DripZone', description: 'Cliente' },
|
||||
{ key: 'orden_id', label: 'No. Orden', example: 'ORD-9842', description: 'ID orden' },
|
||||
{ key: 'cliente_ciudad', label: 'Ciudad', example: 'Bogotá D.C.', description: 'Ciudad' },
|
||||
{ key: 'costo_envio', label: 'Costo Flete', example: '$12.000 COP', description: 'Valor del flete' },
|
||||
@ -572,6 +572,41 @@ Cualquier duda o asesoría adicional, ¡estamos para servirte con todo el gusto!
|
||||
{ key: 'ciudad_tienda', label: 'Ciudad Sede', example: 'Cali', description: 'Ciudad' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'customer_request_received',
|
||||
title: 'Confirmación de Encargo Especial al Cliente',
|
||||
description: 'Notificación enviada al cliente por WhatsApp cuando registra una solicitud de calzado por encargo.',
|
||||
category: 'customer_sales',
|
||||
channel: 'whatsapp',
|
||||
isActive: true,
|
||||
content: `👋 ¡Hola *{cliente_nombre}*!\n\nRecibimos con éxito tu solicitud por encargo para las *{marca} {modelo}* en talla *{talla}*. 👟🔥\n\nNuestro equipo en {tienda_nombre} ya está verificando disponibilidad y mejor precio con nuestras bodegas y proveedores mayoristas. Te responderemos a este mismo chat con la cotización exacta en el menor tiempo posible.\n\n¡Gracias por confiar en {tienda_nombre}! 🚀`,
|
||||
defaultContent: `👋 ¡Hola *{cliente_nombre}*!\n\nRecibimos con éxito tu solicitud por encargo para las *{marca} {modelo}* en talla *{talla}*. 👟🔥\n\nNuestro equipo en {tienda_nombre} ya está verificando disponibilidad y mejor precio con nuestras bodegas y proveedores mayoristas. Te responderemos a este mismo chat con la cotización exacta en el menor tiempo posible.\n\n¡Gracias por confiar en {tienda_nombre}! 🚀`,
|
||||
availableVariables: [
|
||||
{ key: 'tienda_nombre', label: 'Nombre Tienda', example: 'DripZone', description: 'Nombre' },
|
||||
{ key: 'cliente_nombre', label: 'Nombre Cliente', example: 'Cliente DripZone', description: 'Nombre' },
|
||||
{ key: 'marca', label: 'Marca', example: 'Nike', description: 'Marca calzado' },
|
||||
{ key: 'modelo', label: 'Modelo', example: 'Air Jordan 4', description: 'Modelo' },
|
||||
{ key: 'talla', label: 'Talla', example: '41', description: 'Talla' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'customer_deposit_confirmed',
|
||||
title: 'Confirmación de Anticipo Bold al Cliente',
|
||||
description: 'Notificación enviada al cliente por WhatsApp cuando su pago de anticipo de separación es acreditado por Bold.',
|
||||
category: 'customer_sales',
|
||||
channel: 'whatsapp',
|
||||
isActive: true,
|
||||
content: `🎉 *¡Tu anticipo de separación fue confirmado con éxito!*\n\nHola *{cliente_nombre}*, recibimos correctamente tu pago de *{valor_anticipo}* a través de Bold.\n\n📦 Tu orden *#{orden_id}* ya se encuentra en alistamiento y empaque para despacho a *{cliente_ciudad}*.\n🤝 *Saldo restante a pagar contraentrega:* *{saldo_contraentrega}* al recibir en tu puerta.\n\nPuedes consultar el estado de tu pedido en cualquier momento desde la app oficial de {tienda_nombre}.\n\n¡Muchas gracias por tu compra en {tienda_nombre}!`,
|
||||
defaultContent: `🎉 *¡Tu anticipo de separación fue confirmado con éxito!*\n\nHola *{cliente_nombre}*, recibimos correctamente tu pago de *{valor_anticipo}* a través de Bold.\n\n📦 Tu orden *#{orden_id}* ya se encuentra en alistamiento y empaque para despacho a *{cliente_ciudad}*.\n🤝 *Saldo restante a pagar contraentrega:* *{saldo_contraentrega}* al recibir en tu puerta.\n\nPuedes consultar el estado de tu pedido en cualquier momento desde la app oficial de {tienda_nombre}.\n\n¡Muchas gracias por tu compra en {tienda_nombre}!`,
|
||||
availableVariables: [
|
||||
{ key: 'tienda_nombre', label: 'Nombre Tienda', example: 'DripZone', description: 'Nombre de la tienda' },
|
||||
{ key: 'cliente_nombre', label: 'Nombre Cliente', example: 'Cliente DripZone', description: 'Nombre del comprador' },
|
||||
{ key: 'orden_id', label: 'No. de Orden', example: 'ORD-9842', description: 'ID de la orden' },
|
||||
{ key: 'valor_anticipo', label: 'Valor Anticipo', example: '$10.000 COP', description: 'Monto anticipo pagado' },
|
||||
{ key: 'cliente_ciudad', label: 'Ciudad Destino', example: 'Cali', description: 'Municipio de entrega' },
|
||||
{ key: 'saldo_contraentrega', label: 'Saldo Contraentrega', example: '$115.000 COP', description: 'Saldo en puerta' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@ -239,7 +239,7 @@ export type MessageTemplateCategory =
|
||||
export interface MessageTemplateVariable {
|
||||
key: string; // e.g. 'cliente_nombre'
|
||||
label: string; // e.g. 'Nombre del Cliente'
|
||||
example: string; // e.g. 'Carlos Lasso'
|
||||
example: string; // e.g. 'Cliente DripZone'
|
||||
description?: string; // e.g. 'Nombre completo registrado por el cliente en el pedido'
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
import paramiko
|
||||
import sys
|
||||
|
||||
if sys.platform == 'win32':
|
||||
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
||||
sys.stderr.reconfigure(encoding='utf-8', errors='replace')
|
||||
|
||||
HOST = "161.97.93.156"
|
||||
USER = "carlos"
|
||||
PASSWORD = "Innova1978+*?"
|
||||
|
||||
@ -101,7 +101,9 @@ def main():
|
||||
# Copiar APK a contenedores y permisos
|
||||
cmd_copy = f"""
|
||||
docker cp {REMOTE_BASE}/public/DripZone.apk dripzone-production:/app/public/DripZone.apk || true
|
||||
docker cp {REMOTE_BASE}/public/DripZone.apk bfmnoga3lb0ckomhdmxe0rco-021053217798:/app/public/DripZone.apk || true
|
||||
for c in $(docker ps -q --filter "name=bfmnoga3"); do
|
||||
docker cp {REMOTE_BASE}/public/DripZone.apk $c:/app/public/DripZone.apk || true
|
||||
done
|
||||
docker exec dripzone-production ls -lh /app/public/DripZone.apk || true
|
||||
"""
|
||||
stdin, stdout, stderr = ssh.exec_command(cmd_copy)
|
||||
@ -121,22 +123,23 @@ def main():
|
||||
|
||||
print("[5/5] Enviando notificación detallada y archivo APK al WhatsApp de Carlos (+57 316 745 5877)...")
|
||||
message_text = (
|
||||
"🚀 *DripZone v1.6 - Línea Oficial Exclusiva & Instancia DripZone Conectada*\n\n"
|
||||
"Hola Carlos, se ha implementado en producción la división operativa solicitada:\n\n"
|
||||
"📱 *1. Línea Celular Exclusiva DripZone (+57 310 639 0296):*\n"
|
||||
"• Conectada exitosamente en Evolution API bajo la instancia *DripZone* (Token: `83B5BD230796-4324-AE73-3508F69E279D`).\n"
|
||||
"• Todos los envíos de códigos de seguridad OTP, confirmaciones de pedidos, recibos de abono Bold y chats de soporte con clientes ahora se emiten de forma exclusiva desde esta línea oficial.\n\n"
|
||||
"📥 *2. Recepción de Productos y Publicaciones (Tu WhatsApp Personal):*\n"
|
||||
"• Se mantiene conectada tu instancia personal *AutomataAlmeira* para la lectura de los grupos de proveedores mayoristas donde recibes el calzado.\n"
|
||||
"• Ninguna publicación se interrumpe y la catalogación inteligente sigue operando con tus grupos personales.\n\n"
|
||||
"🌐 *3. Dominio Oficial drip-zone.online:*\n"
|
||||
"• Configurado el servidor de producción (Traefik y Docker) para responder en *drip-zone.online*, *www.drip-zone.online* y el alias `dripzone.almeira.com.co`.\n\n"
|
||||
"💳 *4. Pasarela Bold & QR Bre-B en Vivo:*\n"
|
||||
"• Generación dinámica de QR Bre-B para el cobro de la separación de $10.000 COP y acreditación automática en tiempo real.\n\n"
|
||||
"🚀 *DripZone v1.6 - Dominio drip-zone.online & Correo Oficial info@drip-zone.online Configurados*\n\n"
|
||||
"Hola Carlos, se ha completado el despliegue de las nuevas configuraciones en producción:\n\n"
|
||||
"🌐 *1. Dominio Oficial & Correo Electrónico:*\n"
|
||||
"• URL Principal: *https://drip-zone.online*\n"
|
||||
"• Correo Oficial: *info@drip-zone.online*\n"
|
||||
"• Certificados SSL y Traefik operativos y validados.\n\n"
|
||||
"📱 *2. Línea Celular Exclusiva DripZone (+57 310 639 0296):*\n"
|
||||
"• Instancia Evolution API *DripZone* activa (`83B5BD230796-4324-AE73-3508F69E279D`).\n"
|
||||
"• Los códigos OTP, confirmaciones de compras, soporte y enlaces Bold se emiten desde esta línea oficial.\n\n"
|
||||
"📥 *3. Publicaciones y Catálogo (Tu WhatsApp Personal):*\n"
|
||||
"• La instancia *AutomataAlmeira* sigue activa recibiendo los mensajes de los grupos de proveedores mayoristas para sincronización automática de calzado.\n\n"
|
||||
"💳 *4. Pasarela Bold & QR Bre-B en Producción:*\n"
|
||||
"• Cobro de anticipo de $10.000 COP con generación automática de QR Bre-B y acreditación en vivo.\n\n"
|
||||
"📲 *5. App Nativa Android v1.6 Sincronizada:*\n"
|
||||
"• Compilada con la nueva línea de atención oficial (+57 310 639 0296) y soporte dinámico para `drip-zone.online`.\n\n"
|
||||
f"🔗 *Descarga Directa:* {APK_URL}\n\n"
|
||||
"Te adjunto el APK compilado para que puedas instalarlo y probar la experiencia de usuario."
|
||||
"• Compilada con el nuevo dominio y la línea oficial de atención.\n\n"
|
||||
f"🔗 *Descarga Directa del APK:* {APK_URL}\n\n"
|
||||
"Te adjunto el APK compilado para que puedas actualizar la aplicación en tu celular."
|
||||
)
|
||||
|
||||
req_text = urllib.request.Request(
|
||||
@ -156,8 +159,8 @@ def main():
|
||||
"number": TARGET_PHONE,
|
||||
"mediatype": "document",
|
||||
"mimetype": "application/vnd.android.package-archive",
|
||||
"fileName": "DripZone-Native-v1.5.apk",
|
||||
"caption": "📲 *DripZone Native Kotlin v1.5 - Pasarela Bold & QR Bre-B*",
|
||||
"fileName": "DripZone-v1.6-Official.apk",
|
||||
"caption": "📲 *DripZone Native v1.6 - Dominio drip-zone.online & Correo info@drip-zone.online*",
|
||||
"media": APK_URL
|
||||
}).encode("utf-8"),
|
||||
headers={"apikey": EVOLUTION_API_KEY, "Content-Type": "application/json"}
|
||||
|
||||
69
scripts/send_official_whatsapp.py
Normal file
69
scripts/send_official_whatsapp.py
Normal file
@ -0,0 +1,69 @@
|
||||
import urllib.request
|
||||
import json
|
||||
import ssl
|
||||
import sys
|
||||
|
||||
if sys.platform == 'win32':
|
||||
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
||||
sys.stderr.reconfigure(encoding='utf-8', errors='replace')
|
||||
|
||||
EVOLUTION_API_URL = "https://evolution.almeira.com.co"
|
||||
EVOLUTION_API_KEY = "83B5BD230796-4324-AE73-3508F69E279D"
|
||||
INSTANCE_NAME = "DripZone"
|
||||
TARGET_PHONE = "573167455877"
|
||||
APK_URL = "https://drip-zone.online/DripZone.apk"
|
||||
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
|
||||
message_text = (
|
||||
"🚀 *DripZone v1.6 - Dominio drip-zone.online & Correo Oficial info@drip-zone.online Configurados*\n\n"
|
||||
"Hola Carlos, se ha completado el despliegue de las nuevas configuraciones en producción:\n\n"
|
||||
"🌐 *1. Dominio Oficial & Correo Electrónico:*\n"
|
||||
"• URL Principal: *https://drip-zone.online*\n"
|
||||
"• Correo Oficial: *info@drip-zone.online*\n"
|
||||
"• Servidor Traefik y Coolify enlazados con SSL Let's Encrypt activo.\n\n"
|
||||
"📱 *2. Línea Celular Exclusiva DripZone (+57 310 639 0296):*\n"
|
||||
"• Instancia Evolution API *DripZone* activa (`83B5BD230796-4324-AE73-3508F69E279D`).\n"
|
||||
"• Códigos OTP, confirmaciones de compras, soporte y pasarela Bold se emiten desde esta línea oficial.\n\n"
|
||||
"📥 *3. Publicaciones y Catálogo (Tu WhatsApp Personal):*\n"
|
||||
"• La instancia *AutomataAlmeira* continúa recibiendo los mensajes de los grupos de proveedores mayoristas para sincronización inteligente de calzado.\n\n"
|
||||
"💳 *4. Pasarela Bold & QR Bre-B en Producción:*\n"
|
||||
"• Cobro de anticipo de $10.000 COP con generación automática de QR Bre-B y acreditación en vivo.\n\n"
|
||||
"📲 *5. App Nativa Android v1.6 Sincronizada:*\n"
|
||||
"• Compilada con el nuevo dominio y la línea oficial de atención.\n\n"
|
||||
f"🔗 *Descarga Directa del APK:* {APK_URL}\n\n"
|
||||
"Te adjunto el APK compilado para que puedas actualizar la aplicación en tu celular."
|
||||
)
|
||||
|
||||
print("[1/2] Enviando mensaje de texto oficial a Carlos...")
|
||||
req_text = urllib.request.Request(
|
||||
f"{EVOLUTION_API_URL}/message/sendText/{INSTANCE_NAME}",
|
||||
data=json.dumps({"number": TARGET_PHONE, "text": message_text, "delay": 500}).encode("utf-8"),
|
||||
headers={"apikey": EVOLUTION_API_KEY, "Content-Type": "application/json"}
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req_text, context=ctx, timeout=30) as resp:
|
||||
print(" [OK] Mensaje de texto enviado a WhatsApp (Status:", resp.status, ")")
|
||||
except Exception as e:
|
||||
print(" [Error texto WhatsApp]:", e)
|
||||
|
||||
print("[2/2] Enviando archivo APK compilado a WhatsApp...")
|
||||
req_media = urllib.request.Request(
|
||||
f"{EVOLUTION_API_URL}/message/sendMedia/{INSTANCE_NAME}",
|
||||
data=json.dumps({
|
||||
"number": TARGET_PHONE,
|
||||
"mediatype": "document",
|
||||
"mimetype": "application/vnd.android.package-archive",
|
||||
"fileName": "DripZone-v1.6-Official.apk",
|
||||
"caption": "📲 *DripZone Native v1.6 - Dominio drip-zone.online & Correo info@drip-zone.online*",
|
||||
"media": APK_URL
|
||||
}).encode("utf-8"),
|
||||
headers={"apikey": EVOLUTION_API_KEY, "Content-Type": "application/json"}
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req_media, context=ctx, timeout=180) as resp:
|
||||
print(" [OK] Documento APK enviado a WhatsApp (Status:", resp.status, ")")
|
||||
except Exception as e:
|
||||
print(" [Error envío documento APK]:", e)
|
||||
Loading…
Reference in New Issue
Block a user