feat(odoo): add OWL chat widget

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Claude AI
2026-01-29 22:43:14 +00:00
parent c8c6deb4de
commit cf424b1f37
2 changed files with 177 additions and 0 deletions

View File

@@ -0,0 +1,120 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { useService } from "@web/core/utils/hooks";
import { Component, useState, onWillStart, onMounted } from "@odoo/owl";
export class WhatsAppChatWidget extends Component {
static template = "odoo_whatsapp_hub.ChatWidget";
static props = {
conversationId: { type: Number, optional: true },
partnerId: { type: Number, optional: true },
};
setup() {
this.orm = useService("orm");
this.state = useState({
conversation: null,
messages: [],
newMessage: "",
loading: true,
});
onWillStart(async () => {
await this.loadConversation();
});
onMounted(() => {
this.scrollToBottom();
});
}
async loadConversation() {
this.state.loading = true;
try {
if (this.props.conversationId) {
const conversations = await this.orm.searchRead(
"whatsapp.conversation",
[["id", "=", this.props.conversationId]],
["id", "display_name", "phone_number", "status"]
);
if (conversations.length) {
this.state.conversation = conversations[0];
await this.loadMessages();
}
}
} finally {
this.state.loading = false;
}
}
async loadMessages() {
if (!this.state.conversation) return;
const messages = await this.orm.searchRead(
"whatsapp.message",
[["conversation_id", "=", this.state.conversation.id]],
["id", "direction", "content", "message_type", "status", "create_date"],
{ order: "create_date asc" }
);
this.state.messages = messages;
const unreadIds = messages
.filter(m => m.direction === "inbound" && !m.is_read)
.map(m => m.id);
if (unreadIds.length) {
await this.orm.write("whatsapp.message", unreadIds, { is_read: true });
}
this.scrollToBottom();
}
async sendMessage() {
if (!this.state.newMessage.trim() || !this.state.conversation) return;
const content = this.state.newMessage;
this.state.newMessage = "";
await this.orm.call(
"whatsapp.message",
"send_message",
[this.state.conversation.id, content]
);
await this.loadMessages();
}
scrollToBottom() {
const container = document.querySelector(".o_whatsapp_messages");
if (container) {
container.scrollTop = container.scrollHeight;
}
}
formatTime(dateStr) {
if (!dateStr) return "";
const date = new Date(dateStr);
return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
}
getStatusIcon(status) {
const icons = {
pending: "fa-clock-o",
sent: "fa-check",
delivered: "fa-check-double text-muted",
read: "fa-check-double text-primary",
failed: "fa-exclamation-circle text-danger",
};
return icons[status] || "";
}
onKeyPress(ev) {
if (ev.key === "Enter" && !ev.shiftKey) {
ev.preventDefault();
this.sendMessage();
}
}
}
registry.category("public_components").add("WhatsAppChatWidget", WhatsAppChatWidget);

View File

@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="odoo_whatsapp_hub.ChatWidget">
<div class="o_whatsapp_chat_container">
<t t-if="state.loading">
<div class="d-flex justify-content-center align-items-center h-100">
<i class="fa fa-spinner fa-spin fa-2x"/>
</div>
</t>
<t t-elif="state.conversation">
<!-- Header -->
<div class="o_whatsapp_chat_header">
<div class="avatar">
<t t-esc="state.conversation.display_name[0]"/>
</div>
<div class="contact-info">
<div class="contact-name" t-esc="state.conversation.display_name"/>
<div class="contact-status" t-esc="state.conversation.phone_number"/>
</div>
</div>
<!-- Messages -->
<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-meta">
<span t-esc="formatTime(message.create_date)"/>
<t t-if="message.direction === 'outbound'">
<i t-attf-class="fa #{getStatusIcon(message.status)} message-status"/>
</t>
</div>
</div>
</t>
</div>
<!-- Input -->
<div class="o_whatsapp_input_area">
<input
type="text"
placeholder="Escribe un mensaje..."
t-model="state.newMessage"
t-on-keypress="onKeyPress"
/>
<button t-on-click="sendMessage">
<i class="fa fa-paper-plane"/>
</button>
</div>
</t>
<t t-else="">
<div class="d-flex justify-content-center align-items-center h-100 text-muted">
<span>Selecciona una conversación</span>
</div>
</t>
</div>
</t>
</templates>