diff --git a/.gitignore b/.gitignore
index a7b627c..efa98fe 100644
--- a/.gitignore
+++ b/.gitignore
@@ -105,12 +105,13 @@ docker-compose.override.yml
# -----------------------------------------------------------------------------
# Sessions / Data
# -----------------------------------------------------------------------------
-sessions/
+/sessions/
*.session
*.session.json
-# WhatsApp Baileys sessions
+# WhatsApp Baileys sessions (data, not source code)
services/whatsapp-core/sessions/
+!services/whatsapp-core/src/sessions/
auth_info*/
# -----------------------------------------------------------------------------
diff --git a/docker-compose.yml b/docker-compose.yml
index e4bcd10..9f3633d 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -51,6 +51,7 @@ services:
WS_PORT: 3001
volumes:
- whatsapp_sessions:/app/sessions
+ - whatsapp_media:/app/media
ports:
- "3001:3001"
depends_on:
@@ -70,6 +71,9 @@ services:
REDIS_URL: redis://redis:6379
JWT_SECRET: ${JWT_SECRET:?JWT_SECRET required}
WHATSAPP_CORE_URL: http://whatsapp-core:3001
+ WHATSAPP_CORE_PUBLIC_URL: ${WHATSAPP_CORE_PUBLIC_URL:-http://localhost:3001}
+ FLOW_ENGINE_URL: http://flow-engine:8001
+ ODOO_WEBHOOK_URL: ${ODOO_WEBHOOK_URL:-http://192.168.10.188:8069/whatsapp/webhook}
CORS_ORIGINS: ${CORS_ORIGINS:-http://localhost:5173,http://localhost:3000}
ports:
- "8000:8000"
@@ -139,6 +143,7 @@ volumes:
postgres_data:
redis_data:
whatsapp_sessions:
+ whatsapp_media:
networks:
wac_network:
diff --git a/docs/CONTEXTO_DESARROLLO.md b/docs/CONTEXTO_DESARROLLO.md
new file mode 100644
index 0000000..e0489a0
--- /dev/null
+++ b/docs/CONTEXTO_DESARROLLO.md
@@ -0,0 +1,344 @@
+# WhatsApp Centralizado - Contexto de Desarrollo
+
+> **Fecha de última actualización:** 2026-01-30
+> **Estado:** En desarrollo activo
+
+---
+
+## Resumen del Proyecto
+
+Sistema centralizado de WhatsApp para gestión de múltiples números, integrado con Odoo y un frontend React. Permite:
+- Conectar múltiples números de WhatsApp vía QR
+- Recibir y enviar mensajes (texto, imágenes, audio, video, documentos)
+- Gestionar conversaciones desde frontend web o Odoo
+- Automatizar respuestas con flujos de bot
+- Pausar/reanudar conexiones sin perder sesión
+
+---
+
+## Arquitectura del Sistema
+
+```
+┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
+│ Frontend │────▶│ API Gateway │────▶│ WhatsApp Core │
+│ React + Ant │ │ FastAPI │ │ Node + Baileys │
+│ Puerto: 3000 │ │ Puerto: 8000 │ │ Puerto: 3001 │
+└─────────────────┘ └─────────────────┘ └─────────────────┘
+ │ │
+ ▼ │
+ ┌─────────────────┐ │
+ │ PostgreSQL │◀────────────┘
+ │ Puerto: 5432 │
+ └─────────────────┘
+ │
+ ▼
+ ┌─────────────────┐
+ │ Odoo (CAS) │◀── Webhooks
+ │ 192.168.10.188 │
+ │ Puerto: 8069 │
+ └─────────────────┘
+```
+
+---
+
+## Servicios Docker
+
+| Servicio | Puerto | Descripción |
+|----------|--------|-------------|
+| `frontend` | 3000 | React + Ant Design |
+| `api-gateway` | 8000 | FastAPI - API principal |
+| `whatsapp-core` | 3001 | Node.js + Baileys - Conexión WhatsApp |
+| `flow-engine` | 8001 | Motor de flujos de bot |
+| `integrations` | 8002 | Integraciones externas |
+| `postgres` | 5432 | Base de datos PostgreSQL |
+| `redis` | 6379 | Cache y colas |
+
+### Comandos Docker útiles
+
+```bash
+# Reconstruir servicios
+docker compose build whatsapp-core frontend api-gateway
+
+# Reiniciar servicios
+docker compose up -d
+
+# Ver logs
+docker compose logs -f whatsapp-core
+docker compose logs -f api-gateway
+
+# Entrar a un contenedor
+docker exec -it wac_whatsapp sh
+docker exec -it wac_api bash
+```
+
+---
+
+## Servidor Odoo
+
+- **IP:** 192.168.10.188
+- **Puerto:** 8069
+- **Base de datos:** `cas` (NO usar "odoo")
+- **Usuario SSH:** root / Aasi940812
+- **Ruta módulo:** `/opt/odoo/addons/odoo_whatsapp_hub/`
+
+### Comandos Odoo útiles
+
+```bash
+# Conectar por SSH
+sshpass -p 'Aasi940812' ssh root@192.168.10.188
+
+# Actualizar módulo
+systemctl stop odoo
+/usr/bin/odoo -c /etc/odoo/odoo.conf -d cas -u odoo_whatsapp_hub --stop-after-init
+systemctl start odoo
+
+# Instalar módulo
+/usr/bin/odoo -c /etc/odoo/odoo.conf -d cas -i odoo_whatsapp_hub --stop-after-init
+
+# Ver logs
+tail -f /var/log/odoo/odoo-server.log
+
+# Consultas a PostgreSQL
+sudo -u postgres psql -d cas -c "SELECT * FROM whatsapp_conversation;"
+```
+
+### Configuración WhatsApp Account en Odoo
+
+```sql
+-- Ver cuentas
+SELECT id, name, external_id, api_url FROM whatsapp_account;
+
+-- Configurar external_id (debe coincidir con el UUID del frontend)
+UPDATE whatsapp_account
+SET external_id = '33ce868e-1aa0-4795-9d44-a389e8ade0de',
+ api_url = 'http://192.168.10.221:8000'
+WHERE id = 1;
+```
+
+---
+
+## Módulo Odoo (odoo_whatsapp_hub)
+
+### Estructura
+
+```
+odoo_whatsapp_hub/
+├── __manifest__.py
+├── __init__.py
+├── models/
+│ ├── whatsapp_account.py # Cuentas WhatsApp
+│ ├── whatsapp_conversation.py # Conversaciones
+│ └── whatsapp_message.py # Mensajes
+├── controllers/
+│ └── webhook.py # Recibe eventos del sistema
+├── views/
+│ ├── whatsapp_account_views.xml
+│ ├── whatsapp_conversation_views.xml
+│ ├── dollars_action.xml # Interfaz DOLLARS
+│ └── whatsapp_menu.xml
+├── wizards/
+│ └── send_whatsapp_wizard.xml
+├── static/src/
+│ ├── css/
+│ │ ├── whatsapp.css # Estilos WhatsApp/DRRR
+│ │ └── dollars_theme.css # Tema DOLLARS oscuro
+│ ├── js/
+│ │ ├── chat_action.js # Chat con temas
+│ │ └── dollars_chat.js # Chat Hub DOLLARS
+│ └── xml/
+│ ├── chat_template.xml
+│ └── dollars_template.xml
+└── security/
+ └── ir.model.access.csv
+```
+
+### Menú en Odoo
+
+- **WhatsApp Hub > Chat Hub** - Interfaz DOLLARS (oscura, 3 columnas)
+- **WhatsApp Hub > Conversaciones (Lista)** - Vista clásica de Odoo
+- **WhatsApp Hub > Cuentas** - Gestión de números
+- **WhatsApp Hub > Configuración** - Ajustes
+
+### Webhook
+
+```
+POST http://192.168.10.188:8069/whatsapp/webhook
+Header: X-Odoo-Database: cas
+Header: Content-Type: application/json
+
+Body:
+{
+ "type": "message", // message, status_update, conversation_update, account_status
+ "account_id": "uuid-de-la-cuenta",
+ "data": { ... }
+}
+```
+
+---
+
+## Frontend React
+
+### Estructura principal
+
+```
+frontend/src/
+├── pages/
+│ ├── WhatsAppAccounts.tsx # Gestión de números (pausar/reanudar)
+│ ├── Inbox.tsx # Bandeja de entrada
+│ ├── Dashboard.tsx
+│ └── ...
+├── api/
+│ └── client.ts # Cliente API
+└── App.tsx
+```
+
+### Funcionalidades implementadas
+
+1. **Gestión de cuentas WhatsApp**
+ - Crear cuenta y escanear QR
+ - Pausar conexión (mantiene sesión)
+ - Reanudar conexión (sin QR)
+ - Eliminar cuenta
+
+2. **Bandeja de entrada (Inbox)**
+ - Lista de conversaciones
+ - Visualización de mensajes
+ - Envío de mensajes
+ - Soporte para imágenes, audio, video, documentos
+
+### Proxy Nginx para media
+
+```nginx
+location /media/ {
+ proxy_pass http://whatsapp-core:3001/media/;
+ proxy_set_header Host $host;
+}
+```
+
+---
+
+## API Endpoints Principales
+
+### WhatsApp Accounts
+
+```
+GET /api/whatsapp/accounts # Listar cuentas
+POST /api/whatsapp/accounts # Crear cuenta
+GET /api/whatsapp/accounts/:id # Obtener cuenta
+DELETE /api/whatsapp/accounts/:id # Eliminar cuenta
+POST /api/whatsapp/accounts/:id/pause # Pausar conexión
+POST /api/whatsapp/accounts/:id/resume # Reanudar conexión
+```
+
+### Conversaciones y Mensajes
+
+```
+GET /api/whatsapp/conversations # Listar conversaciones
+GET /api/whatsapp/conversations/:id # Detalle con mensajes
+POST /api/whatsapp/conversations/:id/send # Enviar mensaje
+```
+
+### WhatsApp Core (interno)
+
+```
+POST /api/sessions # Crear sesión
+GET /api/sessions/:id # Info de sesión
+POST /api/sessions/:id/disconnect # Cerrar sesión (logout)
+POST /api/sessions/:id/pause # Pausar (sin logout)
+POST /api/sessions/:id/resume # Reanudar
+POST /api/sessions/:id/messages # Enviar mensaje
+```
+
+---
+
+## Temas de Chat en Odoo
+
+### Tema WhatsApp (clásico)
+- Fondo beige con patrón
+- Burbujas verdes (salientes) y blancas (entrantes)
+- Estilo familiar de WhatsApp
+
+### Tema DRRR (Durarara/Dollars)
+- Fondo negro (#0a0a0a)
+- Texto verde neón (#00ff88) para salientes
+- Texto cyan (#00ccff) para entrantes
+- Nombres en corchetes [Usuario]
+- Fuente monospace
+
+### Tema DOLLARS (Chat Hub)
+- Interfaz oscura 3 columnas
+- Sidebar con lista de conversaciones
+- Chat central
+- Panel de detalles a la derecha
+- Acentos ámbar/naranja (#f59e0b)
+
+---
+
+## Problemas Conocidos y Soluciones
+
+### 1. Imágenes no se ven en frontend
+**Causa:** URLs absolutas con hostname Docker interno
+**Solución:** Usar URLs relativas (`/media/uuid.jpg`) + proxy nginx
+
+### 2. Odoo no muestra cambios
+**Causa:** Base de datos incorrecta (odoo vs cas)
+**Solución:** Siempre usar `-d cas` y header `X-Odoo-Database: cas`
+
+### 3. CSS rompe todo Odoo
+**Causa:** Estilos globales no encapsulados
+**Solución:** Prefijar todo con `.o_dollars_chat` o `.o_whatsapp_chat_fullscreen`
+
+### 4. Webhook retorna 404
+**Causa:** Falta header de base de datos
+**Solución:** Agregar `X-Odoo-Database: cas` a todas las peticiones
+
+### 5. Account not found en webhook
+**Causa:** `external_id` no configurado en Odoo
+**Solución:** Actualizar tabla whatsapp_account con el UUID correcto
+
+---
+
+## Próximos Pasos Sugeridos
+
+1. **Testing de pausar/reanudar** - Verificar que funciona correctamente
+2. **Notificaciones en tiempo real** - WebSocket para nuevos mensajes
+3. **Mejoras al Chat Hub DOLLARS** - Indicador de typing, scroll automático
+4. **Integración completa Odoo** - Sincronizar contactos con res.partner
+5. **Panel de métricas** - Dashboard con estadísticas de uso
+
+---
+
+## Credenciales y Accesos
+
+| Servicio | URL | Usuario | Contraseña |
+|----------|-----|---------|------------|
+| Frontend | http://192.168.10.221:3000 | admin | (ver BD) |
+| API Gateway | http://192.168.10.221:8000 | - | - |
+| Odoo | http://192.168.10.188:8069 | ialcarazsalazar@consultoria-as.com | (conocida) |
+| SSH Odoo | 192.168.10.188:22 | root | Aasi940812 |
+| Gitea | https://git.consultoria-as.com | consultoria-as | (token en remote) |
+
+---
+
+## Comandos de Desarrollo
+
+```bash
+# Clonar repositorio
+git clone https://git.consultoria-as.com/consultoria-as/WhatsAppCentralizado.git
+
+# Levantar todo
+cd WhatsAppCentralizado
+docker compose up -d
+
+# Desplegar módulo Odoo
+sshpass -p 'Aasi940812' scp -r odoo_whatsapp_hub root@192.168.10.188:/opt/odoo/addons/
+sshpass -p 'Aasi940812' ssh root@192.168.10.188 "systemctl stop odoo && /usr/bin/odoo -c /etc/odoo/odoo.conf -d cas -u odoo_whatsapp_hub --stop-after-init && systemctl start odoo"
+
+# Ver estado
+docker compose ps
+docker compose logs -f
+```
+
+---
+
+*Documento generado automáticamente. Última sesión de trabajo: 2026-01-30*
diff --git a/frontend/nginx.conf b/frontend/nginx.conf
index 53ca5fb..7c1293a 100644
--- a/frontend/nginx.conf
+++ b/frontend/nginx.conf
@@ -8,21 +8,23 @@ server {
try_files $uri $uri/ /index.html;
}
- location /api {
+ location /api/ {
+ rewrite ^/api/(.*) /$1 break;
proxy_pass http://api-gateway:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
- location /auth {
- proxy_pass http://api-gateway:8000;
- proxy_set_header Host $host;
- }
-
location /ws {
proxy_pass http://whatsapp-core:3001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
+
+ location /media/ {
+ proxy_pass http://whatsapp-core:3001/media/;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ }
}
diff --git a/frontend/src/pages/Inbox.tsx b/frontend/src/pages/Inbox.tsx
index 47d4d89..80b316c 100644
--- a/frontend/src/pages/Inbox.tsx
+++ b/frontend/src/pages/Inbox.tsx
@@ -49,6 +49,7 @@ interface Message {
direction: 'inbound' | 'outbound';
type: string;
content: string | null;
+ media_url: string | null;
created_at: string;
is_internal_note: boolean;
sent_by: string | null;
@@ -368,6 +369,63 @@ export default function Inbox(): JSX.Element {
);
}
+ function renderMessageContent(msg: Message): JSX.Element {
+ // Render media based on type
+ if (msg.media_url) {
+ const mediaType = msg.type?.toUpperCase();
+
+ if (mediaType === 'IMAGE') {
+ return (
+ <>
+
window.open(msg.media_url!, '_blank')}
+ />
+ {msg.content && msg.content !== '[Image]' && (
+ {msg.content}
+ )}
+ >
+ );
+ }
+
+ if (mediaType === 'AUDIO') {
+ return (
+
+ );
+ }
+
+ if (mediaType === 'VIDEO') {
+ return (
+
+ );
+ }
+
+ if (mediaType === 'DOCUMENT') {
+ return (
+
+ 📄 {msg.content || 'Documento'}
+
+ );
+ }
+ }
+
+ // Default text content
+ return {msg.content};
+ }
+
function renderMessage(msg: Message): JSX.Element {
return (
Nota interna
)}
- {msg.content}
+ {renderMessageContent(msg)}
{
+ await apiClient.post(`/api/whatsapp/accounts/${id}/pause`);
+ },
+ onSuccess: () => {
+ message.success('Conexión pausada');
+ queryClient.invalidateQueries({ queryKey: ['whatsapp-accounts'] });
+ },
+ onError: () => {
+ message.error('Error al pausar');
+ },
+ });
+
+ const resumeMutation = useMutation({
+ mutationFn: async (id: string) => {
+ await apiClient.post(`/api/whatsapp/accounts/${id}/resume`);
+ },
+ onSuccess: () => {
+ message.success('Reconectando...');
+ queryClient.invalidateQueries({ queryKey: ['whatsapp-accounts'] });
+ },
+ onError: () => {
+ message.error('Error al reanudar');
+ },
+ });
+
const handleShowQR = async (account: WhatsAppAccount) => {
const data = await apiClient.get(`/api/whatsapp/accounts/${account.id}`);
setQrModal(data);
@@ -102,13 +130,15 @@ export default function WhatsAppAccounts() {
connected: 'green',
connecting: 'orange',
disconnected: 'red',
+ paused: 'gold',
};
const labels: Record = {
connected: 'Conectado',
connecting: 'Conectando',
disconnected: 'Desconectado',
+ paused: 'Pausado',
};
- return {labels[status]};
+ return {labels[status] || status};
},
},
{
@@ -116,7 +146,7 @@ export default function WhatsAppAccounts() {
key: 'actions',
render: (_: any, record: WhatsAppAccount) => (
- {record.status !== 'connected' && (
+ {record.status !== 'connected' && record.status !== 'paused' && (
}
onClick={() => handleShowQR(record)}
@@ -124,6 +154,33 @@ export default function WhatsAppAccounts() {
Ver QR
)}
+ {record.status === 'connected' && (
+ }
+ onClick={() => {
+ Modal.confirm({
+ title: '¿Pausar conexión?',
+ content: 'La conexión se pausará pero podrás reanudarla después sin escanear QR.',
+ okText: 'Pausar',
+ onOk: () => pauseMutation.mutate(record.id),
+ });
+ }}
+ style={{ color: '#faad14', borderColor: '#faad14' }}
+ >
+ Pausar
+
+ )}
+ {(record.status === 'paused' || record.status === 'disconnected') && (
+ }
+ onClick={() => resumeMutation.mutate(record.id)}
+ loading={resumeMutation.isPending}
+ style={{ background: '#25D366', borderColor: '#25D366' }}
+ >
+ Reanudar
+
+ )}
}
diff --git a/odoo_whatsapp_hub/__manifest__.py b/odoo_whatsapp_hub/__manifest__.py
index bdd741c..62fefdc 100644
--- a/odoo_whatsapp_hub/__manifest__.py
+++ b/odoo_whatsapp_hub/__manifest__.py
@@ -16,9 +16,11 @@
'license': 'LGPL-3',
'depends': ['base', 'contacts', 'mail'],
'data': [
+ 'security/ir.model.access.csv',
'views/whatsapp_account_views.xml',
'views/whatsapp_conversation_views.xml',
'views/res_partner_views.xml',
+ 'views/dollars_action.xml',
'wizards/send_whatsapp_wizard.xml',
'views/whatsapp_menu.xml',
'data/whatsapp_data.xml',
@@ -27,8 +29,11 @@
'assets': {
'web.assets_backend': [
'odoo_whatsapp_hub/static/src/css/whatsapp.css',
- 'odoo_whatsapp_hub/static/src/js/chat_widget.js',
- 'odoo_whatsapp_hub/static/src/xml/chat_widget.xml',
+ 'odoo_whatsapp_hub/static/src/css/dollars_theme.css',
+ 'odoo_whatsapp_hub/static/src/js/chat_action.js',
+ 'odoo_whatsapp_hub/static/src/js/dollars_chat.js',
+ 'odoo_whatsapp_hub/static/src/xml/chat_template.xml',
+ 'odoo_whatsapp_hub/static/src/xml/dollars_template.xml',
],
},
'installable': True,
diff --git a/odoo_whatsapp_hub/controllers/webhook.py b/odoo_whatsapp_hub/controllers/webhook.py
index 26c6981..0613b35 100644
--- a/odoo_whatsapp_hub/controllers/webhook.py
+++ b/odoo_whatsapp_hub/controllers/webhook.py
@@ -8,14 +8,15 @@ _logger = logging.getLogger(__name__)
class WhatsAppWebhookController(http.Controller):
- @http.route('/whatsapp/webhook', type='json', auth='public', methods=['POST'], csrf=False)
+ @http.route('/whatsapp/webhook', type='http', auth='public', methods=['POST'], csrf=False)
def webhook(self, **kwargs):
"""
Receive webhooks from WhatsApp Central.
Events: message, status_update, conversation_update
"""
try:
- data = request.jsonrequest
+ # Parse JSON from request body
+ data = json.loads(request.httprequest.data.decode('utf-8'))
event_type = data.get('type')
_logger.info(f'WhatsApp webhook received: {event_type}')
@@ -29,13 +30,14 @@ class WhatsAppWebhookController(http.Controller):
handler = handlers.get(event_type)
if handler:
- return handler(data)
+ result = handler(data)
+ return request.make_json_response(result)
- return {'status': 'ignored', 'reason': f'Unknown event type: {event_type}'}
+ return request.make_json_response({'status': 'ignored', 'reason': f'Unknown event type: {event_type}'})
except Exception as e:
_logger.error(f'WhatsApp webhook error: {e}')
- return {'status': 'error', 'message': str(e)}
+ return request.make_json_response({'status': 'error', 'message': str(e)})
def _handle_message(self, data):
"""Handle incoming message"""
@@ -64,23 +66,25 @@ class WhatsAppWebhookController(http.Controller):
'status': 'bot',
})
+ # Try to find partner by phone
partner = request.env['res.partner'].sudo().search([
- '|',
('phone', 'ilike', phone[-10:]),
- ('mobile', 'ilike', phone[-10:]),
], limit=1)
if partner:
conversation.partner_id = partner
+ # Get direction from webhook data (inbound or outbound)
+ direction = msg_data.get('direction', 'inbound')
+
request.env['whatsapp.message'].sudo().create({
'external_id': msg_data.get('id'),
'conversation_id': conversation.id,
- 'direction': 'inbound',
+ 'direction': direction,
'message_type': msg_data.get('type', 'text'),
'content': msg_data.get('content'),
'media_url': msg_data.get('media_url'),
- 'status': 'delivered',
+ 'status': 'delivered' if direction == 'inbound' else 'sent',
})
return {'status': 'ok'}
@@ -137,4 +141,4 @@ class WhatsAppWebhookController(http.Controller):
@http.route('/whatsapp/webhook/test', type='http', auth='public', methods=['GET'])
def webhook_test(self):
"""Test endpoint to verify webhook connectivity"""
- return json.dumps({'status': 'ok', 'message': 'WhatsApp webhook is active'})
+ return request.make_json_response({'status': 'ok', 'message': 'WhatsApp webhook is active'})
diff --git a/odoo_whatsapp_hub/models/whatsapp_account.py b/odoo_whatsapp_hub/models/whatsapp_account.py
index e5d1576..e6b770f 100644
--- a/odoo_whatsapp_hub/models/whatsapp_account.py
+++ b/odoo_whatsapp_hub/models/whatsapp_account.py
@@ -59,9 +59,9 @@ class WhatsAppAccount(models.Model):
raise UserError('Esta cuenta no está vinculada a WhatsApp Central')
try:
+ # Use internal endpoint (no auth required)
response = requests.get(
- f'{self.api_url}/api/whatsapp/accounts/{self.external_id}',
- headers=self._get_headers(),
+ f'{self.api_url}/api/whatsapp/internal/odoo/accounts/{self.external_id}',
timeout=10,
)
if response.status_code == 200:
@@ -71,7 +71,9 @@ class WhatsAppAccount(models.Model):
'phone_number': data.get('phone_number'),
'qr_code': data.get('qr_code'),
})
- except Exception as e:
+ else:
+ raise UserError(f'Error del servidor: {response.status_code}')
+ except requests.exceptions.RequestException as e:
_logger.error(f'Error syncing WhatsApp account: {e}')
raise UserError(f'Error de conexión: {e}')
diff --git a/odoo_whatsapp_hub/models/whatsapp_conversation.py b/odoo_whatsapp_hub/models/whatsapp_conversation.py
index 5315b23..c48c740 100644
--- a/odoo_whatsapp_hub/models/whatsapp_conversation.py
+++ b/odoo_whatsapp_hub/models/whatsapp_conversation.py
@@ -115,6 +115,79 @@ class WhatsAppConversation(models.Model):
'status': 'active',
})
+ def action_open_send_wizard(self):
+ """Open wizard to send WhatsApp message"""
+ self.ensure_one()
+ return {
+ 'type': 'ir.actions.act_window',
+ 'name': 'Enviar WhatsApp',
+ 'res_model': 'whatsapp.send.wizard',
+ 'view_mode': 'form',
+ 'target': 'new',
+ 'context': {
+ 'default_phone': self.phone_number,
+ 'default_account_id': self.account_id.id,
+ 'default_partner_id': self.partner_id.id if self.partner_id else False,
+ },
+ }
+
+ def action_open_chat(self):
+ """Open fullscreen chat interface"""
+ self.ensure_one()
+ return {
+ 'type': 'ir.actions.client',
+ 'tag': 'whatsapp_chat',
+ 'name': self.display_name,
+ 'context': {
+ 'active_id': self.id,
+ },
+ }
+
+ def send_message_from_chat(self, message):
+ """Send a message from the chat interface"""
+ self.ensure_one()
+ import requests
+
+ account = self.account_id
+ if not account or not account.api_url:
+ raise Exception("Cuenta WhatsApp no configurada")
+
+ # Send via API
+ api_url = account.api_url.rstrip('/')
+ response = requests.post(
+ f"{api_url}/whatsapp/send",
+ json={
+ 'account_id': account.external_id,
+ 'phone': self.phone_number,
+ 'message': message,
+ },
+ timeout=30,
+ )
+
+ if response.status_code != 200:
+ raise Exception(f"Error al enviar: {response.text}")
+
+ result = response.json()
+
+ # Create local message record
+ self.env['whatsapp.message'].create({
+ 'conversation_id': self.id,
+ 'direction': 'outbound',
+ 'message_type': 'text',
+ 'content': message,
+ 'status': 'sent',
+ 'sent_by_id': self.env.user.id,
+ 'external_id': result.get('message_id'),
+ })
+
+ # Update conversation
+ self.write({
+ 'last_message_at': fields.Datetime.now(),
+ 'status': 'active' if self.status == 'bot' else self.status,
+ })
+
+ return True
+
@api.model
def find_or_create_by_phone(self, phone, account_id, contact_name=None):
"""Find or create conversation by phone number"""
diff --git a/odoo_whatsapp_hub/models/whatsapp_message.py b/odoo_whatsapp_hub/models/whatsapp_message.py
index b4091b6..0504816 100644
--- a/odoo_whatsapp_hub/models/whatsapp_message.py
+++ b/odoo_whatsapp_hub/models/whatsapp_message.py
@@ -68,15 +68,23 @@ class WhatsAppMessage(models.Model):
"""Send message via WhatsApp Central API"""
self.ensure_one()
account = self.conversation_id.account_id
+ phone_number = self.conversation_id.phone_number
+
+ if not account.external_id:
+ self.write({
+ 'status': 'failed',
+ 'error_message': 'La cuenta no está vinculada a WhatsApp Central',
+ })
+ return
try:
+ # Use internal endpoint (no auth required)
response = requests.post(
- f'{account.api_url}/api/whatsapp/conversations/{self.conversation_id.external_id}/messages',
- headers=account._get_headers(),
+ f'{account.api_url}/api/whatsapp/internal/odoo/send',
json={
- 'type': self.message_type,
- 'content': self.content,
- 'media_url': self.media_url,
+ 'phone_number': phone_number,
+ 'message': self.content,
+ 'account_id': account.external_id,
},
timeout=30,
)
@@ -84,7 +92,7 @@ class WhatsAppMessage(models.Model):
if response.status_code == 200:
data = response.json()
self.write({
- 'external_id': data.get('id'),
+ 'external_id': data.get('message_id'),
'status': 'sent',
'error_message': False,
})
diff --git a/odoo_whatsapp_hub/security/ir.model.access.csv b/odoo_whatsapp_hub/security/ir.model.access.csv
new file mode 100644
index 0000000..bf06496
--- /dev/null
+++ b/odoo_whatsapp_hub/security/ir.model.access.csv
@@ -0,0 +1,8 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_whatsapp_account_user,whatsapp.account.user,model_whatsapp_account,base.group_user,1,0,0,0
+access_whatsapp_account_admin,whatsapp.account.admin,model_whatsapp_account,base.group_system,1,1,1,1
+access_whatsapp_conversation_user,whatsapp.conversation.user,model_whatsapp_conversation,base.group_user,1,1,1,0
+access_whatsapp_conversation_admin,whatsapp.conversation.admin,model_whatsapp_conversation,base.group_system,1,1,1,1
+access_whatsapp_message_user,whatsapp.message.user,model_whatsapp_message,base.group_user,1,1,1,0
+access_whatsapp_message_admin,whatsapp.message.admin,model_whatsapp_message,base.group_system,1,1,1,1
+access_whatsapp_send_wizard_user,whatsapp.send.wizard.user,model_whatsapp_send_wizard,base.group_user,1,1,1,1
diff --git a/odoo_whatsapp_hub/static/src/css/dollars_theme.css b/odoo_whatsapp_hub/static/src/css/dollars_theme.css
new file mode 100644
index 0000000..0d7eb74
--- /dev/null
+++ b/odoo_whatsapp_hub/static/src/css/dollars_theme.css
@@ -0,0 +1,774 @@
+/* DOLLARS WhatsApp Theme - Dark Mode with Amber Accents */
+/* All styles are scoped to .o_dollars_chat to avoid affecting other Odoo views */
+
+/* ============================================
+ MAIN CONTAINER - All styles scoped here
+ ============================================ */
+.o_dollars_chat {
+ --bg-primary: #0a0a0f;
+ --bg-secondary: #12121a;
+ --bg-tertiary: #1a1a24;
+ --bg-hover: #22222e;
+ --bg-active: #2a2a38;
+ --accent-primary: #f59e0b;
+ --accent-secondary: #fbbf24;
+ --accent-glow: rgba(245, 158, 11, 0.3);
+ --text-primary: #ffffff;
+ --text-secondary: #a1a1aa;
+ --text-muted: #71717a;
+ --border-color: #27272a;
+ --border-light: #3f3f46;
+ --success: #22c55e;
+ --danger: #ef4444;
+ --info: #3b82f6;
+ --msg-inbound-bg: #1e1e2a;
+ --msg-outbound-bg: #2d2a1f;
+ --msg-outbound-border: rgba(245, 158, 11, 0.2);
+ --radius-sm: 4px;
+ --radius-md: 8px;
+ --radius-lg: 12px;
+ --radius-full: 9999px;
+ --shadow-glow: 0 0 20px var(--accent-glow);
+
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+ background: var(--bg-primary);
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ color: var(--text-primary);
+ overflow: hidden;
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ z-index: 1000;
+}
+
+/* Header */
+.o_dollars_chat .o_dollars_header {
+ background: var(--bg-secondary);
+ padding: 12px 24px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ border-bottom: 1px solid var(--border-color);
+ flex-shrink: 0;
+ position: relative;
+}
+
+.o_dollars_chat .o_dollars_header::after {
+ content: '';
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ height: 2px;
+ background: linear-gradient(90deg, transparent, var(--accent-primary), transparent);
+ animation: dollarsHeaderGlow 3s ease-in-out infinite;
+}
+
+@keyframes dollarsHeaderGlow {
+ 0%, 100% { opacity: 0.3; }
+ 50% { opacity: 1; }
+}
+
+.o_dollars_chat .o_dollars_logo {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.o_dollars_chat .o_dollars_logo_icon {
+ width: 40px;
+ height: 40px;
+ background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
+ border-radius: var(--radius-md);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 20px;
+ box-shadow: var(--shadow-glow);
+ color: #000;
+}
+
+.o_dollars_chat .o_dollars_logo_text {
+ font-size: 20px;
+ font-weight: 700;
+ letter-spacing: 2px;
+ color: var(--accent-primary);
+}
+
+.o_dollars_chat .o_dollars_status {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+}
+
+.o_dollars_chat .o_dollars_status_item {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 12px;
+ color: var(--text-secondary);
+ font-family: monospace;
+}
+
+.o_dollars_chat .o_dollars_status_dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background: var(--success);
+ animation: dollarsPulse 2s infinite;
+}
+
+@keyframes dollarsPulse {
+ 0%, 100% { opacity: 1; transform: scale(1); }
+ 50% { opacity: 0.5; transform: scale(0.8); }
+}
+
+.o_dollars_chat .o_dollars_header_actions {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.o_dollars_chat .o_dollars_header_btn {
+ background: var(--bg-tertiary);
+ border: 1px solid var(--border-color);
+ color: var(--text-secondary);
+ padding: 8px 16px;
+ border-radius: var(--radius-md);
+ font-size: 13px;
+ cursor: pointer;
+ transition: all 0.15s ease;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.o_dollars_chat .o_dollars_header_btn:hover {
+ background: var(--bg-hover);
+ color: var(--text-primary);
+ border-color: var(--accent-primary);
+}
+
+/* Main Layout */
+.o_dollars_chat .o_dollars_main {
+ display: flex;
+ flex: 1;
+ overflow: hidden;
+}
+
+/* Sidebar */
+.o_dollars_chat .o_dollars_sidebar {
+ width: 320px;
+ background: var(--bg-secondary);
+ border-right: 1px solid var(--border-color);
+ display: flex;
+ flex-direction: column;
+ flex-shrink: 0;
+}
+
+.o_dollars_chat .o_dollars_sidebar_header {
+ padding: 16px;
+ border-bottom: 1px solid var(--border-color);
+}
+
+.o_dollars_chat .o_dollars_search {
+ position: relative;
+}
+
+.o_dollars_chat .o_dollars_search input {
+ width: 100%;
+ background: var(--bg-tertiary);
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-md);
+ padding: 10px 12px 10px 40px;
+ color: var(--text-primary);
+ font-size: 14px;
+ outline: none;
+ transition: all 0.15s ease;
+}
+
+.o_dollars_chat .o_dollars_search input:focus {
+ border-color: var(--accent-primary);
+ box-shadow: 0 0 0 3px var(--accent-glow);
+}
+
+.o_dollars_chat .o_dollars_search input::placeholder {
+ color: var(--text-muted);
+}
+
+.o_dollars_chat .o_dollars_search_icon {
+ position: absolute;
+ left: 12px;
+ top: 50%;
+ transform: translateY(-50%);
+ color: var(--text-muted);
+}
+
+.o_dollars_chat .o_dollars_conversations {
+ flex: 1;
+ overflow-y: auto;
+}
+
+.o_dollars_chat .o_dollars_conv_item {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 12px 16px;
+ cursor: pointer;
+ transition: all 0.15s ease;
+ border-left: 3px solid transparent;
+}
+
+.o_dollars_chat .o_dollars_conv_item:hover {
+ background: var(--bg-hover);
+}
+
+.o_dollars_chat .o_dollars_conv_item.active {
+ background: var(--bg-active);
+ border-left-color: var(--accent-primary);
+}
+
+.o_dollars_chat .o_dollars_conv_avatar {
+ width: 48px;
+ height: 48px;
+ border-radius: 50%;
+ background: var(--bg-tertiary);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 20px;
+ position: relative;
+ flex-shrink: 0;
+ color: var(--text-primary);
+}
+
+.o_dollars_chat .o_dollars_conv_avatar.online::after {
+ content: '';
+ position: absolute;
+ bottom: 2px;
+ right: 2px;
+ width: 12px;
+ height: 12px;
+ background: var(--success);
+ border-radius: 50%;
+ border: 2px solid var(--bg-secondary);
+}
+
+.o_dollars_chat .o_dollars_conv_info {
+ flex: 1;
+ min-width: 0;
+}
+
+.o_dollars_chat .o_dollars_conv_name {
+ font-weight: 600;
+ font-size: 14px;
+ margin-bottom: 2px;
+ color: var(--text-primary);
+}
+
+.o_dollars_chat .o_dollars_conv_preview {
+ font-size: 13px;
+ color: var(--text-secondary);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.o_dollars_chat .o_dollars_conv_meta {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-end;
+ gap: 4px;
+}
+
+.o_dollars_chat .o_dollars_conv_time {
+ font-size: 11px;
+ color: var(--text-muted);
+ font-family: monospace;
+}
+
+.o_dollars_chat .o_dollars_conv_badge {
+ background: var(--accent-primary);
+ color: #000;
+ font-size: 11px;
+ font-weight: 600;
+ padding: 2px 6px;
+ border-radius: var(--radius-full);
+ min-width: 20px;
+ text-align: center;
+}
+
+/* Chat Area */
+.o_dollars_chat .o_dollars_chat_area {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ background: var(--bg-primary);
+ min-width: 0;
+}
+
+.o_dollars_chat .o_dollars_chat_header {
+ padding: 16px 24px;
+ background: var(--bg-secondary);
+ border-bottom: 1px solid var(--border-color);
+ display: flex;
+ align-items: center;
+ gap: 16px;
+}
+
+.o_dollars_chat .o_dollars_chat_avatar {
+ width: 44px;
+ height: 44px;
+ border-radius: 50%;
+ background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 18px;
+ font-weight: 600;
+ color: #000;
+}
+
+.o_dollars_chat .o_dollars_chat_info {
+ flex: 1;
+}
+
+.o_dollars_chat .o_dollars_chat_name {
+ font-weight: 600;
+ font-size: 16px;
+ color: var(--text-primary);
+ margin-bottom: 2px;
+}
+
+.o_dollars_chat .o_dollars_chat_status {
+ font-size: 12px;
+ color: var(--text-secondary);
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.o_dollars_chat .o_dollars_chat_actions {
+ display: flex;
+ gap: 8px;
+}
+
+.o_dollars_chat .o_dollars_chat_btn {
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ background: var(--bg-tertiary);
+ border: 1px solid var(--border-color);
+ color: var(--text-secondary);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ transition: all 0.15s ease;
+}
+
+.o_dollars_chat .o_dollars_chat_btn:hover {
+ background: var(--bg-hover);
+ color: var(--accent-primary);
+ border-color: var(--accent-primary);
+}
+
+/* Messages */
+.o_dollars_chat .o_dollars_messages {
+ flex: 1;
+ overflow-y: auto;
+ padding: 24px;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+.o_dollars_chat .o_dollars_messages::-webkit-scrollbar {
+ width: 6px;
+}
+
+.o_dollars_chat .o_dollars_messages::-webkit-scrollbar-track {
+ background: var(--bg-primary);
+}
+
+.o_dollars_chat .o_dollars_messages::-webkit-scrollbar-thumb {
+ background: var(--border-color);
+ border-radius: 3px;
+}
+
+.o_dollars_chat .o_dollars_message {
+ display: flex;
+ gap: 12px;
+ max-width: 70%;
+ animation: dollarsMessageIn 0.2s ease-out;
+}
+
+@keyframes dollarsMessageIn {
+ from { opacity: 0; transform: translateY(10px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+.o_dollars_chat .o_dollars_message.inbound {
+ align-self: flex-start;
+}
+
+.o_dollars_chat .o_dollars_message.outbound {
+ align-self: flex-end;
+ flex-direction: row-reverse;
+}
+
+.o_dollars_chat .o_dollars_msg_avatar {
+ width: 36px;
+ height: 36px;
+ border-radius: 50%;
+ background: var(--bg-tertiary);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 14px;
+ flex-shrink: 0;
+ color: var(--text-primary);
+}
+
+.o_dollars_chat .o_dollars_message.outbound .o_dollars_msg_avatar {
+ background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
+ color: #000;
+}
+
+.o_dollars_chat .o_dollars_msg_content {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.o_dollars_chat .o_dollars_msg_header {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 12px;
+}
+
+.o_dollars_chat .o_dollars_msg_sender {
+ font-weight: 600;
+ color: var(--accent-primary);
+}
+
+.o_dollars_chat .o_dollars_message.inbound .o_dollars_msg_sender {
+ color: var(--info);
+}
+
+.o_dollars_chat .o_dollars_msg_time {
+ color: var(--text-muted);
+ font-family: monospace;
+ font-size: 11px;
+}
+
+.o_dollars_chat .o_dollars_msg_bubble {
+ background: var(--msg-inbound-bg);
+ padding: 12px 16px;
+ border-radius: var(--radius-lg);
+ border-top-left-radius: var(--radius-sm);
+ line-height: 1.5;
+ font-size: 14px;
+ color: var(--text-primary);
+}
+
+.o_dollars_chat .o_dollars_message.outbound .o_dollars_msg_bubble {
+ background: var(--msg-outbound-bg);
+ border: 1px solid var(--msg-outbound-border);
+ border-top-left-radius: var(--radius-lg);
+ border-top-right-radius: var(--radius-sm);
+}
+
+.o_dollars_chat .o_dollars_msg_status {
+ font-size: 11px;
+ color: var(--text-muted);
+ text-align: right;
+}
+
+.o_dollars_chat .o_dollars_msg_status.read {
+ color: var(--info);
+}
+
+/* Media */
+.o_dollars_chat .o_dollars_msg_image {
+ max-width: 280px;
+ max-height: 200px;
+ border-radius: var(--radius-md);
+ cursor: pointer;
+ display: block;
+ margin-bottom: 8px;
+}
+
+.o_dollars_chat .o_dollars_msg_audio {
+ width: 240px;
+ height: 40px;
+}
+
+.o_dollars_chat .o_dollars_msg_video {
+ max-width: 320px;
+ border-radius: var(--radius-md);
+}
+
+.o_dollars_chat .o_dollars_msg_doc {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 12px;
+ background: var(--bg-tertiary);
+ border-radius: var(--radius-md);
+ color: var(--text-primary);
+ text-decoration: none;
+ margin-bottom: 8px;
+}
+
+.o_dollars_chat .o_dollars_msg_doc:hover {
+ background: var(--bg-hover);
+}
+
+.o_dollars_chat .o_dollars_msg_doc i {
+ font-size: 24px;
+ color: var(--accent-primary);
+}
+
+/* Input Area */
+.o_dollars_chat .o_dollars_input_area {
+ padding: 16px 24px;
+ background: var(--bg-secondary);
+ border-top: 1px solid var(--border-color);
+ display: flex;
+ align-items: flex-end;
+ gap: 12px;
+}
+
+.o_dollars_chat .o_dollars_input_wrapper {
+ flex: 1;
+}
+
+.o_dollars_chat .o_dollars_input_wrapper textarea {
+ width: 100%;
+ background: var(--bg-tertiary);
+ border: 1px solid var(--border-color);
+ border-radius: var(--radius-lg);
+ padding: 12px 16px;
+ color: var(--text-primary);
+ font-size: 14px;
+ font-family: inherit;
+ resize: none;
+ outline: none;
+ min-height: 44px;
+ max-height: 120px;
+ transition: all 0.15s ease;
+}
+
+.o_dollars_chat .o_dollars_input_wrapper textarea:focus {
+ border-color: var(--accent-primary);
+ box-shadow: 0 0 0 3px var(--accent-glow);
+}
+
+.o_dollars_chat .o_dollars_input_wrapper textarea::placeholder {
+ color: var(--text-muted);
+}
+
+.o_dollars_chat .o_dollars_send_btn {
+ width: 48px;
+ height: 48px;
+ border-radius: 50%;
+ background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
+ border: none;
+ color: #000;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ transition: all 0.15s ease;
+ font-size: 18px;
+ box-shadow: var(--shadow-glow);
+}
+
+.o_dollars_chat .o_dollars_send_btn:hover {
+ transform: scale(1.05);
+}
+
+.o_dollars_chat .o_dollars_send_btn:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ transform: none;
+}
+
+/* Details Panel */
+.o_dollars_chat .o_dollars_details {
+ width: 280px;
+ background: var(--bg-secondary);
+ border-left: 1px solid var(--border-color);
+ display: flex;
+ flex-direction: column;
+ flex-shrink: 0;
+}
+
+.o_dollars_chat .o_dollars_details_header {
+ padding: 24px;
+ text-align: center;
+ border-bottom: 1px solid var(--border-color);
+}
+
+.o_dollars_chat .o_dollars_details_avatar {
+ width: 80px;
+ height: 80px;
+ border-radius: 50%;
+ background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 32px;
+ margin: 0 auto 16px;
+ box-shadow: var(--shadow-glow);
+ color: #000;
+}
+
+.o_dollars_chat .o_dollars_details_name {
+ font-size: 18px;
+ font-weight: 600;
+ margin-bottom: 4px;
+ color: var(--text-primary);
+}
+
+.o_dollars_chat .o_dollars_details_phone {
+ font-size: 14px;
+ color: var(--text-secondary);
+ font-family: monospace;
+}
+
+.o_dollars_chat .o_dollars_details_status {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ margin-top: 12px;
+ padding: 6px 12px;
+ background: var(--bg-tertiary);
+ border-radius: var(--radius-full);
+ font-size: 12px;
+ color: var(--text-secondary);
+}
+
+.o_dollars_chat .o_dollars_details_content {
+ flex: 1;
+ overflow-y: auto;
+ padding: 16px;
+}
+
+.o_dollars_chat .o_dollars_details_section {
+ margin-bottom: 24px;
+}
+
+.o_dollars_chat .o_dollars_details_section_title {
+ font-size: 11px;
+ text-transform: uppercase;
+ letter-spacing: 1px;
+ color: var(--text-muted);
+ margin-bottom: 12px;
+ font-weight: 600;
+}
+
+.o_dollars_chat .o_dollars_details_item {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 10px 12px;
+ background: var(--bg-tertiary);
+ border-radius: var(--radius-md);
+ margin-bottom: 8px;
+ cursor: pointer;
+ transition: all 0.15s ease;
+ color: var(--text-primary);
+}
+
+.o_dollars_chat .o_dollars_details_item:hover {
+ background: var(--bg-hover);
+}
+
+.o_dollars_chat .o_dollars_details_item i {
+ color: var(--accent-primary);
+ width: 20px;
+ text-align: center;
+}
+
+.o_dollars_chat .o_dollars_details_item span {
+ font-size: 13px;
+}
+
+/* Empty State */
+.o_dollars_chat .o_dollars_empty {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ text-align: center;
+ padding: 40px;
+}
+
+.o_dollars_chat .o_dollars_empty_icon {
+ width: 80px;
+ height: 80px;
+ background: var(--bg-tertiary);
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 32px;
+ margin-bottom: 24px;
+ color: var(--text-muted);
+}
+
+.o_dollars_chat .o_dollars_empty_title {
+ font-size: 20px;
+ font-weight: 600;
+ margin-bottom: 8px;
+ color: var(--text-primary);
+}
+
+.o_dollars_chat .o_dollars_empty_text {
+ color: var(--text-secondary);
+ font-size: 14px;
+}
+
+/* Loading */
+.o_dollars_chat .o_dollars_loading {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 40px;
+}
+
+.o_dollars_chat .o_dollars_spinner {
+ width: 40px;
+ height: 40px;
+ border: 3px solid var(--border-color);
+ border-top-color: var(--accent-primary);
+ border-radius: 50%;
+ animation: dollarsSpin 1s linear infinite;
+}
+
+@keyframes dollarsSpin {
+ to { transform: rotate(360deg); }
+}
+
+/* Responsive */
+@media (max-width: 1200px) {
+ .o_dollars_chat .o_dollars_details {
+ display: none;
+ }
+}
+
+@media (max-width: 768px) {
+ .o_dollars_chat .o_dollars_sidebar {
+ width: 100%;
+ position: absolute;
+ z-index: 10;
+ }
+}
diff --git a/odoo_whatsapp_hub/static/src/css/whatsapp.css b/odoo_whatsapp_hub/static/src/css/whatsapp.css
index 0c2e330..a52ff71 100644
--- a/odoo_whatsapp_hub/static/src/css/whatsapp.css
+++ b/odoo_whatsapp_hub/static/src/css/whatsapp.css
@@ -1,97 +1,175 @@
-/* WhatsApp Hub Styles */
+/* WhatsApp Hub - Scoped Styles */
+/* All styles use specific class prefixes to avoid affecting other Odoo views */
-/* WhatsApp Green */
-:root {
+/* ============================================
+ WHATSAPP CHAT FULLSCREEN (Theme selector)
+ ============================================ */
+
+.o_whatsapp_chat_fullscreen {
--whatsapp-green: #25D366;
--whatsapp-dark-green: #128C7E;
--whatsapp-light-green: #DCF8C6;
--whatsapp-bg: #E5DDD5;
}
-/* Chat container */
-.o_whatsapp_chat_container {
- display: flex;
- flex-direction: column;
- height: 100%;
- background: var(--whatsapp-bg);
- border-radius: 8px;
- overflow: hidden;
+/* WhatsApp Theme */
+.o_whatsapp_chat_fullscreen.theme-whatsapp {
+ --chat-bg: #E5DDD5;
+ --header-bg: #128C7E;
+ --header-text: #ffffff;
+ --input-bg: #F0F2F5;
+ --input-field-bg: #ffffff;
+ --input-text: #333333;
+ --input-border: #ddd;
+ --msg-inbound-bg: #ffffff;
+ --msg-inbound-text: #333333;
+ --msg-outbound-bg: #DCF8C6;
+ --msg-outbound-text: #333333;
+ --msg-meta-text: #667781;
+ --btn-primary-bg: #25D366;
+ --btn-primary-hover: #128C7E;
+ --status-read: #53bdeb;
+ --avatar-bg: rgba(255,255,255,0.2);
+ --avatar-text: #ffffff;
}
-/* Chat header */
-.o_whatsapp_chat_header {
- background: var(--whatsapp-dark-green);
- color: white;
- padding: 12px 16px;
+/* DRRR Theme */
+.o_whatsapp_chat_fullscreen.theme-drrr {
+ --chat-bg: #0a0a0a;
+ --header-bg: #1a1a1a;
+ --header-text: #00ff88;
+ --input-bg: #1a1a1a;
+ --input-field-bg: #0d0d0d;
+ --input-text: #00ff88;
+ --input-border: rgba(0, 255, 136, 0.2);
+ --msg-inbound-bg: transparent;
+ --msg-inbound-text: #00ccff;
+ --msg-outbound-bg: transparent;
+ --msg-outbound-text: #00ff88;
+ --msg-meta-text: #666666;
+ --btn-primary-bg: #00ff88;
+ --btn-primary-hover: #00cc6a;
+ --status-read: #00ccff;
+ --avatar-bg: rgba(0, 255, 136, 0.13);
+ --avatar-text: #00ff88;
+}
+
+.o_whatsapp_chat_fullscreen {
+ display: flex;
+ flex-direction: column;
+ height: calc(100vh - 46px);
+ background: var(--chat-bg);
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ transition: all 0.3s ease;
+}
+
+.o_whatsapp_chat_fullscreen.theme-drrr {
+ font-family: 'Courier New', monospace;
+}
+
+/* Header */
+.o_whatsapp_chat_fullscreen .o_whatsapp_chat_header {
+ background: var(--header-bg);
+ color: var(--header-text);
+ padding: 10px 16px;
display: flex;
align-items: center;
gap: 12px;
+ flex-shrink: 0;
+ box-shadow: 0 1px 3px rgba(0,0,0,0.2);
}
-.o_whatsapp_chat_header .avatar {
- width: 40px;
- height: 40px;
+.o_whatsapp_chat_fullscreen .avatar {
+ width: 45px;
+ height: 45px;
border-radius: 50%;
- background: white;
+ background: var(--avatar-bg);
+ color: var(--avatar-text);
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
- color: var(--whatsapp-dark-green);
+ font-size: 20px;
}
-.o_whatsapp_chat_header .contact-info {
+.o_whatsapp_chat_fullscreen .contact-info {
flex: 1;
}
-.o_whatsapp_chat_header .contact-name {
+.o_whatsapp_chat_fullscreen .contact-name {
font-weight: 600;
- font-size: 16px;
+ font-size: 17px;
}
-.o_whatsapp_chat_header .contact-status {
+.o_whatsapp_chat_fullscreen .contact-status {
+ font-size: 13px;
+ opacity: 0.85;
+}
+
+/* Theme Toggle */
+.o_whatsapp_chat_fullscreen .theme-toggle {
+ background: transparent;
+ border: 1px solid var(--header-text);
+ color: var(--header-text);
+ padding: 6px 12px;
+ border-radius: 4px;
+ cursor: pointer;
font-size: 12px;
- opacity: 0.8;
+ transition: all 0.2s ease;
}
-/* Messages container */
-.o_whatsapp_messages {
+.o_whatsapp_chat_fullscreen .theme-toggle:hover {
+ background: var(--header-text);
+ color: var(--header-bg);
+}
+
+/* Messages Container */
+.o_whatsapp_chat_fullscreen .o_whatsapp_messages_container {
flex: 1;
overflow-y: auto;
padding: 16px;
display: flex;
flex-direction: column;
- gap: 8px;
+ gap: 4px;
}
-/* Message bubble */
-.o_whatsapp_message {
+/* WhatsApp Theme Messages */
+.o_whatsapp_chat_fullscreen.theme-whatsapp .o_whatsapp_message {
max-width: 65%;
padding: 8px 12px;
border-radius: 8px;
position: relative;
word-wrap: break-word;
+ box-shadow: 0 1px 1px rgba(0,0,0,0.1);
}
-.o_whatsapp_message.inbound {
+.o_whatsapp_chat_fullscreen.theme-whatsapp .o_whatsapp_message.inbound {
align-self: flex-start;
- background: white;
+ background: var(--msg-inbound-bg);
+ color: var(--msg-inbound-text);
border-top-left-radius: 0;
}
-.o_whatsapp_message.outbound {
+.o_whatsapp_chat_fullscreen.theme-whatsapp .o_whatsapp_message.outbound {
align-self: flex-end;
- background: var(--whatsapp-light-green);
+ background: var(--msg-outbound-bg);
+ color: var(--msg-outbound-text);
border-top-right-radius: 0;
}
-.o_whatsapp_message .message-content {
- margin-bottom: 4px;
+.o_whatsapp_chat_fullscreen.theme-whatsapp .message-sender {
+ display: none;
}
-.o_whatsapp_message .message-meta {
+.o_whatsapp_chat_fullscreen.theme-whatsapp .message-content {
+ margin-bottom: 4px;
+ line-height: 1.4;
+ white-space: pre-wrap;
+}
+
+.o_whatsapp_chat_fullscreen.theme-whatsapp .message-meta {
font-size: 11px;
- color: #667781;
+ color: var(--msg-meta-text);
text-align: right;
display: flex;
align-items: center;
@@ -99,63 +177,178 @@
gap: 4px;
}
-.o_whatsapp_message .message-status {
- color: #53bdeb;
+/* DRRR Theme Messages */
+.o_whatsapp_chat_fullscreen.theme-drrr .o_whatsapp_messages_container {
+ padding: 20px;
+ gap: 2px;
}
-/* Input area */
-.o_whatsapp_input_area {
- background: #F0F2F5;
+.o_whatsapp_chat_fullscreen.theme-drrr .o_whatsapp_message {
+ max-width: 100%;
+ padding: 4px 0;
+ display: flex;
+ flex-wrap: wrap;
+ align-items: baseline;
+ gap: 8px;
+ background: transparent;
+ box-shadow: none;
+}
+
+.o_whatsapp_chat_fullscreen.theme-drrr .o_whatsapp_message.inbound,
+.o_whatsapp_chat_fullscreen.theme-drrr .o_whatsapp_message.outbound {
+ align-self: flex-start;
+}
+
+.o_whatsapp_chat_fullscreen.theme-drrr .o_whatsapp_message.inbound {
+ color: var(--msg-inbound-text);
+}
+
+.o_whatsapp_chat_fullscreen.theme-drrr .o_whatsapp_message.outbound {
+ color: var(--msg-outbound-text);
+}
+
+.o_whatsapp_chat_fullscreen.theme-drrr .message-sender {
+ display: inline;
+ font-weight: bold;
+}
+
+.o_whatsapp_chat_fullscreen.theme-drrr .message-sender::before {
+ content: '[';
+}
+
+.o_whatsapp_chat_fullscreen.theme-drrr .message-sender::after {
+ content: ']';
+}
+
+.o_whatsapp_chat_fullscreen.theme-drrr .message-content {
+ display: inline;
+ margin: 0;
+}
+
+.o_whatsapp_chat_fullscreen.theme-drrr .message-meta {
+ display: inline;
+ font-size: 10px;
+ color: var(--msg-meta-text);
+ margin-left: auto;
+}
+
+/* Input Area */
+.o_whatsapp_chat_fullscreen .o_whatsapp_input_area {
+ background: var(--input-bg);
padding: 12px 16px;
display: flex;
align-items: center;
gap: 12px;
+ flex-shrink: 0;
+ border-top: 1px solid var(--input-border);
}
-.o_whatsapp_input_area input {
+.o_whatsapp_chat_fullscreen .o_whatsapp_input_area input {
flex: 1;
border: none;
- border-radius: 20px;
- padding: 10px 16px;
+ border-radius: 24px;
+ padding: 12px 20px;
outline: none;
+ font-size: 15px;
+ background: var(--input-field-bg);
+ color: var(--input-text);
+ font-family: inherit;
}
-.o_whatsapp_input_area button {
- background: var(--whatsapp-green);
- color: white;
- border: none;
- border-radius: 50%;
- width: 40px;
- height: 40px;
- cursor: pointer;
+.o_whatsapp_chat_fullscreen.theme-drrr .o_whatsapp_input_area input {
+ border-radius: 4px;
+ border: 1px solid var(--input-border);
+}
+
+.o_whatsapp_chat_fullscreen .o_whatsapp_input_area input:focus {
+ box-shadow: 0 0 0 2px var(--btn-primary-bg);
+}
+
+.o_whatsapp_chat_fullscreen .o_whatsapp_input_area .btn {
+ width: 48px;
+ height: 48px;
display: flex;
align-items: center;
justify-content: center;
+ font-size: 18px;
+ background: var(--btn-primary-bg);
+ border-color: var(--btn-primary-bg);
+ color: white;
}
-.o_whatsapp_input_area button:hover {
- background: var(--whatsapp-dark-green);
+.o_whatsapp_chat_fullscreen.theme-drrr .o_whatsapp_input_area .btn {
+ color: #0a0a0a;
}
-/* Status badges */
+.o_whatsapp_chat_fullscreen .o_whatsapp_input_area .btn:hover {
+ background: var(--btn-primary-hover);
+}
+
+.o_whatsapp_chat_fullscreen .o_whatsapp_input_area .btn:disabled {
+ opacity: 0.5;
+}
+
+/* Media */
+.o_whatsapp_chat_fullscreen .o_whatsapp_media_image {
+ max-width: 100%;
+ max-height: 250px;
+ border-radius: 4px;
+ cursor: pointer;
+}
+
+.o_whatsapp_chat_fullscreen .o_whatsapp_media_audio {
+ max-width: 100%;
+ height: 40px;
+}
+
+.o_whatsapp_chat_fullscreen .o_whatsapp_media_video {
+ max-width: 100%;
+ max-height: 200px;
+ border-radius: 4px;
+}
+
+.o_whatsapp_chat_fullscreen .o_whatsapp_media_doc {
+ display: flex;
+ align-items: center;
+ padding: 8px 12px;
+ background: rgba(0, 0, 0, 0.05);
+ border-radius: 4px;
+ color: inherit;
+ text-decoration: none;
+}
+
+/* ============================================
+ FORM VIEW STYLES (for Odoo standard views)
+ ============================================ */
+
+.o_whatsapp_chat_wrapper {
+ background: #e5ddd5 !important;
+ border-radius: 8px;
+ padding: 16px;
+ min-height: 300px;
+ max-height: 500px;
+ overflow-y: auto;
+}
+
+/* Status badges in list/form views */
.o_whatsapp_status_connected {
- color: var(--whatsapp-green);
+ color: #25D366;
}
.o_whatsapp_status_disconnected {
color: #dc3545;
}
-/* Partner WhatsApp button */
+/* WhatsApp button style */
.btn-whatsapp {
- background-color: var(--whatsapp-green);
- border-color: var(--whatsapp-green);
+ background-color: #25D366;
+ border-color: #25D366;
color: white;
}
.btn-whatsapp:hover {
- background-color: var(--whatsapp-dark-green);
- border-color: var(--whatsapp-dark-green);
+ background-color: #128C7E;
+ border-color: #128C7E;
color: white;
}
diff --git a/odoo_whatsapp_hub/static/src/js/chat_action.js b/odoo_whatsapp_hub/static/src/js/chat_action.js
new file mode 100644
index 0000000..aaeea24
--- /dev/null
+++ b/odoo_whatsapp_hub/static/src/js/chat_action.js
@@ -0,0 +1,232 @@
+/** @odoo-module **/
+
+import { Component, useState, useRef, onMounted, onWillStart } from "@odoo/owl";
+import { registry } from "@web/core/registry";
+import { useService } from "@web/core/utils/hooks";
+import { _t } from "@web/core/l10n/translation";
+
+const THEMES = {
+ whatsapp: {
+ name: "WhatsApp",
+ class: "theme-whatsapp",
+ icon: "fa-whatsapp",
+ },
+ drrr: {
+ name: "DRRR",
+ class: "theme-drrr",
+ icon: "fa-terminal",
+ },
+};
+
+export class WhatsAppChat extends Component {
+ static template = "odoo_whatsapp_hub.WhatsAppChat";
+ static props = {
+ action: { type: Object },
+ actionId: { type: [Number, Boolean], optional: true },
+ };
+
+ setup() {
+ this.orm = useService("orm");
+ this.action = useService("action");
+ this.notification = useService("notification");
+
+ // Load saved theme or default to whatsapp
+ const savedTheme = localStorage.getItem("whatsapp_chat_theme") || "whatsapp";
+
+ this.state = useState({
+ messages: [],
+ newMessage: "",
+ conversation: null,
+ loading: true,
+ sending: false,
+ currentTheme: savedTheme,
+ });
+
+ this.messagesEndRef = useRef("messagesEnd");
+ this.inputRef = useRef("messageInput");
+
+ onWillStart(async () => {
+ await this.loadConversation();
+ });
+
+ onMounted(() => {
+ this.scrollToBottom();
+ if (this.inputRef.el) {
+ this.inputRef.el.focus();
+ }
+ });
+ }
+
+ get conversationId() {
+ return this.props.action.context?.active_id ||
+ this.props.action.params?.conversation_id;
+ }
+
+ get themeClass() {
+ return THEMES[this.state.currentTheme]?.class || "theme-whatsapp";
+ }
+
+ get currentThemeName() {
+ return THEMES[this.state.currentTheme]?.name || "WhatsApp";
+ }
+
+ get nextThemeName() {
+ const nextTheme = this.state.currentTheme === "whatsapp" ? "drrr" : "whatsapp";
+ return THEMES[nextTheme]?.name || "DRRR";
+ }
+
+ toggleTheme() {
+ const newTheme = this.state.currentTheme === "whatsapp" ? "drrr" : "whatsapp";
+ this.state.currentTheme = newTheme;
+ localStorage.setItem("whatsapp_chat_theme", newTheme);
+ }
+
+ async loadConversation() {
+ this.state.loading = true;
+ try {
+ const conversationId = this.conversationId;
+ if (!conversationId) {
+ this.state.loading = false;
+ return;
+ }
+
+ // Load conversation data
+ const [conversation] = await this.orm.read(
+ "whatsapp.conversation",
+ [conversationId],
+ ["display_name", "phone_number", "account_id", "status", "partner_id"]
+ );
+ this.state.conversation = conversation;
+
+ // Load messages
+ const messages = await this.orm.searchRead(
+ "whatsapp.message",
+ [["conversation_id", "=", conversationId]],
+ ["content", "direction", "create_date", "message_type", "media_url", "status", "sent_by_id"],
+ { order: "create_date asc" }
+ );
+ this.state.messages = messages;
+
+ // Mark messages as read
+ const unreadIds = messages
+ .filter(m => m.direction === "inbound" && !m.is_read)
+ .map(m => m.id);
+ if (unreadIds.length > 0) {
+ await this.orm.write("whatsapp.message", unreadIds, { is_read: true });
+ }
+ } catch (error) {
+ console.error("Error loading conversation:", error);
+ this.notification.add(_t("Error loading conversation"), { type: "danger" });
+ }
+ this.state.loading = false;
+ setTimeout(() => this.scrollToBottom(), 100);
+ }
+
+ scrollToBottom() {
+ if (this.messagesEndRef.el) {
+ this.messagesEndRef.el.scrollIntoView({ behavior: "smooth" });
+ }
+ }
+
+ onInputChange(ev) {
+ this.state.newMessage = ev.target.value;
+ }
+
+ onKeyDown(ev) {
+ if (ev.key === "Enter" && !ev.shiftKey) {
+ ev.preventDefault();
+ this.sendMessage();
+ }
+ }
+
+ async sendMessage() {
+ const message = this.state.newMessage.trim();
+ if (!message || this.state.sending) return;
+
+ this.state.sending = true;
+ try {
+ // Call the send method on the conversation
+ await this.orm.call(
+ "whatsapp.conversation",
+ "send_message_from_chat",
+ [[this.conversationId], message]
+ );
+
+ this.state.newMessage = "";
+ await this.loadConversation();
+
+ this.notification.add(_t("Message sent"), { type: "success" });
+ } catch (error) {
+ console.error("Error sending message:", error);
+ this.notification.add(_t("Error sending message: ") + error.message, { type: "danger" });
+ }
+ this.state.sending = false;
+
+ if (this.inputRef.el) {
+ this.inputRef.el.focus();
+ }
+ }
+
+ async refreshMessages() {
+ await this.loadConversation();
+ }
+
+ formatTime(dateStr) {
+ if (!dateStr) return "";
+ const date = new Date(dateStr);
+ return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
+ }
+
+ formatDate(dateStr) {
+ if (!dateStr) return "";
+ const date = new Date(dateStr);
+ const today = new Date();
+ const yesterday = new Date(today);
+ yesterday.setDate(yesterday.getDate() - 1);
+
+ if (date.toDateString() === today.toDateString()) {
+ return _t("Today");
+ } else if (date.toDateString() === yesterday.toDateString()) {
+ return _t("Yesterday");
+ }
+ return date.toLocaleDateString();
+ }
+
+ getMessageClass(message) {
+ return message.direction === "outbound" ? "outbound" : "inbound";
+ }
+
+ getSenderName(message) {
+ if (message.direction === "outbound") {
+ // For DRRR theme, show "Tú" or agent name
+ if (message.sent_by_id && message.sent_by_id[1]) {
+ return message.sent_by_id[1];
+ }
+ return "Tú";
+ } else {
+ // For inbound, show contact name
+ return this.state.conversation?.display_name || "Cliente";
+ }
+ }
+
+ getStatusIcon(status) {
+ switch (status) {
+ case "sent": return "✓";
+ case "delivered": return "✓✓";
+ case "read": return "✓✓";
+ default: return "⏳";
+ }
+ }
+
+ goBack() {
+ this.action.doAction({
+ type: "ir.actions.act_window",
+ res_model: "whatsapp.conversation",
+ res_id: this.conversationId,
+ views: [[false, "form"]],
+ target: "current",
+ });
+ }
+}
+
+registry.category("actions").add("whatsapp_chat", WhatsAppChat);
diff --git a/odoo_whatsapp_hub/static/src/js/chat_widget.js b/odoo_whatsapp_hub/static/src/js/chat_widget.js
index 75160d6..e752eb7 100644
--- a/odoo_whatsapp_hub/static/src/js/chat_widget.js
+++ b/odoo_whatsapp_hub/static/src/js/chat_widget.js
@@ -55,7 +55,7 @@ export class WhatsAppChatWidget extends Component {
const messages = await this.orm.searchRead(
"whatsapp.message",
[["conversation_id", "=", this.state.conversation.id]],
- ["id", "direction", "content", "message_type", "status", "create_date"],
+ ["id", "direction", "content", "message_type", "media_url", "status", "create_date"],
{ order: "create_date asc" }
);
this.state.messages = messages;
diff --git a/odoo_whatsapp_hub/static/src/js/dollars_chat.js b/odoo_whatsapp_hub/static/src/js/dollars_chat.js
new file mode 100644
index 0000000..6d60643
--- /dev/null
+++ b/odoo_whatsapp_hub/static/src/js/dollars_chat.js
@@ -0,0 +1,254 @@
+/** @odoo-module **/
+
+import { Component, useState, useRef, onMounted, onWillStart } from "@odoo/owl";
+import { registry } from "@web/core/registry";
+import { useService } from "@web/core/utils/hooks";
+import { _t } from "@web/core/l10n/translation";
+
+export class DollarsChat extends Component {
+ static template = "odoo_whatsapp_hub.DollarsChat";
+ static props = {
+ action: { type: Object, optional: true },
+ actionId: { type: [Number, Boolean], optional: true },
+ };
+
+ setup() {
+ this.orm = useService("orm");
+ this.action = useService("action");
+ this.notification = useService("notification");
+
+ this.state = useState({
+ conversations: [],
+ messages: [],
+ selectedConversation: null,
+ newMessage: "",
+ loading: true,
+ sending: false,
+ searchQuery: "",
+ });
+
+ this.messagesEndRef = useRef("messagesEnd");
+ this.inputRef = useRef("messageInput");
+
+ onWillStart(async () => {
+ await this.loadConversations();
+ });
+
+ onMounted(() => {
+ // Auto-refresh every 10 seconds
+ this.refreshInterval = setInterval(() => {
+ if (this.state.selectedConversation) {
+ this.loadMessages(this.state.selectedConversation.id, true);
+ }
+ this.loadConversations(true);
+ }, 10000);
+ });
+ }
+
+ willUnmount() {
+ if (this.refreshInterval) {
+ clearInterval(this.refreshInterval);
+ }
+ }
+
+ get filteredConversations() {
+ if (!this.state.searchQuery) {
+ return this.state.conversations;
+ }
+ const query = this.state.searchQuery.toLowerCase();
+ return this.state.conversations.filter(conv =>
+ conv.display_name?.toLowerCase().includes(query) ||
+ conv.phone_number?.toLowerCase().includes(query)
+ );
+ }
+
+ async loadConversations(silent = false) {
+ if (!silent) {
+ this.state.loading = true;
+ }
+ try {
+ const conversations = await this.orm.searchRead(
+ "whatsapp.conversation",
+ [],
+ ["display_name", "phone_number", "status", "last_message_at", "last_message_preview", "unread_count", "partner_id"],
+ { order: "last_message_at desc", limit: 50 }
+ );
+ this.state.conversations = conversations;
+ } catch (error) {
+ console.error("Error loading conversations:", error);
+ }
+ if (!silent) {
+ this.state.loading = false;
+ }
+ }
+
+ async selectConversation(conv) {
+ this.state.selectedConversation = conv;
+ await this.loadMessages(conv.id);
+ }
+
+ async loadMessages(conversationId, silent = false) {
+ try {
+ const messages = await this.orm.searchRead(
+ "whatsapp.message",
+ [["conversation_id", "=", conversationId]],
+ ["content", "direction", "create_date", "message_type", "media_url", "status", "sent_by_id", "is_read"],
+ { order: "create_date asc" }
+ );
+ this.state.messages = messages;
+
+ // Mark as read
+ const unreadIds = messages
+ .filter(m => m.direction === "inbound" && !m.is_read)
+ .map(m => m.id);
+ if (unreadIds.length > 0) {
+ await this.orm.write("whatsapp.message", unreadIds, { is_read: true });
+ // Refresh conversation to update unread count
+ await this.loadConversations(true);
+ }
+
+ setTimeout(() => this.scrollToBottom(), 100);
+ } catch (error) {
+ console.error("Error loading messages:", error);
+ }
+ }
+
+ scrollToBottom() {
+ if (this.messagesEndRef.el) {
+ this.messagesEndRef.el.scrollIntoView({ behavior: "smooth" });
+ }
+ }
+
+ onSearchInput(ev) {
+ this.state.searchQuery = ev.target.value;
+ }
+
+ onMessageInput(ev) {
+ this.state.newMessage = ev.target.value;
+ // Auto-resize textarea
+ ev.target.style.height = 'auto';
+ ev.target.style.height = Math.min(ev.target.scrollHeight, 120) + 'px';
+ }
+
+ onKeyDown(ev) {
+ if (ev.key === "Enter" && !ev.shiftKey) {
+ ev.preventDefault();
+ this.sendMessage();
+ }
+ }
+
+ async sendMessage() {
+ const message = this.state.newMessage.trim();
+ if (!message || this.state.sending || !this.state.selectedConversation) return;
+
+ this.state.sending = true;
+ try {
+ await this.orm.call(
+ "whatsapp.conversation",
+ "send_message_from_chat",
+ [[this.state.selectedConversation.id], message]
+ );
+
+ this.state.newMessage = "";
+ if (this.inputRef.el) {
+ this.inputRef.el.style.height = 'auto';
+ }
+ await this.loadMessages(this.state.selectedConversation.id);
+ await this.loadConversations(true);
+
+ } catch (error) {
+ console.error("Error sending message:", error);
+ this.notification.add(_t("Error al enviar: ") + (error.message || error), { type: "danger" });
+ }
+ this.state.sending = false;
+
+ if (this.inputRef.el) {
+ this.inputRef.el.focus();
+ }
+ }
+
+ async refreshMessages() {
+ if (this.state.selectedConversation) {
+ await this.loadMessages(this.state.selectedConversation.id);
+ }
+ }
+
+ formatTime(dateStr) {
+ if (!dateStr) return "";
+ const date = new Date(dateStr);
+ const now = new Date();
+ const isToday = date.toDateString() === now.toDateString();
+
+ if (isToday) {
+ return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
+ }
+
+ const yesterday = new Date(now);
+ yesterday.setDate(yesterday.getDate() - 1);
+ if (date.toDateString() === yesterday.toDateString()) {
+ return "Ayer";
+ }
+
+ return date.toLocaleDateString([], { day: "2-digit", month: "2-digit" });
+ }
+
+ formatMessageTime(dateStr) {
+ if (!dateStr) return "";
+ const date = new Date(dateStr);
+ return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
+ }
+
+ getInitial(name) {
+ if (!name) return "?";
+ return name.charAt(0).toUpperCase();
+ }
+
+ getStatusIcon(status) {
+ switch (status) {
+ case "sent": return "✓";
+ case "delivered": return "✓✓";
+ case "read": return "✓✓";
+ default: return "⏳";
+ }
+ }
+
+ getStatusClass(convStatus) {
+ const statusMap = {
+ 'bot': 'Bot',
+ 'waiting': 'En espera',
+ 'active': 'Activa',
+ 'resolved': 'Resuelta'
+ };
+ return statusMap[convStatus] || convStatus;
+ }
+
+ async markAsResolved() {
+ if (!this.state.selectedConversation) return;
+ try {
+ await this.orm.call(
+ "whatsapp.conversation",
+ "action_mark_resolved",
+ [[this.state.selectedConversation.id]]
+ );
+ await this.loadConversations(true);
+ this.state.selectedConversation.status = 'resolved';
+ this.notification.add(_t("Conversación marcada como resuelta"), { type: "success" });
+ } catch (error) {
+ console.error("Error:", error);
+ }
+ }
+
+ openInOdoo() {
+ if (!this.state.selectedConversation) return;
+ this.action.doAction({
+ type: "ir.actions.act_window",
+ res_model: "whatsapp.conversation",
+ res_id: this.state.selectedConversation.id,
+ views: [[false, "form"]],
+ target: "current",
+ });
+ }
+}
+
+// Register as the main WhatsApp action
+registry.category("actions").add("dollars_whatsapp_chat", DollarsChat);
diff --git a/odoo_whatsapp_hub/static/src/xml/chat_template.xml b/odoo_whatsapp_hub/static/src/xml/chat_template.xml
new file mode 100644
index 0000000..e114d7c
--- /dev/null
+++ b/odoo_whatsapp_hub/static/src/xml/chat_template.xml
@@ -0,0 +1,138 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ [SYSTEM] Conectando al servidor...
+
+
+ Cargando mensajes...
+
+
+
+
+
+
+
+
+ [SYSTEM] No hay mensajes en este chat
+ [SYSTEM] Escribe algo para comenzar...
+
+
+
+ No hay mensajes aún
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Documento
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/odoo_whatsapp_hub/static/src/xml/chat_widget.xml b/odoo_whatsapp_hub/static/src/xml/chat_widget.xml
index f4518e2..ceec03f 100644
--- a/odoo_whatsapp_hub/static/src/xml/chat_widget.xml
+++ b/odoo_whatsapp_hub/static/src/xml/chat_widget.xml
@@ -23,7 +23,37 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+