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