feat(odoo): add OWL chat widget
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
120
odoo_whatsapp_hub/static/src/js/chat_widget.js
Normal file
120
odoo_whatsapp_hub/static/src/js/chat_widget.js
Normal 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);
|
||||
57
odoo_whatsapp_hub/static/src/xml/chat_widget.xml
Normal file
57
odoo_whatsapp_hub/static/src/xml/chat_widget.xml
Normal 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>
|
||||
Reference in New Issue
Block a user