Feat: Agregar botón para iniciar conversación con número nuevo
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m11s
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m11s
- Nuevo endpoint POST /api/messages/:instanceId/new-chat - Valida que el número esté registrado en WhatsApp - Crea el chat en la DB si no existe - Modal en UI para ingresar número de teléfono - Botón verde "+" junto a la barra de búsqueda
This commit is contained in:
@@ -70,6 +70,14 @@
|
|||||||
icon="i-lucide-search"
|
icon="i-lucide-search"
|
||||||
class="flex-1"
|
class="flex-1"
|
||||||
/>
|
/>
|
||||||
|
<button
|
||||||
|
@click="showNewChatModal = true"
|
||||||
|
class="text-xs px-2 py-2 rounded bg-[var(--wa-green)] hover:bg-[var(--wa-green-dark)] text-white"
|
||||||
|
title="Nueva conversacion"
|
||||||
|
:disabled="!selectedInstance"
|
||||||
|
>
|
||||||
|
<UIcon name="i-lucide-plus" class="w-4 h-4" />
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="showChatsDebug = !showChatsDebug"
|
@click="showChatsDebug = !showChatsDebug"
|
||||||
class="text-xs px-2 py-2 rounded bg-gray-700 hover:bg-gray-600 text-gray-300"
|
class="text-xs px-2 py-2 rounded bg-gray-700 hover:bg-gray-600 text-gray-300"
|
||||||
@@ -242,6 +250,57 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- New Chat Modal -->
|
||||||
|
<UModal v-model:open="showNewChatModal">
|
||||||
|
<template #content>
|
||||||
|
<div class="p-6 bg-[var(--wa-bg-dark)]">
|
||||||
|
<h3 class="text-lg font-semibold text-[var(--wa-text)] mb-4">
|
||||||
|
Nueva conversación
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm text-[var(--wa-text-muted)] mb-2">
|
||||||
|
Número de teléfono
|
||||||
|
</label>
|
||||||
|
<UInput
|
||||||
|
v-model="newChatPhoneNumber"
|
||||||
|
placeholder="Ej: 50588887777"
|
||||||
|
icon="i-lucide-phone"
|
||||||
|
class="w-full"
|
||||||
|
@keyup.enter="startNewChat"
|
||||||
|
/>
|
||||||
|
<p class="text-xs text-[var(--wa-text-muted)] mt-1">
|
||||||
|
Ingresa el número con código de país, sin espacios ni guiones
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="newChatError" class="text-sm text-red-400">
|
||||||
|
{{ newChatError }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end gap-2 pt-2">
|
||||||
|
<UButton
|
||||||
|
color="neutral"
|
||||||
|
variant="ghost"
|
||||||
|
@click="closeNewChatModal"
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</UButton>
|
||||||
|
<UButton
|
||||||
|
color="primary"
|
||||||
|
:loading="newChatLoading"
|
||||||
|
:disabled="!newChatPhoneNumber.trim()"
|
||||||
|
@click="startNewChat"
|
||||||
|
>
|
||||||
|
Iniciar conversación
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UModal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -279,6 +338,12 @@ const showDebugPanel = ref(false)
|
|||||||
const showChatsDebug = ref(false)
|
const showChatsDebug = ref(false)
|
||||||
const showSelectedChatDebug = ref(false)
|
const showSelectedChatDebug = ref(false)
|
||||||
|
|
||||||
|
// New chat modal state
|
||||||
|
const showNewChatModal = ref(false)
|
||||||
|
const newChatPhoneNumber = ref('')
|
||||||
|
const newChatLoading = ref(false)
|
||||||
|
const newChatError = ref('')
|
||||||
|
|
||||||
// Instance options for selector
|
// Instance options for selector
|
||||||
const instanceOptions = computed(() =>
|
const instanceOptions = computed(() =>
|
||||||
instances.value
|
instances.value
|
||||||
@@ -658,6 +723,54 @@ const handleReact = async (message: any, emoji: string) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// New chat modal functions
|
||||||
|
const closeNewChatModal = () => {
|
||||||
|
showNewChatModal.value = false
|
||||||
|
newChatPhoneNumber.value = ''
|
||||||
|
newChatError.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const startNewChat = async () => {
|
||||||
|
if (!selectedInstance.value?.value || !newChatPhoneNumber.value.trim()) return
|
||||||
|
|
||||||
|
newChatLoading.value = true
|
||||||
|
newChatError.value = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await $fetch(`/api/messages/${selectedInstance.value.value}/new-chat`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
phoneNumber: newChatPhoneNumber.value.trim()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.success && result.chat) {
|
||||||
|
// If it's a new chat, add it to the list
|
||||||
|
if (result.isNew) {
|
||||||
|
chats.value.unshift(result.chat)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select the chat
|
||||||
|
selectedChat.value = result.chat
|
||||||
|
|
||||||
|
// Close the modal
|
||||||
|
closeNewChatModal()
|
||||||
|
|
||||||
|
toast.add({
|
||||||
|
title: result.isNew ? 'Conversación creada' : 'Conversación encontrada',
|
||||||
|
icon: 'i-lucide-check',
|
||||||
|
color: 'success',
|
||||||
|
timeout: 2000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error('Error creating new chat:', e)
|
||||||
|
newChatError.value = e?.data?.message || e?.message || 'Error al crear la conversación'
|
||||||
|
} finally {
|
||||||
|
newChatLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle typing event from input
|
// Handle typing event from input
|
||||||
const handleTyping = () => {
|
const handleTyping = () => {
|
||||||
if (!selectedChat.value?.jid || selectedChat.value?.isGroup) return
|
if (!selectedChat.value?.jid || selectedChat.value?.isGroup) return
|
||||||
|
|||||||
138
server/api/messages/[instanceId]/new-chat.post.ts
Normal file
138
server/api/messages/[instanceId]/new-chat.post.ts
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
/**
|
||||||
|
* POST /api/messages/:instanceId/new-chat
|
||||||
|
* Create or get a chat by phone number to start a new conversation
|
||||||
|
*/
|
||||||
|
import { query } from '../../../utils/database'
|
||||||
|
import { baileysManager } from '../../../services/baileys/manager'
|
||||||
|
import { randomUUID } from 'crypto'
|
||||||
|
|
||||||
|
interface NewChatBody {
|
||||||
|
phoneNumber: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const username = getHeader(event, 'x-authentik-username')
|
||||||
|
if (!username) {
|
||||||
|
throw createError({ statusCode: 401, message: 'Unauthorized' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const instanceId = getRouterParam(event, 'instanceId')
|
||||||
|
if (!instanceId) {
|
||||||
|
throw createError({ statusCode: 400, message: 'Missing instanceId' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify instance exists and is connected
|
||||||
|
const socket = baileysManager.getSocket(instanceId)
|
||||||
|
if (!socket) {
|
||||||
|
throw createError({ statusCode: 400, message: 'Instance not connected' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await readBody<NewChatBody>(event)
|
||||||
|
|
||||||
|
if (!body.phoneNumber?.trim()) {
|
||||||
|
throw createError({ statusCode: 400, message: 'Phone number is required' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean phone number: remove spaces, dashes, parentheses, and + prefix
|
||||||
|
const cleanNumber = body.phoneNumber.replace(/[\s\-\(\)\+]/g, '')
|
||||||
|
|
||||||
|
// Validate it's a numeric string
|
||||||
|
if (!/^\d+$/.test(cleanNumber)) {
|
||||||
|
throw createError({ statusCode: 400, message: 'Invalid phone number format' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build WhatsApp JID (individual chat)
|
||||||
|
const jid = `${cleanNumber}@s.whatsapp.net`
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if chat already exists
|
||||||
|
const existingChat = await query(
|
||||||
|
`SELECT id, jid, name, is_group, unread_count, last_message_at, last_message_type,
|
||||||
|
(SELECT COALESCE(content, caption) FROM messages m
|
||||||
|
WHERE m.chat_id = chats.id ORDER BY timestamp DESC LIMIT 1) as last_message
|
||||||
|
FROM chats
|
||||||
|
WHERE instance_id = $1 AND jid = $2`,
|
||||||
|
[instanceId, jid]
|
||||||
|
)
|
||||||
|
|
||||||
|
if (existingChat.rows.length > 0) {
|
||||||
|
// Chat exists, return it
|
||||||
|
const chat = existingChat.rows[0]
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
chat: {
|
||||||
|
id: chat.id,
|
||||||
|
jid: chat.jid,
|
||||||
|
name: chat.name || cleanNumber,
|
||||||
|
isGroup: chat.is_group,
|
||||||
|
unreadCount: chat.unread_count || 0,
|
||||||
|
lastMessage: chat.last_message,
|
||||||
|
lastMessageAt: chat.last_message_at,
|
||||||
|
lastMessageType: chat.last_message_type
|
||||||
|
},
|
||||||
|
isNew: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chat doesn't exist, create it
|
||||||
|
const chatId = randomUUID()
|
||||||
|
|
||||||
|
// Try to get contact name from WhatsApp (optional, may fail)
|
||||||
|
let contactName = cleanNumber
|
||||||
|
try {
|
||||||
|
// Check if number exists on WhatsApp using onWhatsApp
|
||||||
|
const [exists] = await socket.onWhatsApp(jid)
|
||||||
|
if (!exists?.exists) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
message: 'Este número no está registrado en WhatsApp'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to get the push name or contact name
|
||||||
|
if (exists.notify) {
|
||||||
|
contactName = exists.notify
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
// If it's our validation error, rethrow
|
||||||
|
if (e.statusCode === 400) {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
// Otherwise just use the number as name
|
||||||
|
console.log('[NewChat] Could not verify WhatsApp status:', e.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert new chat
|
||||||
|
await query(
|
||||||
|
`INSERT INTO chats (id, instance_id, jid, name, is_group, unread_count, created_at, updated_at)
|
||||||
|
VALUES ($1, $2, $3, $4, false, 0, NOW(), NOW())`,
|
||||||
|
[chatId, instanceId, jid, contactName]
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
chat: {
|
||||||
|
id: chatId,
|
||||||
|
jid: jid,
|
||||||
|
name: contactName,
|
||||||
|
isGroup: false,
|
||||||
|
unreadCount: 0,
|
||||||
|
lastMessage: null,
|
||||||
|
lastMessageAt: null,
|
||||||
|
lastMessageType: null
|
||||||
|
},
|
||||||
|
isNew: true
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
// Rethrow HTTP errors
|
||||||
|
if (e.statusCode) {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error('[NewChat] Error creating chat:', e)
|
||||||
|
throw createError({
|
||||||
|
statusCode: 500,
|
||||||
|
message: `Error creating chat: ${e.message}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user