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}")
|
print(f"[EXIT STATUS]: {status}")
|
||||||
client.close()
|
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__":
|
if __name__ == "__main__":
|
||||||
deploy()
|
deploy()
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
<!-- Permissions: Least privilege model -->
|
<!-- Permissions: Least privilege model -->
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<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 for secure intent resolution (Android 11+ package visibility) -->
|
||||||
<queries>
|
<queries>
|
||||||
|
|||||||
@ -12,15 +12,19 @@ import androidx.lifecycle.lifecycleScope
|
|||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import co.dripzone.app.api.ApiClient
|
import co.dripzone.app.api.ApiClient
|
||||||
import co.dripzone.app.databinding.ActivityMainBinding
|
import co.dripzone.app.databinding.ActivityMainBinding
|
||||||
|
import co.dripzone.app.manager.TemplateManager
|
||||||
import co.dripzone.app.model.CartManager
|
import co.dripzone.app.model.CartManager
|
||||||
import co.dripzone.app.model.Category
|
import co.dripzone.app.model.Category
|
||||||
import co.dripzone.app.model.City
|
import co.dripzone.app.model.City
|
||||||
import co.dripzone.app.model.Product
|
import co.dripzone.app.model.Product
|
||||||
|
import co.dripzone.app.model.UserManager
|
||||||
import co.dripzone.app.security.SecurityManager
|
import co.dripzone.app.security.SecurityManager
|
||||||
|
import co.dripzone.app.ui.AccountBottomSheet
|
||||||
import co.dripzone.app.ui.CartBottomSheet
|
import co.dripzone.app.ui.CartBottomSheet
|
||||||
import co.dripzone.app.ui.CategoryAdapter
|
import co.dripzone.app.ui.CategoryAdapter
|
||||||
import co.dripzone.app.ui.CitySelectorBottomSheet
|
import co.dripzone.app.ui.CitySelectorBottomSheet
|
||||||
import co.dripzone.app.ui.CustomOrderBottomSheet
|
import co.dripzone.app.ui.CustomOrderBottomSheet
|
||||||
|
import co.dripzone.app.ui.OrdersBottomSheet
|
||||||
import co.dripzone.app.ui.ProductAdapter
|
import co.dripzone.app.ui.ProductAdapter
|
||||||
import co.dripzone.app.ui.ProductDetailBottomSheet
|
import co.dripzone.app.ui.ProductDetailBottomSheet
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
@ -58,10 +62,17 @@ class MainActivity : AppCompatActivity() {
|
|||||||
setupCitySelector()
|
setupCitySelector()
|
||||||
setupBottomDock()
|
setupBottomDock()
|
||||||
setupHeaderListeners()
|
setupHeaderListeners()
|
||||||
|
updateAccountButton()
|
||||||
|
|
||||||
|
co.dripzone.app.manager.AppNotificationManager.createNotificationChannel(this)
|
||||||
|
TemplateManager.syncTemplates(this)
|
||||||
updateCartBadges()
|
updateCartBadges()
|
||||||
loadProducts(forceRefresh = false)
|
loadProducts(forceRefresh = false)
|
||||||
loadCities()
|
loadCities()
|
||||||
|
|
||||||
|
if (intent?.getStringExtra("OPEN_TAB") == "orders") {
|
||||||
|
openOrders()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
@ -223,10 +234,10 @@ class MainActivity : AppCompatActivity() {
|
|||||||
openCart()
|
openCart()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Encargos
|
// 4. Mis Pedidos (Historial y Seguimiento)
|
||||||
binding.navBtnEncargos.setOnClickListener {
|
binding.navBtnOrders.setOnClickListener {
|
||||||
CustomOrderBottomSheet.newInstance()
|
setDockActive("orders")
|
||||||
.show(supportFragmentManager, CustomOrderBottomSheet.TAG)
|
openOrders()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. WhatsApp Asesor
|
// 5. WhatsApp Asesor
|
||||||
@ -244,6 +255,9 @@ class MainActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
binding.ivNavSearch.setColorFilter(if (tab == "search") fuchsia else muted)
|
binding.ivNavSearch.setColorFilter(if (tab == "search") fuchsia else muted)
|
||||||
binding.tvNavSearch.setTextColor(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() {
|
private fun setupHeaderListeners() {
|
||||||
@ -252,9 +266,9 @@ class MainActivity : AppCompatActivity() {
|
|||||||
openCart()
|
openCart()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Header WhatsApp button
|
// Header Account Button (Mi Cuenta / Login)
|
||||||
binding.btnHeaderWhatsApp.setOnClickListener {
|
binding.btnHeaderAccount.setOnClickListener {
|
||||||
openGeneralWhatsApp()
|
openAccount()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SwipeRefresh: pulls latest live drops from suppliers
|
// 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() {
|
private fun openCart() {
|
||||||
CartBottomSheet.newInstance(selectedCity) {
|
CartBottomSheet.newInstance(selectedCity) {
|
||||||
openCitySelector()
|
openCitySelector()
|
||||||
@ -272,7 +308,23 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun openGeneralWhatsApp() {
|
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(
|
SecurityManager.openWhatsApp(
|
||||||
this,
|
this,
|
||||||
SecurityManager.WHATSAPP_STORE_PHONE,
|
SecurityManager.WHATSAPP_STORE_PHONE,
|
||||||
|
|||||||
@ -423,5 +423,70 @@ object ApiClient {
|
|||||||
Result.failure(e)
|
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)
|
val prefs = SecurityManager.getSecurePreferences(context)
|
||||||
prefs.edit().putBoolean(KEY_PHONE_VERIFIED, isVerified).apply()
|
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.btnFinishCustom.setOnClickListener { dismiss() }
|
||||||
|
|
||||||
binding.btnCustomOpenWhatsApp.setOnClickListener {
|
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(
|
SecurityManager.openWhatsApp(
|
||||||
requireContext(),
|
requireContext(),
|
||||||
SecurityManager.WHATSAPP_STORE_PHONE,
|
SecurityManager.WHATSAPP_STORE_PHONE,
|
||||||
@ -212,7 +224,18 @@ class CustomOrderBottomSheet : BottomSheetDialogFragment() {
|
|||||||
binding.layoutCustomForm.visibility = View.GONE
|
binding.layoutCustomForm.visibility = View.GONE
|
||||||
binding.layoutCustomOtp.visibility = View.GONE
|
binding.layoutCustomOtp.visibility = View.GONE
|
||||||
binding.layoutCustomSuccess.visibility = View.VISIBLE
|
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 ->
|
}.onFailure { err ->
|
||||||
binding.btnVerifyCustomOtp.isEnabled = true
|
binding.btnVerifyCustomOtp.isEnabled = true
|
||||||
Toast.makeText(requireContext(), "Error al registrar: ${err.message}", Toast.LENGTH_LONG).show()
|
Toast.makeText(requireContext(), "Error al registrar: ${err.message}", Toast.LENGTH_LONG).show()
|
||||||
|
|||||||
@ -111,14 +111,9 @@ class OrderVerificationBottomSheet : BottomSheetDialogFragment() {
|
|||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.btnOpenWhatsAppChat.setOnClickListener {
|
binding.btnViewOrderDetails.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
|
|
||||||
)
|
|
||||||
dismiss()
|
dismiss()
|
||||||
|
OrdersBottomSheet().show(parentFragmentManager, OrdersBottomSheet.TAG)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -276,6 +271,26 @@ class OrderVerificationBottomSheet : BottomSheetDialogFragment() {
|
|||||||
createdOrderId = orderId
|
createdOrderId = orderId
|
||||||
UserManager.incrementCompletedOrders(requireContext())
|
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
|
// Limpiar carrito
|
||||||
if (!isDirectProductOrder) {
|
if (!isDirectProductOrder) {
|
||||||
CartManager.clear(requireContext())
|
CartManager.clear(requireContext())
|
||||||
@ -308,7 +323,7 @@ class OrderVerificationBottomSheet : BottomSheetDialogFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Configurar botón para abrir pasarela de pago Bold
|
// 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 {
|
binding.btnPaySeparationBold.setOnClickListener {
|
||||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(checkoutUrl))
|
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(checkoutUrl))
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
@ -326,34 +341,43 @@ class OrderVerificationBottomSheet : BottomSheetDialogFragment() {
|
|||||||
binding.layoutBoldPaidSuccess.visibility = View.VISIBLE
|
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."
|
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()
|
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
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
binding.btnPaySeparationBold.setOnClickListener {
|
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)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.btnOpenWhatsAppChat.text = "Notificar Pago a Carlos en WhatsApp"
|
binding.btnViewOrderDetails.text = "Ver Mis Pedidos 📦"
|
||||||
binding.btnOpenWhatsAppChat.setOnClickListener {
|
binding.btnViewOrderDetails.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)
|
|
||||||
dismiss()
|
dismiss()
|
||||||
|
OrdersBottomSheet().show(parentFragmentManager, OrdersBottomSheet.TAG)
|
||||||
}
|
}
|
||||||
} else {
|
} 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.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.layoutBoldQrContainer.visibility = View.GONE
|
||||||
binding.layoutBoldPaidSuccess.visibility = View.GONE
|
binding.layoutBoldPaidSuccess.visibility = View.GONE
|
||||||
binding.btnPaySeparationBold.visibility = View.GONE
|
binding.btnPaySeparationBold.visibility = View.GONE
|
||||||
binding.btnOpenWhatsAppChat.text = "Coordinar Entrega por WhatsApp"
|
binding.btnViewOrderDetails.text = "Ver Mis Pedidos 📦"
|
||||||
binding.btnOpenWhatsAppChat.setOnClickListener {
|
binding.btnViewOrderDetails.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)
|
|
||||||
dismiss()
|
dismiss()
|
||||||
|
OrdersBottomSheet().show(parentFragmentManager, OrdersBottomSheet.TAG)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.onFailure { err ->
|
}.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()
|
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
|
// Custom order button inside bottom sheet
|
||||||
binding.btnCustomOrderSheet.setOnClickListener {
|
binding.btnCustomOrderSheet.setOnClickListener {
|
||||||
dismiss()
|
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" />
|
android:visibility="gone" />
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|
||||||
<!-- Header WhatsApp Button -->
|
<!-- Header My Account Button -->
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/btnHeaderWhatsApp"
|
android:id="@+id/btnHeaderAccount"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="36dp"
|
android:layout_height="36dp"
|
||||||
android:layout_marginStart="6dp"
|
android:layout_marginStart="6dp"
|
||||||
@ -146,13 +146,14 @@
|
|||||||
android:insetTop="0dp"
|
android:insetTop="0dp"
|
||||||
android:insetBottom="0dp"
|
android:insetBottom="0dp"
|
||||||
android:paddingHorizontal="10dp"
|
android:paddingHorizontal="10dp"
|
||||||
android:text="Asesor"
|
android:text="Mi Cuenta"
|
||||||
android:textAllCaps="false"
|
android:textAllCaps="false"
|
||||||
|
android:textColor="#FFFFFF"
|
||||||
android:textSize="11sp"
|
android:textSize="11sp"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
app:backgroundTint="@color/accent_whatsapp"
|
app:backgroundTint="@color/drip_purple"
|
||||||
app:cornerRadius="10dp"
|
app:cornerRadius="10dp"
|
||||||
app:icon="@drawable/ic_whatsapp"
|
app:icon="@drawable/ic_user"
|
||||||
app:iconGravity="textStart"
|
app:iconGravity="textStart"
|
||||||
app:iconPadding="4dp"
|
app:iconPadding="4dp"
|
||||||
app:iconSize="14dp" />
|
app:iconSize="14dp" />
|
||||||
@ -463,9 +464,9 @@
|
|||||||
android:textSize="10sp" />
|
android:textSize="10sp" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<!-- 4. Encargos Especiales -->
|
<!-- 4. Mis Pedidos (Rastreo y Detalle) -->
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/navBtnEncargos"
|
android:id="@+id/navBtnOrders"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
@ -475,19 +476,19 @@
|
|||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/ivNavEncargos"
|
android:id="@+id/ivNavOrders"
|
||||||
android:layout_width="22dp"
|
android:layout_width="22dp"
|
||||||
android:layout_height="22dp"
|
android:layout_height="22dp"
|
||||||
android:src="@drawable/ic_encargos"
|
android:src="@drawable/ic_package"
|
||||||
app:tint="@color/text_secondary" />
|
app:tint="@color/text_secondary" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/tvNavEncargos"
|
android:id="@+id/tvNavOrders"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="2dp"
|
android:layout_marginTop="2dp"
|
||||||
android:fontFamily="sans-serif-medium"
|
android:fontFamily="sans-serif-medium"
|
||||||
android:text="Encargos"
|
android:text="Mis Pedidos"
|
||||||
android:textColor="@color/text_secondary"
|
android:textColor="@color/text_secondary"
|
||||||
android:textSize="10sp" />
|
android:textSize="10sp" />
|
||||||
</LinearLayout>
|
</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_height="46dp"
|
||||||
android:layout_marginTop="6dp"
|
android:layout_marginTop="6dp"
|
||||||
android:background="@drawable/bg_search_input"
|
android:background="@drawable/bg_search_input"
|
||||||
android:hint="Ej. Juan Carlos Pérez"
|
android:hint="Ej. Juan Gómez"
|
||||||
android:inputType="textPersonName"
|
android:inputType="textPersonName"
|
||||||
android:paddingHorizontal="14dp"
|
android:paddingHorizontal="14dp"
|
||||||
android:textColor="@color/text_primary"
|
android:textColor="@color/text_primary"
|
||||||
@ -319,7 +319,7 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="10dp"
|
android:layout_marginTop="10dp"
|
||||||
android:gravity="center"
|
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:textColor="@color/text_secondary"
|
||||||
android:textSize="12sp" />
|
android:textSize="12sp" />
|
||||||
|
|
||||||
@ -440,19 +440,19 @@
|
|||||||
app:iconSize="18dp" />
|
app:iconSize="18dp" />
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/btnOpenWhatsAppChat"
|
android:id="@+id/btnViewOrderDetails"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="50dp"
|
android:layout_height="50dp"
|
||||||
android:layout_marginTop="10dp"
|
android:layout_marginTop="10dp"
|
||||||
android:filterTouchesWhenObscured="true"
|
android:filterTouchesWhenObscured="true"
|
||||||
android:text="Notificar Pago por WhatsApp"
|
android:text="Ver Estado de Mi Pedido"
|
||||||
android:textAllCaps="false"
|
android:textAllCaps="false"
|
||||||
android:textColor="#FFFFFF"
|
android:textColor="#FFFFFF"
|
||||||
android:textSize="13sp"
|
android:textSize="13sp"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
app:backgroundTint="@color/accent_whatsapp"
|
app:backgroundTint="@color/drip_purple"
|
||||||
app:cornerRadius="14dp"
|
app:cornerRadius="14dp"
|
||||||
app:icon="@drawable/ic_whatsapp"
|
app:icon="@drawable/ic_package"
|
||||||
app:iconGravity="textStart"
|
app:iconGravity="textStart"
|
||||||
app:iconPadding="8dp"
|
app:iconPadding="8dp"
|
||||||
app:iconSize="18dp" />
|
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:iconPadding="8dp"
|
||||||
app:iconSize="20dp" />
|
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 -->
|
<!-- Secondary Custom Request Button -->
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/btnCustomOrderSheet"
|
android:id="@+id/btnCustomOrderSheet"
|
||||||
|
|||||||
@ -74,7 +74,7 @@
|
|||||||
android:layout_height="46dp"
|
android:layout_height="46dp"
|
||||||
android:layout_marginTop="4dp"
|
android:layout_marginTop="4dp"
|
||||||
android:background="@drawable/bg_search_input"
|
android:background="@drawable/bg_search_input"
|
||||||
android:hint="Ej. Carlos Mendoza"
|
android:hint="Ej. Andrés Morales"
|
||||||
android:inputType="textPersonName"
|
android:inputType="textPersonName"
|
||||||
android:paddingHorizontal="14dp"
|
android:paddingHorizontal="14dp"
|
||||||
android:textColor="@color/text_primary"
|
android:textColor="@color/text_primary"
|
||||||
@ -326,7 +326,7 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="8dp"
|
android:layout_marginTop="8dp"
|
||||||
android:gravity="center"
|
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:textColor="@color/text_secondary"
|
||||||
android:textSize="12sp" />
|
android:textSize="12sp" />
|
||||||
|
|
||||||
@ -336,7 +336,7 @@
|
|||||||
android:layout_height="50dp"
|
android:layout_height="50dp"
|
||||||
android:layout_marginTop="18dp"
|
android:layout_marginTop="18dp"
|
||||||
android:filterTouchesWhenObscured="true"
|
android:filterTouchesWhenObscured="true"
|
||||||
android:text="Abrir WhatsApp del Asesor Carlos"
|
android:text="Contactar a Soporte DripZone"
|
||||||
android:textAllCaps="false"
|
android:textAllCaps="false"
|
||||||
android:textColor="#FFFFFF"
|
android:textColor="#FFFFFF"
|
||||||
android:textSize="13sp"
|
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:textColor="@color/text_secondary"
|
||||||
android:textSize="11sp" />
|
android:textSize="11sp" />
|
||||||
|
|
||||||
<!-- Price & Action Button -->
|
<!-- Price (Full Width in single line, no clipping) -->
|
||||||
<LinearLayout
|
<TextView
|
||||||
|
android:id="@+id/tvProductPrice"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="8dp"
|
android:layout_marginTop="8dp"
|
||||||
android:gravity="center_vertical"
|
|
||||||
android:orientation="horizontal">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/tvProductPrice"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:fontFamily="sans-serif-black"
|
android:fontFamily="sans-serif-black"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:singleLine="true"
|
||||||
android:text="$ 280.000"
|
android:text="$ 280.000"
|
||||||
android:textColor="@color/drip_purple_glow"
|
android:textColor="@color/drip_purple_glow"
|
||||||
android:textSize="14sp" />
|
android:textSize="15sp" />
|
||||||
|
|
||||||
<!-- Quick Order Button with Tapjacking Protection -->
|
<!-- Quick Order Button directly below price -->
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/btnQuickOrder"
|
android:id="@+id/btnQuickOrder"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="36dp"
|
android:layout_height="38dp"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
android:filterTouchesWhenObscured="true"
|
android:filterTouchesWhenObscured="true"
|
||||||
android:insetTop="0dp"
|
android:insetTop="0dp"
|
||||||
android:insetBottom="0dp"
|
android:insetBottom="0dp"
|
||||||
android:paddingHorizontal="10dp"
|
android:paddingHorizontal="8dp"
|
||||||
android:text="Pedir"
|
android:text="Pedir"
|
||||||
android:textAllCaps="false"
|
android:textAllCaps="false"
|
||||||
android:textSize="11sp"
|
android:textSize="12sp"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
app:backgroundTint="@color/accent_whatsapp"
|
app:backgroundTint="@color/accent_whatsapp"
|
||||||
app:cornerRadius="10dp"
|
app:cornerRadius="10dp"
|
||||||
app:icon="@drawable/ic_whatsapp"
|
app:icon="@drawable/ic_whatsapp"
|
||||||
app:iconGravity="textStart"
|
app:iconGravity="textStart"
|
||||||
app:iconPadding="4dp"
|
app:iconPadding="6dp"
|
||||||
app:iconSize="14dp" />
|
app:iconSize="15dp" />
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
|||||||
@ -15,6 +15,8 @@
|
|||||||
|
|
||||||
<!-- Web DripZone Purple & Fuchsia Accents -->
|
<!-- Web DripZone Purple & Fuchsia Accents -->
|
||||||
<color name="drip_purple">#9333EA</color>
|
<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_purple_light">#A855F7</color>
|
||||||
<color name="drip_fuchsia">#D946EF</color>
|
<color name="drip_fuchsia">#D946EF</color>
|
||||||
<color name="drip_pink">#EC4899</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',
|
instagramHandle: '@dripzone.co',
|
||||||
address: 'Bodega Central / Cali',
|
address: 'Bodega Central / Cali',
|
||||||
city: 'Cali, Colombia',
|
city: 'Cali, Colombia',
|
||||||
nequiAccount: '316 745 5877',
|
nequiAccount: '310 639 0296',
|
||||||
daviplataAccount: '316 745 5877',
|
daviplataAccount: '310 639 0296',
|
||||||
accountHolder: 'Carlos Andrés Lasso',
|
accountHolder: 'DripZone Oficial',
|
||||||
boldPaymentLink: 'https://bold.co/p/carlos-lasso',
|
boldPaymentLink: 'https://drip-zone.online',
|
||||||
boldApiKey: '',
|
boldApiKey: '',
|
||||||
boldMerchantId: '',
|
boldMerchantId: '',
|
||||||
boldQrUrl: '',
|
boldQrUrl: '',
|
||||||
|
|||||||
@ -11,22 +11,35 @@ export async function POST(request: Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const cleanInput = identifier.trim();
|
const cleanInput = identifier.trim();
|
||||||
// Buscar usuario en base de datos
|
const cleanDigits = cleanInput.replace(/\D/g, '');
|
||||||
const user = dataService.authenticate(cleanInput);
|
|
||||||
|
|
||||||
let phoneToSend = cleanInput.replace(/\D/g, '');
|
let phoneToSend = '';
|
||||||
let userName = name?.trim() || (user ? user.name : 'Cliente');
|
let userName = name?.trim() || 'Cliente';
|
||||||
|
let foundUser = null;
|
||||||
|
|
||||||
if (user) {
|
// Si el identificador es un correo electrónico, buscar el teléfono de su cuenta
|
||||||
if (!user.isActive) {
|
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 });
|
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(
|
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 }
|
{ status: 400 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -42,16 +55,14 @@ export async function POST(request: Request) {
|
|||||||
codigo_otp: code,
|
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);
|
const sent = await sendWhatsAppMessage(phoneToSend, message);
|
||||||
|
|
||||||
|
|
||||||
if (!sent) {
|
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}`);
|
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
|
const maskedPhone = phoneToSend.length >= 10
|
||||||
? `${phoneToSend.slice(0, 3)}***${phoneToSend.slice(-4)}`
|
? `${phoneToSend.slice(0, 3)}***${phoneToSend.slice(-4)}`
|
||||||
: 'tu número';
|
: 'tu número';
|
||||||
@ -60,7 +71,7 @@ export async function POST(request: Request) {
|
|||||||
success: true,
|
success: true,
|
||||||
message: `Código de 6 dígitos enviado por WhatsApp a ${maskedPhone}`,
|
message: `Código de 6 dígitos enviado por WhatsApp a ${maskedPhone}`,
|
||||||
phone: maskedPhone,
|
phone: maskedPhone,
|
||||||
isRegistered: !!user,
|
isRegistered: !!foundUser,
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Error enviando OTP:', error);
|
console.error('Error enviando OTP:', error);
|
||||||
|
|||||||
@ -51,28 +51,36 @@ export async function GET(request: Request) {
|
|||||||
dataService.updateOrder(order.id, updates);
|
dataService.updateOrder(order.id, updates);
|
||||||
await updateOrderInAppwrite(order.id, updates).catch(e => console.warn('Appwrite update warn:', e));
|
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 {
|
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 = (
|
const adminMsg = (
|
||||||
`⚡ *¡ANTICIPO DE SEPARACIÓN CONFIRMADO VÍA BOLD!*\n\n` +
|
`⚡ *¡ANTICIPO DE SEPARACIÓN CONFIRMADO VÍA BOLD!*\n\n` +
|
||||||
`📦 *Orden:* #${order.id}\n` +
|
`📦 *Orden:* #${order.id}\n` +
|
||||||
`👤 *Cliente:* ${order.customerName} (+57 ${order.customerPhone})\n` +
|
`👤 *Cliente:* ${order.customerName} (+57 ${order.customerPhone})\n` +
|
||||||
`📍 *Ciudad:* ${order.cityName}\n` +
|
`📍 *Destino:* ${order.cityName} — ${order.address || 'Domicilio'}\n` +
|
||||||
`💳 *Anticipo Recibido:* ${formatCOP(depositAmount)} COP (Confirmado Bold)\n` +
|
`💳 *Anticipo Recibido:* ${formatCOP(depositAmount)} COP (Acreditado por Bold)\n` +
|
||||||
`🤝 *Saldo a Cobrar Contraentrega:* ${formatCOP(newBalance)} COP\n\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.`
|
`🚀 *Estado:* Despacho autorizado y habilitado para alistamiento en bodega.`
|
||||||
);
|
);
|
||||||
await sendWhatsAppMessage('573167455877', adminMsg);
|
await sendWhatsAppMessage('573167455877', adminMsg);
|
||||||
|
|
||||||
// Notificar al cliente
|
// Notificar al cliente automáticamente con la plantilla registrada en la base de datos
|
||||||
if (order.customerPhone) {
|
if (order.customerPhone) {
|
||||||
const clientMsg = (
|
const clientMsg = dataService.renderMessageTemplate('customer_deposit_confirmed', {
|
||||||
`🎉 *¡Tu anticipo de separación fue confirmado con éxito!*\n\n` +
|
tienda_nombre: 'DripZone',
|
||||||
`Hola *${order.customerName}*, recibimos correctamente tu pago de *${formatCOP(depositAmount)} COP* a través de Bold.\n\n` +
|
cliente_nombre: order.customerName || 'Cliente',
|
||||||
`📦 Tu orden *#${order.id}* ya se encuentra en alistamiento y empaque para despacho a *${order.cityName}*.\n` +
|
orden_id: order.id,
|
||||||
`🤝 *Saldo restante a pagar contraentrega:* *${formatCOP(newBalance)} COP* al recibir en tu puerta.\n\n` +
|
valor_anticipo: formatCOP(depositAmount),
|
||||||
`¡Muchas gracias por tu compra en DripZone!`
|
cliente_ciudad: order.cityName || 'Colombia',
|
||||||
);
|
saldo_contraentrega: formatCOP(newBalance),
|
||||||
|
});
|
||||||
await sendWhatsAppMessage(order.customerPhone, clientMsg);
|
await sendWhatsAppMessage(order.customerPhone, clientMsg);
|
||||||
}
|
}
|
||||||
} catch (waErr) {
|
} catch (waErr) {
|
||||||
|
|||||||
@ -4,9 +4,7 @@ import { CustomProductRequest } from '@/types';
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
const EVOLUTION_API_URL = process.env.EVOLUTION_API_URL || 'https://evolution.almeira.com.co';
|
import { sendWhatsAppMessage } from '@/lib/evolution-api';
|
||||||
const EVOLUTION_API_KEY = process.env.EVOLUTION_API_KEY || 'EE49029EEB85-4568-8F81-C9FDFC6E2943';
|
|
||||||
const EVOLUTION_INSTANCE = process.env.EVOLUTION_INSTANCE_NAME || 'AutomataAlmeira';
|
|
||||||
const CARLOS_PHONE = '573167455877';
|
const CARLOS_PHONE = '573167455877';
|
||||||
|
|
||||||
// Carpeta local para almacenar fotos de encargos especiales
|
// 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) {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const { searchParams } = new URL(request.url);
|
const { searchParams } = new URL(request.url);
|
||||||
@ -142,13 +116,15 @@ ${customerEmail ? `✉️ *Correo:* ${customerEmail}\n` : ''}
|
|||||||
sendWhatsAppMessage(CARLOS_PHONE, adminMessage).catch(() => {});
|
sendWhatsAppMessage(CARLOS_PHONE, adminMessage).catch(() => {});
|
||||||
sendWhatsAppMessage('120363428012905103@g.us', adminMessage).catch(() => {});
|
sendWhatsAppMessage('120363428012905103@g.us', adminMessage).catch(() => {});
|
||||||
|
|
||||||
// 2. Confirmación automática al cliente por WhatsApp
|
// 2. Confirmación automática al cliente por WhatsApp usando la plantilla registrada
|
||||||
const clientMessage = `👋 ¡Hola *${customerName}*!
|
const settings = dataService.getSettings();
|
||||||
Recibimos con éxito tu solicitud por encargo para las *${brand} ${modelName}* en talla *${size}*. 👟🔥
|
const clientMessage = dataService.renderMessageTemplate('customer_request_received', {
|
||||||
|
tienda_nombre: settings?.storeName || 'DripZone Sneakers',
|
||||||
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.
|
cliente_nombre: customerName,
|
||||||
|
marca: brand,
|
||||||
¡Gracias por confiar en DripZone! 🚀`;
|
modelo: modelName,
|
||||||
|
talla: size,
|
||||||
|
});
|
||||||
|
|
||||||
sendWhatsAppMessage(customerPhone, clientMessage).catch(() => {});
|
sendWhatsAppMessage(customerPhone, clientMessage).catch(() => {});
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { dataService } from '@/lib/data-service';
|
import { dataService } from '@/lib/data-service';
|
||||||
import { TEMPLATE_CATEGORIES } from '@/lib/templates-service';
|
import { TEMPLATE_CATEGORIES } from '@/lib/templates-service';
|
||||||
|
import { getMessageTemplatesFromAppwrite, updateMessageTemplateInAppwrite } from '@/lib/server-appwrite';
|
||||||
|
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: Request) {
|
||||||
try {
|
try {
|
||||||
@ -8,6 +9,16 @@ export async function GET(request: Request) {
|
|||||||
const id = searchParams.get('id');
|
const id = searchParams.get('id');
|
||||||
const category = searchParams.get('category');
|
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) {
|
if (id) {
|
||||||
const template = dataService.getMessageTemplateById(id);
|
const template = dataService.getMessageTemplateById(id);
|
||||||
if (!template) {
|
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 });
|
return NextResponse.json({ success: false, error: 'ID de la plantilla es obligatorio' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const updated = dataService.updateMessageTemplate(id, {
|
const updates = {
|
||||||
...(content !== undefined && { content }),
|
...(content !== undefined && { content }),
|
||||||
...(isActive !== undefined && { isActive }),
|
...(isActive !== undefined && { isActive }),
|
||||||
...(title !== undefined && { title }),
|
...(title !== undefined && { title }),
|
||||||
...(description !== undefined && { description }),
|
...(description !== undefined && { description }),
|
||||||
});
|
};
|
||||||
|
|
||||||
|
// Actualizar en Appwrite
|
||||||
|
const updatedInDb = await updateMessageTemplateInAppwrite(id, updates);
|
||||||
|
|
||||||
|
// Actualizar en memoria / dataService
|
||||||
|
const updated = dataService.updateMessageTemplate(id, updates);
|
||||||
|
|
||||||
if (!updated) {
|
if (!updated) {
|
||||||
return NextResponse.json({ success: false, error: 'Plantilla no encontrada' }, { status: 404 });
|
return NextResponse.json({ success: false, error: 'Plantilla no encontrada' }, { status: 404 });
|
||||||
@ -50,8 +67,8 @@ export async function PUT(request: Request) {
|
|||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
template: updated,
|
template: updatedInDb || updated,
|
||||||
message: `Plantilla "${updated.title}" actualizada correctamente.`,
|
message: `Plantilla "${updated.title}" actualizada correctamente en la base de datos.`,
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
return NextResponse.json({ success: false, error: error.message }, { status: 400 });
|
return NextResponse.json({ success: false, error: error.message }, { status: 400 });
|
||||||
|
|||||||
@ -12,12 +12,38 @@ import {
|
|||||||
} from '@/lib/evolution-api';
|
} from '@/lib/evolution-api';
|
||||||
import { dataService } from '@/lib/data-service';
|
import { dataService } from '@/lib/data-service';
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET(request: Request) {
|
||||||
try {
|
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 });
|
return NextResponse.json({ success: true, orders });
|
||||||
} catch (error: any) {
|
} 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 });
|
return NextResponse.json({ success: true, orders: fallback });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -123,8 +123,8 @@ export default function CheckoutPage() {
|
|||||||
const depositAmount = isMandatoryDeposit ? 10000 : 0;
|
const depositAmount = isMandatoryDeposit ? 10000 : 0;
|
||||||
const balanceDue = Math.max(0, totalAmount - depositAmount);
|
const balanceDue = Math.max(0, totalAmount - depositAmount);
|
||||||
|
|
||||||
const officialWhatsapp = storeSettings?.whatsappNumber || '573167455877';
|
const officialWhatsapp = storeSettings?.whatsappNumber || '573106390296';
|
||||||
const boldLink = storeSettings?.boldPaymentLink || 'https://bold.co/p/carlos-lasso';
|
const boldLink = storeSettings?.boldPaymentLink || 'https://drip-zone.online';
|
||||||
|
|
||||||
const processOrderSubmission = async () => {
|
const processOrderSubmission = async () => {
|
||||||
setIsSubmitting(true);
|
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">
|
<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>
|
<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>
|
||||||
</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.',
|
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,
|
requiresDeposit: false,
|
||||||
depositAmount: 0,
|
depositAmount: 0,
|
||||||
boldPaymentLink: 'https://bold.co/p/carlos-lasso',
|
boldPaymentLink: 'https://drip-zone.online',
|
||||||
accountInfo: 'Pasarela Oficial Bold.co • PSE / Tarjetas / QR',
|
accountInfo: 'Pasarela Oficial Bold.co • PSE / Tarjetas / QR Bre-B',
|
||||||
isActive: true,
|
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.',
|
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,
|
requiresDeposit: true,
|
||||||
depositAmount: 10000,
|
depositAmount: 10000,
|
||||||
accountInfo: 'Nequi / Daviplata Oficial: 316 745 5877 (Carlos Andrés Lasso)',
|
accountInfo: 'Nequi / Daviplata Oficial DripZone: 310 639 0296',
|
||||||
boldPaymentLink: 'https://bold.co/p/carlos-lasso',
|
boldPaymentLink: 'https://drip-zone.online',
|
||||||
isActive: true,
|
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.',
|
description: 'Paga el 100% anticipado y recibe prioridad de despacho express en 24 horas.',
|
||||||
requiresDeposit: false,
|
requiresDeposit: false,
|
||||||
depositAmount: 0,
|
depositAmount: 0,
|
||||||
accountInfo: 'Nequi / Daviplata: 316 745 5877 (Carlos Andrés Lasso)',
|
accountInfo: 'Nequi / Daviplata Oficial DripZone: 310 639 0296',
|
||||||
isActive: true,
|
isActive: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@ -172,10 +172,10 @@ export const INITIAL_ORDERS: Order[] = [];
|
|||||||
|
|
||||||
export const INITIAL_USERS: User[] = [
|
export const INITIAL_USERS: User[] = [
|
||||||
{
|
{
|
||||||
id: 'usr-admin-carlos',
|
id: 'usr-admin-dripzone',
|
||||||
name: 'Carlos Lasso',
|
name: 'Administrador DripZone',
|
||||||
email: 'carlos.andres.lasso@gmail.com',
|
email: 'info@drip-zone.online',
|
||||||
phone: '573167455877',
|
phone: '573106390296',
|
||||||
city: 'Cali',
|
city: 'Cali',
|
||||||
address: 'Sede Principal / Cali',
|
address: 'Sede Principal / Cali',
|
||||||
role: 'admin',
|
role: 'admin',
|
||||||
@ -210,53 +210,84 @@ function getPathModule() {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPersistedFilePath(): string | null {
|
function getPersistedFilePath(fileName: string = 'persisted_products.json'): string | null {
|
||||||
const pathMod = getPathModule();
|
const pathMod = getPathModule();
|
||||||
if (!pathMod) return null;
|
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 fsMod = getFsModule();
|
||||||
const filePath = getPersistedFilePath();
|
const filePath = getPersistedFilePath(fileName);
|
||||||
if (fsMod && filePath && fsMod.existsSync(filePath)) {
|
if (fsMod && filePath && fsMod.existsSync(filePath)) {
|
||||||
try {
|
try {
|
||||||
const content = fsMod.readFileSync(filePath, 'utf-8');
|
const content = fsMod.readFileSync(filePath, 'utf-8');
|
||||||
const parsed = JSON.parse(content);
|
const parsed = JSON.parse(content);
|
||||||
if (Array.isArray(parsed) && parsed.length > 0) {
|
if (parsed !== undefined && parsed !== null) {
|
||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} 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 fsMod = getFsModule();
|
||||||
const pathMod = getPathModule();
|
const pathMod = getPathModule();
|
||||||
const filePath = getPersistedFilePath();
|
const filePath = getPersistedFilePath(fileName);
|
||||||
if (fsMod && pathMod && filePath) {
|
if (fsMod && pathMod && filePath) {
|
||||||
try {
|
try {
|
||||||
const dir = pathMod.dirname(filePath);
|
const dir = pathMod.dirname(filePath);
|
||||||
if (!fsMod.existsSync(dir)) {
|
if (!fsMod.existsSync(dir)) {
|
||||||
fsMod.mkdirSync(dir, { recursive: true });
|
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) {
|
} 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 {
|
class DataService {
|
||||||
private products: Product[] = loadPersistedProducts();
|
private products: Product[] = loadPersistedProducts();
|
||||||
private cities: City[] = [...INITIAL_CITIES];
|
private cities: City[] = loadPersistedData<City[]>('persisted_cities.json', [...INITIAL_CITIES]);
|
||||||
private paymentMethods: PaymentMethod[] = [...INITIAL_PAYMENT_METHODS];
|
private paymentMethods: PaymentMethod[] = loadPersistedData<PaymentMethod[]>('persisted_payment_methods.json', [...INITIAL_PAYMENT_METHODS]);
|
||||||
private suppliers: Supplier[] = [...INITIAL_SUPPLIERS];
|
private suppliers: Supplier[] = loadPersistedData<Supplier[]>('persisted_suppliers.json', [...INITIAL_SUPPLIERS]);
|
||||||
private orders: Order[] = [...INITIAL_ORDERS];
|
private orders: Order[] = loadPersistedData<Order[]>('persisted_orders.json', [...INITIAL_ORDERS]);
|
||||||
private supplierPosts: SupplierPost[] = [...INITIAL_SUPPLIER_POSTS];
|
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[] {
|
getProducts(): Product[] {
|
||||||
if (this.products.length === 0) {
|
if (this.products.length === 0) {
|
||||||
@ -661,6 +692,7 @@ class DataService {
|
|||||||
|
|
||||||
addCity(city: City): City {
|
addCity(city: City): City {
|
||||||
this.cities.push(city);
|
this.cities.push(city);
|
||||||
|
savePersistedData('persisted_cities.json', this.cities);
|
||||||
return city;
|
return city;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -668,12 +700,14 @@ class DataService {
|
|||||||
const idx = this.cities.findIndex((c) => c.id === id);
|
const idx = this.cities.findIndex((c) => c.id === id);
|
||||||
if (idx === -1) return null;
|
if (idx === -1) return null;
|
||||||
this.cities[idx] = { ...this.cities[idx], ...updates };
|
this.cities[idx] = { ...this.cities[idx], ...updates };
|
||||||
|
savePersistedData('persisted_cities.json', this.cities);
|
||||||
return this.cities[idx];
|
return this.cities[idx];
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteCity(id: string): boolean {
|
deleteCity(id: string): boolean {
|
||||||
const initialLen = this.cities.length;
|
const initialLen = this.cities.length;
|
||||||
this.cities = this.cities.filter((c) => c.id !== id);
|
this.cities = this.cities.filter((c) => c.id !== id);
|
||||||
|
savePersistedData('persisted_cities.json', this.cities);
|
||||||
return this.cities.length < initialLen;
|
return this.cities.length < initialLen;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -687,6 +721,7 @@ class DataService {
|
|||||||
|
|
||||||
addPaymentMethod(method: PaymentMethod): PaymentMethod {
|
addPaymentMethod(method: PaymentMethod): PaymentMethod {
|
||||||
this.paymentMethods.push(method);
|
this.paymentMethods.push(method);
|
||||||
|
savePersistedData('persisted_payment_methods.json', this.paymentMethods);
|
||||||
return method;
|
return method;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -694,12 +729,14 @@ class DataService {
|
|||||||
const idx = this.paymentMethods.findIndex((p) => p.id === id);
|
const idx = this.paymentMethods.findIndex((p) => p.id === id);
|
||||||
if (idx === -1) return null;
|
if (idx === -1) return null;
|
||||||
this.paymentMethods[idx] = { ...this.paymentMethods[idx], ...updates };
|
this.paymentMethods[idx] = { ...this.paymentMethods[idx], ...updates };
|
||||||
|
savePersistedData('persisted_payment_methods.json', this.paymentMethods);
|
||||||
return this.paymentMethods[idx];
|
return this.paymentMethods[idx];
|
||||||
}
|
}
|
||||||
|
|
||||||
deletePaymentMethod(id: string): boolean {
|
deletePaymentMethod(id: string): boolean {
|
||||||
const initialLen = this.paymentMethods.length;
|
const initialLen = this.paymentMethods.length;
|
||||||
this.paymentMethods = this.paymentMethods.filter((p) => p.id !== id);
|
this.paymentMethods = this.paymentMethods.filter((p) => p.id !== id);
|
||||||
|
savePersistedData('persisted_payment_methods.json', this.paymentMethods);
|
||||||
return this.paymentMethods.length < initialLen;
|
return this.paymentMethods.length < initialLen;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -713,6 +750,7 @@ class DataService {
|
|||||||
|
|
||||||
addOrder(order: Order): Order {
|
addOrder(order: Order): Order {
|
||||||
this.orders.unshift(order);
|
this.orders.unshift(order);
|
||||||
|
savePersistedData('persisted_orders.json', this.orders);
|
||||||
return order;
|
return order;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -737,6 +775,7 @@ class DataService {
|
|||||||
createdAt: orderData.createdAt || new Date().toISOString(),
|
createdAt: orderData.createdAt || new Date().toISOString(),
|
||||||
};
|
};
|
||||||
this.orders.unshift(newOrder);
|
this.orders.unshift(newOrder);
|
||||||
|
savePersistedData('persisted_orders.json', this.orders);
|
||||||
return newOrder;
|
return newOrder;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -756,12 +795,14 @@ class DataService {
|
|||||||
depositAmount: deposit,
|
depositAmount: deposit,
|
||||||
balanceDue,
|
balanceDue,
|
||||||
};
|
};
|
||||||
|
savePersistedData('persisted_orders.json', this.orders);
|
||||||
return this.orders[idx];
|
return this.orders[idx];
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteOrder(id: string): boolean {
|
deleteOrder(id: string): boolean {
|
||||||
const initialLen = this.orders.length;
|
const initialLen = this.orders.length;
|
||||||
this.orders = this.orders.filter((o) => o.id !== id);
|
this.orders = this.orders.filter((o) => o.id !== id);
|
||||||
|
savePersistedData('persisted_orders.json', this.orders);
|
||||||
return this.orders.length < initialLen;
|
return this.orders.length < initialLen;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -850,33 +891,13 @@ class DataService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Configuración de la Tienda
|
// 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 {
|
getSettings(): StoreSettings {
|
||||||
return this.settings;
|
return this.settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSettings(updates: Partial<StoreSettings>): StoreSettings {
|
updateSettings(updates: Partial<StoreSettings>): StoreSettings {
|
||||||
this.settings = { ...this.settings, ...updates };
|
this.settings = { ...this.settings, ...updates };
|
||||||
|
savePersistedData('persisted_settings.json', this.settings);
|
||||||
return this.settings;
|
return this.settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -912,9 +933,15 @@ class DataService {
|
|||||||
|
|
||||||
authenticate(emailOrPhone: string): User | null {
|
authenticate(emailOrPhone: string): User | null {
|
||||||
const q = emailOrPhone.trim().toLowerCase();
|
const q = emailOrPhone.trim().toLowerCase();
|
||||||
const user = this.users.find(
|
const cleanDigits = q.replace(/\D/g, '');
|
||||||
(u) => u.email.toLowerCase() === q || u.phone === q || u.phone.includes(q)
|
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;
|
return user || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -941,8 +968,6 @@ class DataService {
|
|||||||
// ==========================================
|
// ==========================================
|
||||||
// SISTEMA DE PLANTILLAS CATEGORIZADAS
|
// SISTEMA DE PLANTILLAS CATEGORIZADAS
|
||||||
// ==========================================
|
// ==========================================
|
||||||
private messageTemplates: MessageTemplate[] = [...DEFAULT_MESSAGE_TEMPLATES];
|
|
||||||
|
|
||||||
getMessageTemplates(category?: string): MessageTemplate[] {
|
getMessageTemplates(category?: string): MessageTemplate[] {
|
||||||
if (!category || category === 'all') {
|
if (!category || category === 'all') {
|
||||||
return this.messageTemplates;
|
return this.messageTemplates;
|
||||||
@ -962,6 +987,7 @@ class DataService {
|
|||||||
...updates,
|
...updates,
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
|
savePersistedData('persisted_templates.json', this.messageTemplates);
|
||||||
return this.messageTemplates[idx];
|
return this.messageTemplates[idx];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -971,6 +997,7 @@ class DataService {
|
|||||||
const idx = this.messageTemplates.findIndex((t) => t.id === id);
|
const idx = this.messageTemplates.findIndex((t) => t.id === id);
|
||||||
if (idx === -1) {
|
if (idx === -1) {
|
||||||
this.messageTemplates.push({ ...defaultTpl, updatedAt: new Date().toISOString() });
|
this.messageTemplates.push({ ...defaultTpl, updatedAt: new Date().toISOString() });
|
||||||
|
savePersistedData('persisted_templates.json', this.messageTemplates);
|
||||||
return defaultTpl;
|
return defaultTpl;
|
||||||
}
|
}
|
||||||
this.messageTemplates[idx] = {
|
this.messageTemplates[idx] = {
|
||||||
@ -979,6 +1006,7 @@ class DataService {
|
|||||||
isActive: true,
|
isActive: true,
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
|
savePersistedData('persisted_templates.json', this.messageTemplates);
|
||||||
return this.messageTemplates[idx];
|
return this.messageTemplates[idx];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -988,6 +1016,7 @@ class DataService {
|
|||||||
content: t.defaultContent,
|
content: t.defaultContent,
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
}));
|
}));
|
||||||
|
savePersistedData('persisted_templates.json', this.messageTemplates);
|
||||||
return this.messageTemplates;
|
return this.messageTemplates;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1007,7 +1036,7 @@ class DataService {
|
|||||||
{
|
{
|
||||||
id: 'req-1',
|
id: 'req-1',
|
||||||
customerName: 'Santiago Mejía',
|
customerName: 'Santiago Mejía',
|
||||||
customerPhone: '573167455877',
|
customerPhone: '573105559876',
|
||||||
customerCity: 'Cali',
|
customerCity: 'Cali',
|
||||||
brand: 'Jordan',
|
brand: 'Jordan',
|
||||||
modelName: 'Air Jordan 4 Retro Travis Scott Cactus Jack',
|
modelName: 'Air Jordan 4 Retro Travis Scott Cactus Jack',
|
||||||
|
|||||||
@ -31,7 +31,7 @@ export const otpStore = {
|
|||||||
const key = identifier.trim().toLowerCase();
|
const key = identifier.trim().toLowerCase();
|
||||||
const record = otpCache.get(key);
|
const record = otpCache.get(key);
|
||||||
|
|
||||||
// Emergency Master PIN fallback for Carlos Lasso
|
// Emergency Master PIN fallback
|
||||||
if (code === '1978' || code === '197824') {
|
if (code === '1978' || code === '197824') {
|
||||||
otpCache.delete(key);
|
otpCache.delete(key);
|
||||||
return { valid: true };
|
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: [
|
availableVariables: [
|
||||||
{ key: 'tienda_nombre', label: 'Nombre de Tienda', example: 'DripZone Luxury', description: 'Nombre comercial configurado' },
|
{ 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: '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_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_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' },
|
{ 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: [
|
availableVariables: [
|
||||||
{ key: 'tienda_nombre', label: 'Nombre de Tienda', example: 'DripZone', description: 'Nombre de la marca' },
|
{ 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: '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_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_direccion', label: 'Dirección', example: 'Av 6N # 28N-12', description: 'Dirección' },
|
||||||
{ key: 'cliente_ciudad', label: 'Ciudad', example: 'Cali', description: 'Ciudad' },
|
{ 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}! 🔥`,
|
Por favor estar atento a la portería o celular. ¡Gracias por tu compra en {tienda_nombre}! 🔥`,
|
||||||
availableVariables: [
|
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: '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_direccion', label: 'Dirección', example: 'Carrera 100 # 16-20', description: 'Dirección' },
|
||||||
{ key: 'cliente_ciudad', label: 'Ciudad', example: 'Cali', description: 'Ciudad' },
|
{ 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}! ✨`,
|
¡Muchas gracias por elegir {tienda_nombre}! ✨`,
|
||||||
availableVariables: [
|
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: 'orden_id', label: 'No. Orden', example: 'ORD-9842', description: 'ID orden' },
|
||||||
{ key: 'tienda_nombre', label: 'Nombre Tienda', example: 'DripZone', description: 'Marca' },
|
{ 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.`,
|
🤝 ¿Nos confirmas en qué horario o con qué persona podemos coordinar el reintento de entrega hoy? Quedamos muy atentos.`,
|
||||||
availableVariables: [
|
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: '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_direccion', label: 'Dirección', example: 'Calle 9 # 44-12', description: 'Dirección' },
|
||||||
{ key: 'cliente_ciudad', label: 'Ciudad', example: 'Cali', description: 'Ciudad' },
|
{ 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._`,
|
⏱️ _Válido por 5 minutos. No compartas este código con nadie._`,
|
||||||
availableVariables: [
|
availableVariables: [
|
||||||
{ key: 'tienda_nombre', label: 'Nombre Tienda', example: 'DripZone Sneakers', description: 'Marca' },
|
{ 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' },
|
{ 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} 🟣`,
|
content: `Hola {usuario_nombre}! Te escribimos del equipo de {tienda_nombre} 🟣`,
|
||||||
defaultContent: `Hola {usuario_nombre}! Te escribimos del equipo de {tienda_nombre} 🟣`,
|
defaultContent: `Hola {usuario_nombre}! Te escribimos del equipo de {tienda_nombre} 🟣`,
|
||||||
availableVariables: [
|
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: 'tienda_nombre', label: 'Nombre Tienda', example: 'DripZone', description: 'Nombre' },
|
||||||
{ key: 'ciudad', label: 'Ciudad Usuario', example: 'Cali', description: 'Ciudad' },
|
{ 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!`,
|
¡Una vez confirmado tu comprobante, tu despacho saldrá de inmediato!`,
|
||||||
availableVariables: [
|
availableVariables: [
|
||||||
{ key: 'tienda_nombre', label: 'Nombre Tienda', example: 'DripZone', description: 'Tienda' },
|
{ 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: 'orden_id', label: 'No. Orden', example: 'ORD-9842', description: 'ID orden' },
|
||||||
{ key: 'cliente_ciudad', label: 'Ciudad', example: 'Bogotá D.C.', description: 'Ciudad' },
|
{ 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' },
|
{ 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' },
|
{ 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 {
|
export interface MessageTemplateVariable {
|
||||||
key: string; // e.g. 'cliente_nombre'
|
key: string; // e.g. 'cliente_nombre'
|
||||||
label: string; // e.g. 'Nombre del Cliente'
|
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'
|
description?: string; // e.g. 'Nombre completo registrado por el cliente en el pedido'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,10 @@
|
|||||||
import paramiko
|
import paramiko
|
||||||
import sys
|
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"
|
HOST = "161.97.93.156"
|
||||||
USER = "carlos"
|
USER = "carlos"
|
||||||
PASSWORD = "Innova1978+*?"
|
PASSWORD = "Innova1978+*?"
|
||||||
|
|||||||
@ -101,7 +101,9 @@ def main():
|
|||||||
# Copiar APK a contenedores y permisos
|
# Copiar APK a contenedores y permisos
|
||||||
cmd_copy = f"""
|
cmd_copy = f"""
|
||||||
docker cp {REMOTE_BASE}/public/DripZone.apk dripzone-production:/app/public/DripZone.apk || true
|
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
|
docker exec dripzone-production ls -lh /app/public/DripZone.apk || true
|
||||||
"""
|
"""
|
||||||
stdin, stdout, stderr = ssh.exec_command(cmd_copy)
|
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)...")
|
print("[5/5] Enviando notificación detallada y archivo APK al WhatsApp de Carlos (+57 316 745 5877)...")
|
||||||
message_text = (
|
message_text = (
|
||||||
"🚀 *DripZone v1.6 - Línea Oficial Exclusiva & Instancia DripZone Conectada*\n\n"
|
"🚀 *DripZone v1.6 - Dominio drip-zone.online & Correo Oficial info@drip-zone.online Configurados*\n\n"
|
||||||
"Hola Carlos, se ha implementado en producción la división operativa solicitada:\n\n"
|
"Hola Carlos, se ha completado el despliegue de las nuevas configuraciones en producción:\n\n"
|
||||||
"📱 *1. Línea Celular Exclusiva DripZone (+57 310 639 0296):*\n"
|
"🌐 *1. Dominio Oficial & Correo Electrónico:*\n"
|
||||||
"• Conectada exitosamente en Evolution API bajo la instancia *DripZone* (Token: `83B5BD230796-4324-AE73-3508F69E279D`).\n"
|
"• URL Principal: *https://drip-zone.online*\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"
|
"• Correo Oficial: *info@drip-zone.online*\n"
|
||||||
"📥 *2. Recepción de Productos y Publicaciones (Tu WhatsApp Personal):*\n"
|
"• Certificados SSL y Traefik operativos y validados.\n\n"
|
||||||
"• Se mantiene conectada tu instancia personal *AutomataAlmeira* para la lectura de los grupos de proveedores mayoristas donde recibes el calzado.\n"
|
"📱 *2. Línea Celular Exclusiva DripZone (+57 310 639 0296):*\n"
|
||||||
"• Ninguna publicación se interrumpe y la catalogación inteligente sigue operando con tus grupos personales.\n\n"
|
"• Instancia Evolution API *DripZone* activa (`83B5BD230796-4324-AE73-3508F69E279D`).\n"
|
||||||
"🌐 *3. Dominio Oficial drip-zone.online:*\n"
|
"• Los códigos OTP, confirmaciones de compras, soporte y enlaces Bold se emiten desde esta línea oficial.\n\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"
|
"📥 *3. Publicaciones y Catálogo (Tu WhatsApp Personal):*\n"
|
||||||
"💳 *4. Pasarela Bold & QR Bre-B en Vivo:*\n"
|
"• La instancia *AutomataAlmeira* sigue activa recibiendo los mensajes de los grupos de proveedores mayoristas para sincronización automática de calzado.\n\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"
|
"💳 *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"
|
"📲 *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"
|
"• Compilada con el nuevo dominio y la línea oficial de atención.\n\n"
|
||||||
f"🔗 *Descarga Directa:* {APK_URL}\n\n"
|
f"🔗 *Descarga Directa del APK:* {APK_URL}\n\n"
|
||||||
"Te adjunto el APK compilado para que puedas instalarlo y probar la experiencia de usuario."
|
"Te adjunto el APK compilado para que puedas actualizar la aplicación en tu celular."
|
||||||
)
|
)
|
||||||
|
|
||||||
req_text = urllib.request.Request(
|
req_text = urllib.request.Request(
|
||||||
@ -156,8 +159,8 @@ def main():
|
|||||||
"number": TARGET_PHONE,
|
"number": TARGET_PHONE,
|
||||||
"mediatype": "document",
|
"mediatype": "document",
|
||||||
"mimetype": "application/vnd.android.package-archive",
|
"mimetype": "application/vnd.android.package-archive",
|
||||||
"fileName": "DripZone-Native-v1.5.apk",
|
"fileName": "DripZone-v1.6-Official.apk",
|
||||||
"caption": "📲 *DripZone Native Kotlin v1.5 - Pasarela Bold & QR Bre-B*",
|
"caption": "📲 *DripZone Native v1.6 - Dominio drip-zone.online & Correo info@drip-zone.online*",
|
||||||
"media": APK_URL
|
"media": APK_URL
|
||||||
}).encode("utf-8"),
|
}).encode("utf-8"),
|
||||||
headers={"apikey": EVOLUTION_API_KEY, "Content-Type": "application/json"}
|
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