(function () {
"use strict";
// ---------------------------------------------------------------------------
// State
// ---------------------------------------------------------------------------
const state = {
zohoContext: null,
linkedContactId: null,
linkedDealId: null,
currentPayload: null,
};
// ---------------------------------------------------------------------------
// Config
// ---------------------------------------------------------------------------
const cfg = () => window.EASYBUSY_CHATS_CONFIG || {};
// ---------------------------------------------------------------------------
// Status bar
// ---------------------------------------------------------------------------
const statusBar = document.getElementById("status-bar");
function showStatus(message, type = "info") {
if (!statusBar) return;
statusBar.textContent = message;
statusBar.className = "visible " + type;
clearTimeout(statusBar._t);
if (type === "success" || type === "info" || type === "warning") {
statusBar._t = setTimeout(() => { statusBar.className = ""; }, 4500);
}
}
// ---------------------------------------------------------------------------
// DOM helpers
// ---------------------------------------------------------------------------
const $ = (id) => document.getElementById(id);
function openModal(el) {
if (!el) return;
el.classList.add("visible");
el.setAttribute("aria-hidden", "false");
}
function closeModal(el) {
if (!el) return;
el.classList.remove("visible");
el.setAttribute("aria-hidden", "true");
}
function setButtonLoading(button, loading, textWhenLoading) {
if (!button) return;
if (!button.dataset.originalText) button.dataset.originalText = button.textContent;
button.disabled = !!loading;
button.textContent = loading ? textWhenLoading : button.dataset.originalText;
}
function escapeHtml(v) {
return String(v || "")
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
// ---------------------------------------------------------------------------
// Payload normalization
// ---------------------------------------------------------------------------
function normalizeChatPayload(raw) {
if (!raw || typeof raw !== "object") return null;
return {
provider: raw.provider || "",
accountId: raw.account_id || raw.accountId || "",
chatId: raw.chat_id || raw.chatId || raw.id || "",
chatTitle: raw.chat_title || raw.chatTitle || raw.title || "",
externalContactId: raw.contact_id || raw.externalContactId || "",
contactUserName: raw.contact_user_name || raw.contactUserName || raw.userName || "",
name: raw.name || raw.contact_user_name || raw.contactUserName || raw.userName || "",
phone: raw.phone || raw.phoneNumber || "",
email: raw.email || raw.emailAddress || "",
raw,
};
}
function splitName(fullName) {
const parts = String(fullName || "").trim().split(/\s+/).filter(Boolean);
if (!parts.length) return { firstName: "", lastName: "" };
if (parts.length === 1) return { firstName: "", lastName: parts[0] };
return {
firstName: parts.slice(0, -1).join(" "),
lastName: parts[parts.length - 1],
};
}
function getChatLabel(payload) {
if (!payload) return "EasyBusy Chat";
return payload.chatTitle || payload.name || payload.contactUserName || payload.phone || payload.email || "EasyBusy Chat";
}
function getEasyBusyChatUrl(payload) {
const base = (cfg().easyBusyChatBaseUrl || cfg().websiteUrl || "").replace(/\/$/, "");
if (!base || !payload?.chatId) return "";
return `${base}/chat/${encodeURIComponent(payload.chatId)}`;
}
// ---------------------------------------------------------------------------
// LocalStorage link adapter
// Phase 1: localStorage. Phase 2: replace with EasyBusy backend / Zoho custom module.
// ---------------------------------------------------------------------------
const LinkStorage = {
_key(payload) {
const p = payload || state.currentPayload;
if (!p) return null;
const identity = [
p.provider || "provider",
p.accountId || "account",
p.chatId || "chat",
p.externalContactId || p.email || p.phone || "contact",
].join("::");
return `easybusy_zoho_link::${identity}`;
},
async getLink(payload) {
const key = this._key(payload);
if (!key) return null;
try { return JSON.parse(localStorage.getItem(key) || "null"); }
catch { return null; }
},
async saveLink(payload, patch) {
const key = this._key(payload);
if (!key) return;
const oldValue = await this.getLink(payload);
const nextValue = {
...(oldValue || {}),
...(patch || {}),
payload,
savedAt: new Date().toISOString(),
};
localStorage.setItem(key, JSON.stringify(nextValue));
},
async removeLink(payload) {
const key = this._key(payload);
if (key) localStorage.removeItem(key);
},
};
// ---------------------------------------------------------------------------
// Zoho CRM helpers
// ---------------------------------------------------------------------------
function ensureZohoSdk() {
if (typeof ZOHO === "undefined" || !ZOHO.CRM || !ZOHO.CRM.API) {
throw new Error("Zoho CRM SDK is not available. Open this widget inside Zoho CRM.");
}
}
function coqlEscape(v) {
return String(v || "").replace(/\\/g, "\\\\").replace(/'/g, "\\'");
}
async function coqlQuery(query) {
ensureZohoSdk();
const res = await ZOHO.CRM.API.coql({ select_query: query });
return res?.data || [];
}
function normalizeInsertResponse(res, fallbackEntity) {
const row = res?.data?.[0];
if (!row || row.code !== "SUCCESS") {
throw new Error(row?.message || `${fallbackEntity || "Record"} create failed`);
}
return String(row.details?.id || "");
}
async function openZohoRecord(entity, recordId) {
if (!recordId) return;
if (ZOHO.CRM.UI?.Record?.open) {
await ZOHO.CRM.UI.Record.open({ Entity: entity, RecordID: recordId });
return;
}
showStatus(`${entity} ID: ${recordId}`, "info");
}
// ---------------------------------------------------------------------------
// Contact operations
// ---------------------------------------------------------------------------
async function findZohoContactByChatPayload(payload) {
if (!payload) return null;
if (payload.email) {
const rows = await coqlQuery(
`select id, Full_Name, First_Name, Last_Name, Email, Phone, Mobile from Contacts where Email = '${coqlEscape(payload.email)}' limit 1`
);
if (rows.length) return rows[0];
}
if (payload.phone) {
const esc = coqlEscape(payload.phone);
const rows = await coqlQuery(
`select id, Full_Name, First_Name, Last_Name, Email, Phone, Mobile from Contacts where Phone = '${esc}' or Mobile = '${esc}' limit 1`
);
if (rows.length) return rows[0];
}
return null;
}
async function searchZohoContacts(searchText) {
const q = String(searchText || "").trim();
if (!q) {
return coqlQuery(
"select id, Full_Name, First_Name, Last_Name, Email, Phone, Mobile from Contacts where Last_Name is not null order by Modified_Time desc limit 20"
);
}
const esc = coqlEscape(q);
return coqlQuery(
`select id, Full_Name, First_Name, Last_Name, Email, Phone, Mobile from Contacts where Full_Name like '%${esc}%' or Email like '%${esc}%' or Phone like '%${esc}%' or Mobile like '%${esc}%' limit 20`
);
}
async function createZohoContact(data, payload) {
ensureZohoSdk();
const apiData = {
First_Name: data.firstName || undefined,
Last_Name: data.lastName || "EasyBusy Contact",
Phone: data.phone || undefined,
Email: data.email || undefined,
Description: buildZohoDescription("EasyBusy chat linked", payload),
};
Object.keys(apiData).forEach((k) => apiData[k] === undefined && delete apiData[k]);
const res = await ZOHO.CRM.API.insertRecord({
Entity: "Contacts",
APIData: apiData,
Trigger: [],
});
return normalizeInsertResponse(res, "Contact");
}
// ---------------------------------------------------------------------------
// Deal operations
// ---------------------------------------------------------------------------
async function findDuplicateDeals(contactId) {
if (!contactId) return [];
return coqlQuery(
`select id, Deal_Name, Stage from Deals where Contact_Name = '${coqlEscape(contactId)}' limit 10`
);
}
async function createZohoDeal(data, payload, contactId) {
ensureZohoSdk();
const { defaultDealPipeline } = cfg();
const apiData = {
Deal_Name: data.dealName,
Stage: data.stage,
Amount: data.amount || undefined,
Closing_Date: data.closingDate || undefined,
Description: buildZohoDescription("EasyBusy chat linked", payload),
};
if (contactId) apiData.Contact_Name = { id: contactId };
if (defaultDealPipeline) apiData.Pipeline = defaultDealPipeline;
Object.keys(apiData).forEach((k) => apiData[k] === undefined && delete apiData[k]);
const res = await ZOHO.CRM.API.insertRecord({
Entity: "Deals",
APIData: apiData,
Trigger: [],
});
return normalizeInsertResponse(res, "Deal");
}
function buildZohoDescription(title, payload) {
const lines = [
title,
"Source: EasyBusy Chats",
];
if (payload?.provider) lines.push(`Channel: ${payload.provider}`);
if (payload?.chatId) lines.push(`Chat ID: ${payload.chatId}`);
if (payload?.externalContactId) lines.push(`External Contact ID: ${payload.externalContactId}`);
if (payload?.chatTitle) lines.push(`Chat Title: ${payload.chatTitle}`);
if (payload?.name) lines.push(`Customer Name: ${payload.name}`);
const chatUrl = getEasyBusyChatUrl(payload);
if (chatUrl) lines.push(`Chat URL: ${chatUrl}`);
return lines.join("\n");
}
// ---------------------------------------------------------------------------
// Create Contact modal
// ---------------------------------------------------------------------------
const createContactModal = $("create-contact-modal");
const ccFirstName = $("create-contact-first-name");
const ccLastName = $("create-contact-last-name");
const ccPhone = $("create-contact-phone");
const ccEmail = $("create-contact-email");
const btnCreateContactSave = $("btn-create-contact-save");
const btnCreateContactCancel = $("btn-create-contact-cancel");
let createContactResolve = null;
let createContactReject = null;
function openCreateContactModal(payload) {
return new Promise((resolve, reject) => {
createContactResolve = resolve;
createContactReject = reject;
const name = splitName(payload?.name || payload?.contactUserName || payload?.chatTitle || "");
if (ccFirstName) ccFirstName.value = name.firstName;
if (ccLastName) ccLastName.value = name.lastName || payload?.phone || "EasyBusy Contact";
if (ccPhone) ccPhone.value = payload?.phone || "";
if (ccEmail) ccEmail.value = payload?.email || "";
openModal(createContactModal);
setTimeout(() => ccLastName?.focus(), 50);
});
}
function closeCreateContactModal() {
closeModal(createContactModal);
}
btnCreateContactSave?.addEventListener("click", () => {
const data = {
firstName: (ccFirstName?.value || "").trim(),
lastName: (ccLastName?.value || "").trim(),
phone: (ccPhone?.value || "").trim(),
email: (ccEmail?.value || "").trim(),
};
if (!data.lastName) {
showStatus("Last Name is required for Zoho Contact.", "warning");
ccLastName?.focus();
return;
}
closeCreateContactModal();
createContactResolve?.(data);
});
btnCreateContactCancel?.addEventListener("click", () => {
closeCreateContactModal();
createContactReject?.(new Error("cancelled"));
});
createContactModal?.addEventListener("keydown", (e) => {
if (e.key === "Escape") btnCreateContactCancel?.click();
});
// ---------------------------------------------------------------------------
// Link Contact modal
// ---------------------------------------------------------------------------
const linkContactModal = $("link-contact-modal");
const linkContactSearch = $("link-contact-search");
const linkContactResults = $("link-contact-results");
const btnLinkContactSearch = $("btn-link-contact-search");
const btnLinkContactCancel = $("btn-link-contact-cancel");
let linkContactResolve = null;
let linkContactReject = null;
function openLinkContactModal(payload) {
return new Promise((resolve, reject) => {
linkContactResolve = resolve;
linkContactReject = reject;
const initial = payload?.name || payload?.contactUserName || payload?.phone || payload?.email || "";
if (linkContactSearch) linkContactSearch.value = initial;
renderContactResults([], "Enter search text and click Search.");
openModal(linkContactModal);
setTimeout(() => linkContactSearch?.focus(), 50);
});
}
function closeLinkContactModal() {
closeModal(linkContactModal);
}
function renderContactResults(rows, emptyText) {
if (!linkContactResults) return;
if (!rows || !rows.length) {
linkContactResults.innerHTML = `
${escapeHtml(emptyText || "No contacts found.")}
`;
return;
}
linkContactResults.innerHTML = rows.map((c) => {
const name = c.Full_Name || [c.First_Name, c.Last_Name].filter(Boolean).join(" ") || c.id;
const meta = [c.Email, c.Phone, c.Mobile].filter(Boolean).join(" · ");
return `
`;
}).join("");
}
async function runContactSearch() {
try {
setButtonLoading(btnLinkContactSearch, true, "Searching...");
renderContactResults([], "Searching...");
const rows = await searchZohoContacts(linkContactSearch?.value || "");
renderContactResults(rows, "No contacts found.");
} catch (err) {
console.error("EasyBusy Zoho: contact search failed", err);
renderContactResults([], "Search failed: " + (err.message || err));
} finally {
setButtonLoading(btnLinkContactSearch, false);
}
}
btnLinkContactSearch?.addEventListener("click", runContactSearch);
linkContactSearch?.addEventListener("keydown", (e) => {
if (e.key === "Enter") runContactSearch();
if (e.key === "Escape") btnLinkContactCancel?.click();
});
linkContactResults?.addEventListener("click", (e) => {
const item = e.target.closest(".contact-result-item");
if (!item) return;
const contact = {
id: item.dataset.contactId,
name: item.dataset.contactName,
};
closeLinkContactModal();
linkContactResolve?.(contact);
});
btnLinkContactCancel?.addEventListener("click", () => {
closeLinkContactModal();
linkContactReject?.(new Error("cancelled"));
});
// ---------------------------------------------------------------------------
// Create Deal modal
// ---------------------------------------------------------------------------
const createDealModal = $("create-deal-modal");
const cdName = $("create-deal-name");
const cdStage = $("create-deal-stage");
const cdAmount = $("create-deal-amount");
const cdClosingDate = $("create-deal-closing-date");
const btnCreateDealSave = $("btn-create-deal-save");
const btnCreateDealCancel = $("btn-create-deal-cancel");
let createDealResolve = null;
let createDealReject = null;
function openCreateDealModal(payload) {
return new Promise((resolve, reject) => {
createDealResolve = resolve;
createDealReject = reject;
const defaultName = getChatLabel(payload);
const defaultStage = cfg().defaultDealStage || "Qualification";
if (cdName) cdName.value = defaultName;
if (cdStage) cdStage.value = defaultStage;
if (cdAmount) cdAmount.value = "";
if (cdClosingDate) cdClosingDate.value = getDefaultClosingDate();
openModal(createDealModal);
setTimeout(() => cdName?.focus(), 50);
});
}
function closeCreateDealModal() {
closeModal(createDealModal);
}
function getDefaultClosingDate() {
const d = new Date();
d.setDate(d.getDate() + 14);
return d.toISOString().slice(0, 10);
}
btnCreateDealSave?.addEventListener("click", () => {
const data = {
dealName: (cdName?.value || "").trim(),
stage: (cdStage?.value || "").trim(),
amount: (cdAmount?.value || "").trim(),
closingDate: (cdClosingDate?.value || "").trim(),
};
if (!data.dealName) {
showStatus("Deal Name is required for Zoho Deal.", "warning");
cdName?.focus();
return;
}
if (!data.stage) {
showStatus("Stage is required for Zoho Deal.", "warning");
cdStage?.focus();
return;
}
closeCreateDealModal();
createDealResolve?.(data);
});
btnCreateDealCancel?.addEventListener("click", () => {
closeCreateDealModal();
createDealReject?.(new Error("cancelled"));
});
createDealModal?.addEventListener("keydown", (e) => {
if (e.key === "Escape") btnCreateDealCancel?.click();
});
// ---------------------------------------------------------------------------
// EasyBusy actions
// ---------------------------------------------------------------------------
async function onOpenChat(rawPayload) {
const payload = normalizeChatPayload(rawPayload);
state.currentPayload = payload;
const link = await LinkStorage.getLink(payload);
state.linkedContactId = link?.zohoContactId || null;
state.linkedDealId = link?.zohoDealId || null;
return buildChatActions(payload);
}
function buildChatActions(payload) {
return [
{
id: "create_contact",
label: "Create Contact",
description: "Create Zoho Contact and link this chat",
callback: () => action_create_contact(payload),
},
{
id: "link_contact",
label: "Link Existing Contact",
description: "Search Zoho Contacts and link selected contact",
callback: () => action_link_contact(payload),
},
{
id: "view_contact",
label: "View Contact",
description: "Open the linked Zoho Contact",
callback: () => action_view_contact(),
},
{
id: "create_deal",
label: "Create Deal",
description: "Create Zoho Deal and link this chat",
callback: () => action_create_deal(payload),
},
];
}
async function action_create_contact(payload) {
try {
const existing = await findZohoContactByChatPayload(payload);
if (existing) {
state.linkedContactId = String(existing.id);
await LinkStorage.saveLink(payload, {
zohoContactId: state.linkedContactId,
zohoContactName: existing.Full_Name || existing.id,
});
showStatus("Linked to existing contact: " + (existing.Full_Name || existing.id), "warning");
return;
}
const formData = await openCreateContactModal(payload);
showStatus("Creating Zoho Contact...", "info");
const contactId = await createZohoContact(formData, payload);
state.linkedContactId = contactId;
await LinkStorage.saveLink(payload, {
zohoContactId: contactId,
zohoContactName: [formData.firstName, formData.lastName].filter(Boolean).join(" "),
});
showStatus("Contact created and linked.", "success");
} catch (err) {
if (err.message === "cancelled") throw err;
console.error("EasyBusy Zoho: create_contact failed", err);
showStatus("Failed to create contact: " + (err.message || err), "error");
throw err;
}
}
async function action_link_contact(payload) {
try {
const contact = await openLinkContactModal(payload);
state.linkedContactId = String(contact.id);
await LinkStorage.saveLink(payload, {
zohoContactId: state.linkedContactId,
zohoContactName: contact.name || contact.id,
});
showStatus("Contact linked: " + (contact.name || contact.id), "success");
} catch (err) {
if (err.message !== "cancelled") {
console.error("EasyBusy Zoho: link_contact failed", err);
showStatus("Link failed: " + (err.message || err), "error");
}
throw err;
}
}
async function action_view_contact() {
const contactId = state.linkedContactId;
if (!contactId) {
showStatus("No linked contact. Use Create or Link first.", "warning");
throw new Error("no_contact");
}
try {
await openZohoRecord("Contacts", contactId);
} catch (err) {
console.error("EasyBusy Zoho: view_contact failed", err);
showStatus("Could not open contact: " + (err.message || err), "error");
throw err;
}
}
async function action_create_deal(payload) {
try {
if (state.linkedContactId) {
const dupes = await findDuplicateDeals(state.linkedContactId);
if (dupes.length) {
showStatus(dupes.length + " existing deal(s) found for linked contact.", "warning");
}
}
const formData = await openCreateDealModal(payload);
showStatus("Creating Zoho Deal...", "info");
const dealId = await createZohoDeal(formData, payload, state.linkedContactId);
state.linkedDealId = dealId;
await LinkStorage.saveLink(payload, {
zohoDealId: dealId,
zohoDealName: formData.dealName,
});
showStatus("Deal created and linked.", "success");
} catch (err) {
if (err.message === "cancelled") throw err;
console.error("EasyBusy Zoho: create_deal failed", err);
showStatus("Failed to create deal: " + (err.message || err), "error");
throw err;
}
}
// ---------------------------------------------------------------------------
// EasyBusy flow config helpers
// ---------------------------------------------------------------------------
function ebCfg() {
const c = cfg();
const extentionNamespace = c.extentionNamespace || "easybusy";
const connectorName = c.connectorName || "easybusychatsconnector";
return {
extentionNamespace,
connectorName,
orgVarCompanyId: c.orgVarCompanyId || "UNDEFINED_EASYBUSY_COMPANY_ID",
setOrgVarFunction: c.setOrgVarFunction || "easybusy_set_org_variable",
tokenStorageKey: c.tokenStorageKey || "EasyBusyToken",
api: c.api || {},
};
}
function hasConnectorSdk() {
return typeof ZOHO !== "undefined" && ZOHO.CRM &&
(ZOHO.CRM.CONNECTOR && ZOHO.CRM.CONNECTOR.invokeAPI);
}
// ---------------------------------------------------------------------------
// Zoho org / user context
// ---------------------------------------------------------------------------
async function getZohoOrgInfo() {
try {
const res = await ZOHO.CRM.CONFIG.getOrgInfo();
return (res && res.org && res.org[0]) || {};
} catch (e) {
return {};
}
}
async function getZohoCurrentUser() {
try {
const res = await ZOHO.CRM.CONFIG.getCurrentUser();
return (res && res.users && res.users[0]) || {};
} catch (e) {
return {};
}
}
// ---------------------------------------------------------------------------
// Connector authorization + invocation
// ---------------------------------------------------------------------------
async function authorizeConnector() {
const { extentionNamespace, connectorName } = ebCfg();
const authTarget = (extentionNamespace + "." + connectorName).toLowerCase();
if (ZOHO.CRM.CONNECTOR && ZOHO.CRM.CONNECTOR.authorize) {
showStatus("Authorizing EasyBusy connector...", "info");
const ok = await ZOHO.CRM.CONNECTOR.authorize(authTarget);
if (!ok) throw new Error("Connector authorization was not completed.");
return true;
}
throw new Error(
"Connector authorization is unavailable. An admin must connect EasyBusy in Setup → Connectors."
);
}
function isUnauthorizedResponse(res) {
if (!res) return false;
const blob = JSON.stringify(res).toLowerCase();
if (blob.includes("no api available with this criteria")) return false;
return (
blob.includes("authorization exception") ||
blob.includes("not authori") ||
blob.includes("unauthori") ||
blob.includes("no_connection") ||
blob.includes("invalid_token") ||
blob.includes("invalid_oauth") ||
blob.includes("token expired") ||
/"(http_?status_?code|statuscode|status_code)"\s*:\s*"?401/.test(blob)
);
}
function parseConnectorBody(res) {
if (res == null) return null;
const candidates = [
res?.details?.statusMessage,
res?.details?.response,
res?.details?.body,
res?.response,
res?.body,
res?.data,
res,
];
for (const cand of candidates) {
if (cand == null) continue;
if (typeof cand === "object") return cand;
if (typeof cand === "string") {
const s = cand.trim();
if (s.startsWith("{") || s.startsWith("[")) {
try { return JSON.parse(s); } catch { /* keep looking */ }
}
}
}
return res;
}
// All connector API calls route through invokeAPI using the format:
// auth: (extentionNamespace.connectorName).toLowerCase()
// method: (extentionNamespace.connectorName.apimethod).toLowerCase()
async function connectorInvoke(reqData, opts) {
ensureZohoSdk();
const options = opts || {};
const { extentionNamespace, connectorName } = ebCfg();
const doInvoke = async () => {
if (ZOHO.CRM.CONNECTOR && ZOHO.CRM.CONNECTOR.invokeAPI) {
const ns = reqData.apiname
? (extentionNamespace + "." + connectorName + "." + reqData.apiname).toLowerCase()
: (extentionNamespace + "." + connectorName).toLowerCase();
const data = reqData.apiname ? (reqData.parameters || {}) : reqData;
return ZOHO.CRM.CONNECTOR.invokeAPI(ns, data);
}
throw new Error("ZOHO.CRM.CONNECTOR.invokeAPI is unavailable.");
};
let res;
try {
res = await doInvoke();
} catch (e) {
const msg = String(e && e.message || e).toLowerCase();
if (msg.includes("parentwindow")) {
// SDK couldn't reach the CRM parent frame — not running inside Zoho CRM.
state.insideCrm = false;
throw e;
}
if (options.autoAuthorize !== false && isUnauthorizedResponse({ error: e })) {
await authorizeConnector();
res = await doInvoke();
} else {
throw e;
}
}
if (options.autoAuthorize !== false && isUnauthorizedResponse(res)) {
await authorizeConnector();
res = await doInvoke();
}
const body = parseConnectorBody(res);
if (body && typeof body === "object") {
const code = String(body.code || body.status_code || "");
if (/^[45]\d\d$/.test(code)) {
const msg =
body.message ||
body.error ||
body.response ||
body.statusMessage ||
"unknown error";
throw new Error(`Connector API error ${code}: ${msg}`);
}
}
return body;
}
// ---------------------------------------------------------------------------
// Org variable (company id) persistence
// ---------------------------------------------------------------------------
async function readOrgVar(apiName) {
try {
const res = await ZOHO.CRM.API.getOrgVariable(apiName);
const v = res && res.Success && res.Success.Content;
return v ? String(v) : "";
} catch (e) {
return "";
}
}
async function writeOrgVar(apiName, value) {
const { setOrgVarFunction } = ebCfg();
if (!(ZOHO.CRM.FUNCTIONS && ZOHO.CRM.FUNCTIONS.execute)) {
throw new Error("Cannot persist company id — FUNCTIONS.execute is unavailable.");
}
const res = await ZOHO.CRM.FUNCTIONS.execute(setOrgVarFunction, {
arguments: JSON.stringify({ variable: apiName, value: String(value) }),
});
if (res && res.code && res.code !== "SUCCESS") {
throw new Error(res.message || JSON.stringify(res));
}
return res;
}
// ---------------------------------------------------------------------------
// console-api calls (through the connector)
// ---------------------------------------------------------------------------
function normalizeCompanies(body) {
const list = Array.isArray(body) ? body : (body && (body.companies || body.data || body.items)) || [];
return list
.map((c) => ({
id: c.id || c.companyId || c.Id || c.company_id || "",
name: c.name || c.companyName || c.title || c.Name || c.id || "Company",
}))
.filter((c) => c.id);
}
async function fetchCompanies() {
const body = await connectorInvoke({ apiname: "GetCompanies" });
return normalizeCompanies(body);
}
async function createCompany() {
const org = await getZohoOrgInfo();
const orgName = org.company_name || org.name || "Zoho Company";
return connectorInvoke({
apiname: "CreateCompany",
parameters: { key: orgName },
});
}
async function fetchUserToken() {
const user = await getZohoCurrentUser();
const body = await connectorInvoke({
apiname: "GetUserToken",
parameters: {
zohoUserId: user.id || "",
email: user.email || "",
},
});
return {
token: (body && body.token) || "",
status: (body && body.status) || "ok",
message: (body && body.message) || "",
};
}
// ---------------------------------------------------------------------------
// Onboarding overlay UI
// ---------------------------------------------------------------------------
const onb = {
root: $("eb-onboarding"),
title: $("eb-onb-title"),
message: $("eb-onb-message"),
companies: $("eb-onb-companies"),
primary: $("eb-onb-primary"),
secondary: $("eb-onb-secondary"),
};
function showOnboarding() {
if (!onb.root) return;
onb.root.classList.add("visible");
onb.root.setAttribute("aria-hidden", "false");
}
function hideOnboarding() {
if (!onb.root) return;
onb.root.classList.remove("visible");
onb.root.setAttribute("aria-hidden", "true");
}
function setButton(btn, label, onClick) {
if (!btn) return;
if (label) {
btn.style.display = "";
btn.disabled = false;
btn.textContent = label;
btn.onclick = onClick || null;
} else {
btn.style.display = "none";
btn.disabled = false;
btn.onclick = null;
}
}
function renderOnboarding(opts) {
if (onb.title) onb.title.textContent = opts.title || "EasyBusy";
if (onb.message) onb.message.textContent = opts.message || "";
if (onb.companies) {
onb.companies.innerHTML = "";
(opts.companies || []).forEach((c) => {
const item = document.createElement("div");
item.className = "eb-company-item";
item.innerHTML =
`${escapeHtml(c.name)}
` ;
// `${escapeHtml(c.id)}
`;
item.addEventListener("click", () => opts.onSelect && opts.onSelect(c));
onb.companies.appendChild(item);
});
}
setButton(onb.primary, opts.primaryLabel, opts.onPrimary);
setButton(onb.secondary, opts.secondaryLabel, opts.onSecondary);
showOnboarding();
}
// ---------------------------------------------------------------------------
// Flow steps
// ---------------------------------------------------------------------------
async function ensureCompanyId() {
const { orgVarCompanyId } = ebCfg();
const existing = await readOrgVar(orgVarCompanyId);
if (existing) return existing;
showStatus("Loading EasyBusy companies...", "info");
let companies = await fetchCompanies();
if (!companies.length) {
await new Promise((resolve, reject) => {
renderOnboarding({
title: "No company yet",
message: "This EasyBusy account has no company. Create one to continue.",
primaryLabel: "Create company",
onPrimary: async () => {
try {
onb.primary.disabled = true;
onb.primary.textContent = "Creating...";
await createCompany();
companies = await fetchCompanies();
resolve();
} catch (e) {
onb.primary.disabled = false;
onb.primary.textContent = "Create company";
showStatus("Create company failed: " + (e.message || e), "error");
reject(e);
}
},
});
});
}
if (!companies.length) {
throw new Error("Could not load or create an EasyBusy company.");
}
const chosen = await new Promise((resolve) => {
renderOnboarding({
title: "Select a company",
message: "Choose the EasyBusy company for this organization. " +
"This applies to all CRM users.",
companies,
onSelect: (c) => resolve(c),
secondaryLabel: "Refresh list",
onSecondary: async () => {
try {
const fresh = await fetchCompanies();
renderOnboarding({
title: "Select a company",
message: "Choose the EasyBusy company for this organization.",
companies: fresh,
onSelect: (c) => resolve(c),
});
} catch (e) {
showStatus("Refresh failed: " + (e.message || e), "error");
}
},
});
});
showStatus("Saving company selection...", "info");
try {
await writeOrgVar(orgVarCompanyId, chosen.id);
} catch (e) {
console.warn("EasyBusy Zoho: failed to persist company id", e);
showStatus("Company saved for this session only: " + (e.message || e), "warning");
}
return chosen.id;
}
async function ensureUserToken() {
const { tokenStorageKey } = ebCfg();
let token = "";
try { token = localStorage.getItem(tokenStorageKey) || ""; } catch {}
if (token) return token;
showStatus("Requesting EasyBusy access...", "info");
const res = await fetchUserToken();
if (res.status === "waitForAcceptingRequest" || !res.token) {
renderOnboarding({
title: "Access pending",
message: res.message || "Your access request is awaiting approval.",
primaryLabel: "Check again",
onPrimary: () => startEasyBusyFlow(),
});
return null;
}
try { localStorage.setItem(tokenStorageKey, res.token); } catch {}
return res.token;
}
// ---------------------------------------------------------------------------
// EasyBusy plugin loader
// ---------------------------------------------------------------------------
function ensurePluginLoaded() {
return new Promise((resolve, reject) => {
const pluginUrl = cfg().pluginLoaderUrl || "";
if (window.renderChatsApp) return resolve();
if (!pluginUrl) {
return reject(new Error("pluginLoaderUrl is not configured."));
}
const script = document.createElement("script");
script.src = pluginUrl;
script.onload = () => resolve();
script.onerror = () => reject(new Error("Failed to load EasyBusy plugin from: " + pluginUrl));
document.head.appendChild(script);
});
}
function renderChats(companyId, accessToken) {
const { defaultLang, logoUrl, websiteUrl, fontFamily, themeColors } = cfg();
try { localStorage.setItem("token", accessToken); } catch {}
const interval = setInterval(() => {
if (!window.renderChatsApp) return;
clearInterval(interval);
window.renderChatsApp("chat_container_holder", {
companyId: companyId || undefined,
accessToken: accessToken || undefined,
lang: defaultLang || "uk-UA",
callbacks: {
onOpenChat,
},
disableBrowserNotifications: true,
theme: {
colors: themeColors || {},
fontFamily: fontFamily || "Inter",
},
});
showStatus("EasyBusy Chats loaded.", "success");
}, 100);
}
// ---------------------------------------------------------------------------
// Orchestrator: connector auth → company → token → chats
// ---------------------------------------------------------------------------
async function startEasyBusyFlow() {
try {
if (!hasConnectorSdk() || !state.insideCrm) {
const { standaloneCompanyId, standaloneAccessToken } = cfg();
hideOnboarding();
showStatus("Standalone preview — open inside Zoho CRM to use the connector.", "warning");
await ensurePluginLoaded();
renderChats(standaloneCompanyId || undefined, standaloneAccessToken || undefined);
return;
}
const companyId = await ensureCompanyId();
const token = await ensureUserToken();
if (!token) return;
hideOnboarding();
await ensurePluginLoaded();
renderChats(companyId, token);
} catch (err) {
const msg = String(err && err.message || err).toLowerCase();
if (msg.includes("parentwindow")) {
const { standaloneCompanyId, standaloneAccessToken } = cfg();
hideOnboarding();
showStatus("Standalone preview — open inside Zoho CRM to use the connector.", "warning");
await ensurePluginLoaded();
renderChats(standaloneCompanyId || undefined, standaloneAccessToken || undefined);
return;
}
console.error("EasyBusy Zoho: flow failed", err);
showStatus("EasyBusy setup failed: " + (err.message || err), "error");
renderOnboarding({
title: "Something went wrong",
message: (err && err.message) || String(err),
primaryLabel: "Try again",
onPrimary: () => startEasyBusyFlow(),
});
}
}
// ---------------------------------------------------------------------------
// Zoho SDK bootstrap
// ---------------------------------------------------------------------------
function boot() {
let embedded;
try { embedded = window.self !== window.top; }
catch (e) { embedded = true; }
state.insideCrm = embedded;
if (typeof ZOHO === "undefined" || !ZOHO.embeddedApp) {
console.warn("EasyBusy Zoho: ZOHO SDK not available — standalone mode");
state.insideCrm = false;
startEasyBusyFlow();
return;
}
let started = false;
const start = () => {
if (started) return;
started = true;
startEasyBusyFlow();
};
ZOHO.embeddedApp.on("PageLoad", function (data) {
console.log("EasyBusy Zoho: PageLoad", data);
state.zohoContext = data;
state.insideCrm = true;
start();
});
// Web tabs may never fire PageLoad — start once init has had time to settle.
setTimeout(function () {
if (!started) {
console.warn("EasyBusy Zoho: PageLoad not received — starting (insideCrm=" + state.insideCrm + ")");
start();
}
}, 3000);
ZOHO.embeddedApp.init();
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", boot);
} else {
boot();
}
})();