(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 `
${escapeHtml(name)}
${escapeHtml(meta || c.id)}
`; }).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(); } })();