feat: Major WhatsApp integration update with Odoo and pause/resume
## Frontend - Add media display (images, audio, video, docs) in Inbox - Add pause/resume functionality for WhatsApp accounts - Fix media URLs to use nginx proxy (relative URLs) ## API Gateway - Add /accounts/:id/pause and /accounts/:id/resume endpoints - Fix media URL handling for browser access ## WhatsApp Core - Add pauseSession() - disconnect without logout - Add resumeSession() - reconnect using saved credentials - Add media download and storage for incoming messages - Serve media files via /media/ static route ## Odoo Module (odoo_whatsapp_hub) - Add Chat Hub interface with DOLLARS theme (dark, 3-column layout) - Add WhatsApp/DRRR theme switcher for chat view - Add "ABRIR CHAT" button in conversation form - Add send_message_from_chat() method - Add security/ir.model.access.csv - Fix CSS scoping to avoid breaking Odoo UI - Update webhook to handle message events properly ## Documentation - Add docs/CONTEXTO_DESARROLLO.md with complete project context ## Infrastructure - Add whatsapp_media Docker volume - Configure nginx proxy for /media/ route - Update .gitignore to track src/sessions/ source files Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
774
odoo_whatsapp_hub/static/src/css/dollars_theme.css
Normal file
774
odoo_whatsapp_hub/static/src/css/dollars_theme.css
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
232
odoo_whatsapp_hub/static/src/js/chat_action.js
Normal file
232
odoo_whatsapp_hub/static/src/js/chat_action.js
Normal file
@@ -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);
|
||||
@@ -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;
|
||||
|
||||
254
odoo_whatsapp_hub/static/src/js/dollars_chat.js
Normal file
254
odoo_whatsapp_hub/static/src/js/dollars_chat.js
Normal file
@@ -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);
|
||||
138
odoo_whatsapp_hub/static/src/xml/chat_template.xml
Normal file
138
odoo_whatsapp_hub/static/src/xml/chat_template.xml
Normal file
@@ -0,0 +1,138 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="odoo_whatsapp_hub.WhatsAppChat">
|
||||
<div t-attf-class="o_whatsapp_chat_fullscreen {{ themeClass }}">
|
||||
<!-- Header -->
|
||||
<div class="o_whatsapp_chat_header">
|
||||
<button class="btn btn-link" style="color: inherit;" t-on-click="goBack">
|
||||
<i class="fa fa-arrow-left fa-lg"/>
|
||||
</button>
|
||||
<div class="avatar">
|
||||
<t t-if="state.conversation">
|
||||
<t t-esc="(state.conversation.display_name || 'W')[0].toUpperCase()"/>
|
||||
</t>
|
||||
</div>
|
||||
<div class="contact-info">
|
||||
<div class="contact-name">
|
||||
<t t-esc="state.conversation?.display_name || 'Cargando...'"/>
|
||||
</div>
|
||||
<div class="contact-status">
|
||||
<t t-esc="state.conversation?.phone_number || ''"/>
|
||||
</div>
|
||||
</div>
|
||||
<button class="theme-toggle" t-on-click="toggleTheme">
|
||||
<i t-attf-class="fa {{ state.currentTheme === 'whatsapp' ? 'fa-terminal' : 'fa-whatsapp' }} me-1"/>
|
||||
<t t-esc="nextThemeName"/>
|
||||
</button>
|
||||
<button class="btn btn-link" style="color: inherit;" t-on-click="refreshMessages">
|
||||
<i class="fa fa-refresh fa-lg"/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Messages Container -->
|
||||
<div class="o_whatsapp_messages_container">
|
||||
<t t-if="state.loading">
|
||||
<div class="text-center py-5">
|
||||
<i class="fa fa-spinner fa-spin fa-2x"/>
|
||||
<p class="mt-2">
|
||||
<t t-if="state.currentTheme === 'drrr'">
|
||||
[SYSTEM] Conectando al servidor...
|
||||
</t>
|
||||
<t t-else="">
|
||||
Cargando mensajes...
|
||||
</t>
|
||||
</p>
|
||||
</div>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-if="state.messages.length === 0">
|
||||
<div class="text-center py-5" style="opacity: 0.6;">
|
||||
<t t-if="state.currentTheme === 'drrr'">
|
||||
<p>[SYSTEM] No hay mensajes en este chat</p>
|
||||
<p>[SYSTEM] Escribe algo para comenzar...</p>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<i class="fa fa-comments fa-3x mb-3"/>
|
||||
<p>No hay mensajes aún</p>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
<t t-foreach="state.messages" t-as="message" t-key="message.id">
|
||||
<div t-attf-class="o_whatsapp_message {{ getMessageClass(message) }}">
|
||||
<!-- Sender name (visible in DRRR theme) -->
|
||||
<span class="message-sender">
|
||||
<t t-esc="getSenderName(message)"/>
|
||||
</span>
|
||||
|
||||
<!-- Media content -->
|
||||
<t t-if="message.media_url">
|
||||
<t t-if="message.message_type === 'image'">
|
||||
<img t-att-src="message.media_url" class="o_whatsapp_media_image"
|
||||
t-on-click="() => window.open(message.media_url, '_blank')"/>
|
||||
</t>
|
||||
<t t-elif="message.message_type === 'audio'">
|
||||
<audio controls="" class="o_whatsapp_media_audio">
|
||||
<source t-att-src="message.media_url"/>
|
||||
</audio>
|
||||
</t>
|
||||
<t t-elif="message.message_type === 'video'">
|
||||
<video controls="" class="o_whatsapp_media_video">
|
||||
<source t-att-src="message.media_url"/>
|
||||
</video>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<a t-att-href="message.media_url" target="_blank" class="o_whatsapp_media_doc">
|
||||
<i class="fa fa-file me-2"/>
|
||||
Documento
|
||||
</a>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<!-- Text content -->
|
||||
<t t-if="message.content">
|
||||
<span class="message-content">
|
||||
<t t-esc="message.content"/>
|
||||
</span>
|
||||
</t>
|
||||
|
||||
<!-- Meta info -->
|
||||
<span class="message-meta">
|
||||
<span class="message-time">
|
||||
<t t-esc="formatTime(message.create_date)"/>
|
||||
</span>
|
||||
<t t-if="message.direction === 'outbound'">
|
||||
<span t-attf-class="message-status {{ message.status === 'read' ? 'text-info' : '' }}">
|
||||
<t t-esc="getStatusIcon(message.status)"/>
|
||||
</span>
|
||||
</t>
|
||||
</span>
|
||||
</div>
|
||||
</t>
|
||||
<div t-ref="messagesEnd"/>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
<!-- Input Area -->
|
||||
<div class="o_whatsapp_input_area">
|
||||
<input type="text"
|
||||
t-ref="messageInput"
|
||||
class="form-control"
|
||||
t-att-placeholder="state.currentTheme === 'drrr' ? '> Escribe tu mensaje...' : 'Escribe un mensaje...'"
|
||||
t-att-value="state.newMessage"
|
||||
t-on-input="onInputChange"
|
||||
t-on-keydown="onKeyDown"
|
||||
t-att-disabled="state.sending"/>
|
||||
<button class="btn btn-success rounded-circle"
|
||||
t-on-click="sendMessage"
|
||||
t-att-disabled="state.sending || !state.newMessage.trim()">
|
||||
<t t-if="state.sending">
|
||||
<i class="fa fa-spinner fa-spin"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<i class="fa fa-paper-plane"/>
|
||||
</t>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -23,7 +23,37 @@
|
||||
<div class="o_whatsapp_messages">
|
||||
<t t-foreach="state.messages" t-as="message" t-key="message.id">
|
||||
<div t-attf-class="o_whatsapp_message #{message.direction}">
|
||||
<div class="message-content" t-esc="message.content"/>
|
||||
<div class="message-content">
|
||||
<!-- Image message -->
|
||||
<t t-if="message.message_type === 'image' and message.media_url">
|
||||
<img t-att-src="message.media_url" class="o_whatsapp_media_image" t-att-alt="message.content"/>
|
||||
<t t-if="message.content and message.content !== '[Image]'">
|
||||
<div class="mt-1" t-esc="message.content"/>
|
||||
</t>
|
||||
</t>
|
||||
<!-- Audio message -->
|
||||
<t t-elif="message.message_type === 'audio' and message.media_url">
|
||||
<audio controls="controls" class="o_whatsapp_media_audio">
|
||||
<source t-att-src="message.media_url" type="audio/ogg"/>
|
||||
</audio>
|
||||
</t>
|
||||
<!-- Video message -->
|
||||
<t t-elif="message.message_type === 'video' and message.media_url">
|
||||
<video controls="controls" class="o_whatsapp_media_video">
|
||||
<source t-att-src="message.media_url" type="video/mp4"/>
|
||||
</video>
|
||||
</t>
|
||||
<!-- Document message -->
|
||||
<t t-elif="message.message_type === 'document' and message.media_url">
|
||||
<a t-att-href="message.media_url" target="_blank" class="o_whatsapp_media_doc">
|
||||
<i class="fa fa-file-o me-1"/> <t t-esc="message.content or 'Documento'"/>
|
||||
</a>
|
||||
</t>
|
||||
<!-- Text message -->
|
||||
<t t-else="">
|
||||
<t t-esc="message.content"/>
|
||||
</t>
|
||||
</div>
|
||||
<div class="message-meta">
|
||||
<span t-esc="formatTime(message.create_date)"/>
|
||||
<t t-if="message.direction === 'outbound'">
|
||||
|
||||
282
odoo_whatsapp_hub/static/src/xml/dollars_template.xml
Normal file
282
odoo_whatsapp_hub/static/src/xml/dollars_template.xml
Normal file
@@ -0,0 +1,282 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="odoo_whatsapp_hub.DollarsChat">
|
||||
<div class="o_dollars_chat">
|
||||
<!-- Header -->
|
||||
<div class="o_dollars_header">
|
||||
<div class="o_dollars_logo">
|
||||
<div class="o_dollars_logo_icon">
|
||||
<i class="fa fa-whatsapp"/>
|
||||
</div>
|
||||
<span class="o_dollars_logo_text">WHATSAPP HUB</span>
|
||||
</div>
|
||||
|
||||
<div class="o_dollars_status">
|
||||
<div class="o_dollars_status_item">
|
||||
<span class="o_dollars_status_dot"/>
|
||||
<span>Conectado</span>
|
||||
</div>
|
||||
<div class="o_dollars_status_item">
|
||||
<i class="fa fa-lock"/>
|
||||
<span>Cifrado E2E</span>
|
||||
</div>
|
||||
<div class="o_dollars_status_item">
|
||||
<i class="fa fa-comments"/>
|
||||
<span><t t-esc="state.conversations.length"/> chats</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="o_dollars_header_actions">
|
||||
<button class="o_dollars_header_btn" t-on-click="() => this.loadConversations()">
|
||||
<i class="fa fa-refresh"/>
|
||||
<span>Actualizar</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main 3-column layout -->
|
||||
<div class="o_dollars_main">
|
||||
<!-- Left Sidebar - Conversations -->
|
||||
<div class="o_dollars_sidebar">
|
||||
<div class="o_dollars_sidebar_header">
|
||||
<div class="o_dollars_search">
|
||||
<i class="fa fa-search o_dollars_search_icon"/>
|
||||
<input type="text"
|
||||
placeholder="Buscar conversación..."
|
||||
t-att-value="state.searchQuery"
|
||||
t-on-input="onSearchInput"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="o_dollars_conversations">
|
||||
<t t-if="state.loading">
|
||||
<div class="o_dollars_loading">
|
||||
<div class="o_dollars_spinner"/>
|
||||
</div>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-foreach="filteredConversations" t-as="conv" t-key="conv.id">
|
||||
<div t-attf-class="o_dollars_conv_item {{ state.selectedConversation?.id === conv.id ? 'active' : '' }}"
|
||||
t-on-click="() => this.selectConversation(conv)">
|
||||
<div t-attf-class="o_dollars_conv_avatar {{ conv.status === 'active' ? 'online' : '' }}">
|
||||
<t t-esc="getInitial(conv.display_name)"/>
|
||||
</div>
|
||||
<div class="o_dollars_conv_info">
|
||||
<div class="o_dollars_conv_name">
|
||||
<t t-esc="conv.display_name || conv.phone_number"/>
|
||||
</div>
|
||||
<div class="o_dollars_conv_preview">
|
||||
<t t-esc="conv.last_message_preview || 'Sin mensajes'"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_dollars_conv_meta">
|
||||
<span class="o_dollars_conv_time">
|
||||
<t t-esc="formatTime(conv.last_message_at)"/>
|
||||
</span>
|
||||
<t t-if="conv.unread_count > 0">
|
||||
<span class="o_dollars_conv_badge">
|
||||
<t t-esc="conv.unread_count"/>
|
||||
</span>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
<t t-if="filteredConversations.length === 0">
|
||||
<div class="o_dollars_empty" style="padding: 40px 20px;">
|
||||
<div class="o_dollars_empty_icon">
|
||||
<i class="fa fa-inbox"/>
|
||||
</div>
|
||||
<div class="o_dollars_empty_text">No hay conversaciones</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Center - Chat Area -->
|
||||
<div class="o_dollars_chat_area">
|
||||
<t t-if="state.selectedConversation">
|
||||
<!-- Chat Header -->
|
||||
<div class="o_dollars_chat_header">
|
||||
<div class="o_dollars_chat_avatar">
|
||||
<t t-esc="getInitial(state.selectedConversation.display_name)"/>
|
||||
</div>
|
||||
<div class="o_dollars_chat_info">
|
||||
<div class="o_dollars_chat_name">
|
||||
<t t-esc="state.selectedConversation.display_name || state.selectedConversation.phone_number"/>
|
||||
</div>
|
||||
<div class="o_dollars_chat_status">
|
||||
<span class="o_dollars_status_dot" style="width: 6px; height: 6px;"/>
|
||||
<span><t t-esc="getStatusClass(state.selectedConversation.status)"/></span>
|
||||
<span style="margin-left: 8px; opacity: 0.7;">
|
||||
<t t-esc="state.selectedConversation.phone_number"/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_dollars_chat_actions">
|
||||
<button class="o_dollars_chat_btn" t-on-click="refreshMessages" title="Actualizar">
|
||||
<i class="fa fa-refresh"/>
|
||||
</button>
|
||||
<button class="o_dollars_chat_btn" t-on-click="markAsResolved" title="Marcar como resuelta">
|
||||
<i class="fa fa-check"/>
|
||||
</button>
|
||||
<button class="o_dollars_chat_btn" t-on-click="openInOdoo" title="Abrir en Odoo">
|
||||
<i class="fa fa-external-link"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Messages -->
|
||||
<div class="o_dollars_messages">
|
||||
<t t-foreach="state.messages" t-as="message" t-key="message.id">
|
||||
<div t-attf-class="o_dollars_message {{ message.direction }}">
|
||||
<div class="o_dollars_msg_avatar">
|
||||
<t t-if="message.direction === 'outbound'">
|
||||
<i class="fa fa-user"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-esc="getInitial(state.selectedConversation.display_name)"/>
|
||||
</t>
|
||||
</div>
|
||||
<div class="o_dollars_msg_content">
|
||||
<div class="o_dollars_msg_header">
|
||||
<span class="o_dollars_msg_sender">
|
||||
<t t-if="message.direction === 'outbound'">
|
||||
<t t-esc="message.sent_by_id?.[1] || 'Tú'"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-esc="state.selectedConversation.display_name || 'Cliente'"/>
|
||||
</t>
|
||||
</span>
|
||||
<span class="o_dollars_msg_time">
|
||||
<t t-esc="formatMessageTime(message.create_date)"/>
|
||||
</span>
|
||||
</div>
|
||||
<div class="o_dollars_msg_bubble">
|
||||
<!-- Media -->
|
||||
<t t-if="message.media_url">
|
||||
<t t-if="message.message_type === 'image'">
|
||||
<img t-att-src="message.media_url"
|
||||
class="o_dollars_msg_image"
|
||||
t-on-click="() => window.open(message.media_url, '_blank')"/>
|
||||
</t>
|
||||
<t t-elif="message.message_type === 'audio'">
|
||||
<audio controls="" class="o_dollars_msg_audio">
|
||||
<source t-att-src="message.media_url"/>
|
||||
</audio>
|
||||
</t>
|
||||
<t t-elif="message.message_type === 'video'">
|
||||
<video controls="" class="o_dollars_msg_video">
|
||||
<source t-att-src="message.media_url"/>
|
||||
</video>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<a t-att-href="message.media_url" target="_blank" class="o_dollars_msg_doc">
|
||||
<i class="fa fa-file"/>
|
||||
<span>Documento</span>
|
||||
</a>
|
||||
</t>
|
||||
</t>
|
||||
<!-- Text -->
|
||||
<t t-if="message.content">
|
||||
<t t-esc="message.content"/>
|
||||
</t>
|
||||
</div>
|
||||
<t t-if="message.direction === 'outbound'">
|
||||
<div t-attf-class="o_dollars_msg_status {{ message.status === 'read' ? 'read' : '' }}">
|
||||
<t t-esc="getStatusIcon(message.status)"/>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
<div t-ref="messagesEnd"/>
|
||||
</div>
|
||||
|
||||
<!-- Input Area -->
|
||||
<div class="o_dollars_input_area">
|
||||
<div class="o_dollars_input_wrapper">
|
||||
<textarea t-ref="messageInput"
|
||||
rows="1"
|
||||
placeholder="Escribe un mensaje..."
|
||||
t-att-value="state.newMessage"
|
||||
t-on-input="onMessageInput"
|
||||
t-on-keydown="onKeyDown"
|
||||
t-att-disabled="state.sending"/>
|
||||
</div>
|
||||
<button class="o_dollars_send_btn"
|
||||
t-on-click="sendMessage"
|
||||
t-att-disabled="state.sending || !state.newMessage.trim()">
|
||||
<t t-if="state.sending">
|
||||
<i class="fa fa-spinner fa-spin"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<i class="fa fa-paper-plane"/>
|
||||
</t>
|
||||
</button>
|
||||
</div>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<!-- Empty state -->
|
||||
<div class="o_dollars_empty">
|
||||
<div class="o_dollars_empty_icon">
|
||||
<i class="fa fa-comments"/>
|
||||
</div>
|
||||
<div class="o_dollars_empty_title">WhatsApp Hub</div>
|
||||
<div class="o_dollars_empty_text">
|
||||
Selecciona una conversación para comenzar
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
<!-- Right Panel - Contact Details -->
|
||||
<t t-if="state.selectedConversation">
|
||||
<div class="o_dollars_details">
|
||||
<div class="o_dollars_details_header">
|
||||
<div class="o_dollars_details_avatar">
|
||||
<t t-esc="getInitial(state.selectedConversation.display_name)"/>
|
||||
</div>
|
||||
<div class="o_dollars_details_name">
|
||||
<t t-esc="state.selectedConversation.display_name || 'Sin nombre'"/>
|
||||
</div>
|
||||
<div class="o_dollars_details_phone">
|
||||
<t t-esc="state.selectedConversation.phone_number"/>
|
||||
</div>
|
||||
<div class="o_dollars_details_status">
|
||||
<span class="o_dollars_status_dot"/>
|
||||
<span><t t-esc="getStatusClass(state.selectedConversation.status)"/></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="o_dollars_details_content">
|
||||
<div class="o_dollars_details_section">
|
||||
<div class="o_dollars_details_section_title">Acciones</div>
|
||||
<div class="o_dollars_details_item" t-on-click="markAsResolved">
|
||||
<i class="fa fa-check-circle"/>
|
||||
<span>Marcar como resuelta</span>
|
||||
</div>
|
||||
<div class="o_dollars_details_item" t-on-click="openInOdoo">
|
||||
<i class="fa fa-external-link"/>
|
||||
<span>Ver en Odoo</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="o_dollars_details_section">
|
||||
<div class="o_dollars_details_section_title">Información</div>
|
||||
<div class="o_dollars_details_item">
|
||||
<i class="fa fa-phone"/>
|
||||
<span><t t-esc="state.selectedConversation.phone_number"/></span>
|
||||
</div>
|
||||
<div class="o_dollars_details_item">
|
||||
<i class="fa fa-comment"/>
|
||||
<span><t t-esc="state.messages.length"/> mensajes</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
Reference in New Issue
Block a user