1// app/javascript/controllers/lead_automation_engine_controller.js 2import { Controller } from "@hotwired/stimulus" 3 4// Identifier: data-controller="lead-automation-engine" 5export default class extends Controller { 6 static targets = [ 7 "publishNotification", 8 "notificationSubtitle", 9 "tierListView", 10 "tierDetailView", 11 "tierDetailsPanel", 12 "detailTitle", 13 "actionLibrary", 14 "emailTemplates", 15 "actionModal", 16 "modalTitle", 17 "modalBody" 18 ] 19 20 static values = { 21 csrf: String, 22 restoreUrl: String 23 } 24 25 connect() { 26 // Hide publish banner by default 27 if (this.hasPublishNotificationTarget) { 28 this.publishNotificationTarget.classList.remove("show") 29 } 30 31 // Start with detail view hidden; weâll show it once we pick a tier 32 if (this.hasTierDetailViewTarget) { 33 this.tierDetailViewTarget.classList.add("hidden") 34 } 35 36 // Internal state: per-tier welcome message (subject/body) 37 this.welcomeState = {} 38 this.dirty = false 39 this.senderDirty = false 40 this.calendarDirty = false // ð track calendar changes separately 41 42 // Load templates from inline JSON so we can prefill welcome messages 43 this.templates = [] 44 this._loadTemplatesFromDOM() 45 46 // Open Hot Leads by default 47 this._selectDefaultTier() 48 } 49 50 51 // --------------------------------------------------------------------------- 52 // TEMPLATE HANDLING 53 // --------------------------------------------------------------------------- 54 55 _loadTemplatesFromDOM() { 56 try { 57 const el = document.getElementById("automation-email-templates") 58 if (!el) return 59 const text = (el.textContent || "").trim() 60 if (!text) return 61 62 const parsed = JSON.parse(text) 63 const rawList = Array.isArray(parsed) 64 ? parsed 65 : Array.isArray(parsed?.templates) 66 ? parsed.templates 67 : [] 68 69 this.templates = this._normalizeTemplates(rawList) 70 } catch (e) { 71 console.warn("[lead-automation-engine] inline template JSON parse failed:", e) 72 this.templates = [] 73 } 74 } 75 76 _normalizeTemplates(list) { 77 const arr = Array.isArray(list) ? list : [] 78 const byId = new Map() 79 80 const friendlyFromId = (id) => { 81 return String(id) 82 .replace(/^welcome_tier_/, "") 83 .replace(/_/g, " ") 84 .replace(/\b\w/g, (c) => c.toUpperCase()) 85 } 86 87 for (const t of arr) { 88 const id = String(t?.id ?? t?.key ?? t?.slug ?? "") 89 if (!id) continue 90 91 // Infer band from id if missing 92 let band = t.band || null 93 const lowerId = id.toLowerCase() 94 if (!band) { 95 if (lowerId.includes("_hot")) { 96 band = "hot" 97 } else if (lowerId.includes("_warm")) { 98 band = "warm" 99 } else if (lowerId.includes("_lukewarm")) { 100 band = "lukewarm" 101 } else if (lowerId.includes("_cold")) { 102 band = "cold" 103 } 104 } 105 106 const name = t.name || t.title || friendlyFromId(id) 107 108 // Subject: use existing subject if present, else name 109 const subject = t.subject || name 110 111 // Email body: use existing html/text if present, else sensible defaults 112 let html_body = t.html_body || t.text_body || "" 113 114 if (!html_body) { 115 if (band === "hot") { 116 html_body = [ 117 "<p>Hi {{lead_first_name}},</p>", 118 "<p>Thanks for reaching out and for sharing more about your needs.</p>", 119 "<p>Based on your answers, you look like a strong fit for what we do at {{company_name}}.</p>", 120 "<p>Weâll review your details and get back to you shortly with next steps.</p>", 121 "<p>Best,<br>The {{company_name}} Team</p>" 122 ].join("") 123 } else if (band === "warm") { 124 html_body = [ 125 "<p>Hi {{lead_first_name}},</p>", 126 "<p>Thanks for getting in touch.</p>", 127 "<p>You look like a promising fit, and weâd love to explore how we can help your business.</p>", 128 "<p>Weâll review your answers and follow up with suggested next steps.</p>", 129 "<p>Best,<br>The {{company_name}} Team</p>" 130 ].join("") 131 } else if (band === "lukewarm") { 132 html_body = [ 133 "<p>Hi {{lead_first_name}},</p>", 134 "<p>Thank you for your interest.</p>", 135 "<p>You might be a bit earlier in your journey, but we have resources that can help you clarify next steps.</p>", 136 "<p>Weâll send over a few suggestions and options for you to review.</p>", 137 "<p>Best,<br>The {{company_name}} Team</p>" 138 ].join("") 139 } else if (band === "cold") { 140 html_body = [ 141 "<p>Hi {{lead_first_name}},</p>", 142 "<p>Thanks for reaching out.</p>", 143 "<p>
143Right now, you may not be an immediate fit for our core offer, but weâll keep you in mind as your needs evolve.</p>", 144 "<p>In the meantime, weâll share a few resources that might still be useful.</p>", 145 "<p>Best,<br>The {{company_name}} Team</p>" 146 ].join("") 147 } else { 148 // Generic fallback for non-tier-specific templates 149 html_body = [ 150 "<p>Hi {{lead_first_name}},</p>", 151 "<p>Thanks for getting in touch with {{company_name}}.</p>", 152 "<p>Weâve received your details and will follow up with you shortly.</p>", 153 "<p>Best,<br>The {{company_name}} Team</p>" 154 ].join("") 155 } 156 } 157 158 const description = 159 t.description || 160 (band ? `Welcome email for ${band} leads` : "Email template") 161 162 const base = { 163 id, 164 key: id, 165 name, 166 title: name, 167 subject, 168 html_body, 169 text_body: "", // we only use html_body in the UI 170 band, 171 description, 172 scope: t.scope || "global", 173 version: t.version || 1, 174 locale: t.locale || "en" 175 } 176 177 const existing = byId.get(id) 178 if (!existing) { 179 byId.set(id, base) 180 } else { 181 // Prefer company-scoped overrides if they exist 182 const isCompany = (t.scope === "company") 183 if (isCompany) byId.set(id, base) 184 } 185 } 186 187 return Array.from(byId.values()).sort((a, b) => { 188 const scopeWeight = (x) => (x.scope === "company" ? 0 : 1) 189 const sw = scopeWeight(a) - scopeWeight(b) 190 if (sw !== 0) return sw 191 return (a.name || "").localeCompare(b.name || "") 192 }) 193 } 194 195 _templateForBand(band) { 196 if (!band) return null 197 198 const lower = String(band).toLowerCase() 199 const templates = this.templates || [] 200 201 // 1) Explicit mapping: welcome_tier_* should always be the primary for each band 202 const welcomeKeyByBand = { 203 hot: "welcome_tier_hot", 204 warm: "welcome_tier_warm", 205 lukewarm: "welcome_tier_lukewarm", 206 cold: "welcome_tier_cold" 207 } 208 209 const preferredKey = welcomeKeyByBand[lower] 210 211 if (preferredKey) { 212 const preferred = templates.find((t) => { 213 const id = String(t.id || t.key || t.slug || "").toLowerCase() 214 return id === preferredKey 215 }) 216 217 if (preferred) return preferred 218 } 219 220 // 2) Fallback: allow a template whose *id* contains the welcome key 221 if (preferredKey) { 222 const byIdHint = templates.find((t) => { 223 const id = String(t.id || t.key || t.slug || "").toLowerCase() 224 return id.includes(preferredKey) 225 }) 226 if (byIdHint) return byIdHint 227 } 228 229 // 3) Last-resort fallback: something with matching band 230 const byBand = templates.find((t) => { 231 const b = String(t.band || "").toLowerCase() 232 return b === lower 233 }) 234 if (byBand) return byBand 235 236 // 4) Final generic fallback (very unlikely to be needed) 237 return templates.find((t) => { 238 const id = String(t.id || t.key || t.slug || "").toLowerCase() 239 return id.includes(lower) 240 }) || null 241 } 242 243 244 // --------------------------------------------------------------------------- 245 // PUBLISH BANNER 246 // --------------------------------------------------------------------------- 247 248 showPublishBanner(message) { 249 if (!this.hasPublishNotificationTarget) return 250 if (message && this.hasNotificationSubtitleTarget) { 251 this.notificationSubtitleTarget.textContent = message 252 } 253 this.publishNotificationTarget.classList.add("show") 254 } 255 256 hidePublishNotification() { 257 if (!this.hasPublishNotificationTarget) return 258 this.publishNotificationTarget.classList.remove("show") 259 } 260 261 _markDirty(message) { 262 this.dirty = true 263 this.showPublishBanner( 264 message || "You have unsaved changes to your automation settings." 265 ) 266 } 267 268 discardDraft() { 269 this.welcomeState = {} 270 this._loadTemplatesFromDOM() 271 this.dirty = false 272 this.senderDirty = false 273 this.hidePublishNotification() 274 275 if (this.selectedTierId) this._renderWelcomeEditor() 276 } 277 278 calendarFieldChanged() { 279 this.calendarDirty = true 280 this._markDirty( 281 "You have unsaved changes to your calendar link or which tiers it applies to." 282 ) 283 } 284 285 286 senderFieldChanged() { 287 this.senderDirty = true 288 this._markDirty( 289 "You have unsaved changes to your sender configuration or welcome emails." 290 ) 291 } 292 293 async handlePublishClick(event) { 294 event.preventDefault() 295 296 const senderForm = document.getElementById("lead-automation-sender-form") 297 const calendarForm = document.getElementById("lead-automation-calendar-form") 298 const promises = [] 299 300 // 1) Sender config 301 if (senderForm && this.senderDirty) { 302 promises.push(this._saveSenderConfig(senderForm)) 303 } 304 305 // 2) Calendar config 306 if (calendarForm && this.calendarDirty) { 307 promises.push(this._saveCalendarConfig(calendarForm)) 308 } 309 310 // 3) Tier email templates 311 const bands = Object.keys(this.welcomeState || {}) 312 313 for (const band of bands) { 314 const state = this.welcomeState[band] 315 const tmpl = this._templateForBand(band) 316 if (!tmpl) continue 317 318 const originalSubject = tmpl.subject || "" 319 const originalBody = tmpl.html_body || tmpl.text_body || "" 320 321 const currentSubject = state.subject || "" 322 const currentBody = state.body || "" 323 324 if (currentSubject !== originalSubject || currentBody !== originalBody) { 325 promises.push(this._saveBandWelcome(band)) 326 } 327 } 328 329 if (promises.length === 0) { 330 this.showPublishBanner("No changes to publish.") 331 setTimeout(() => this.hidePublishNotification(), 2500) 332 return 333 } 334 335 try { 336 await Promise.all(promises) 337 338 this.senderDirty = false 339 this.calendarDirty = false 340 this.dirty = false 341 342 this.showPublishBanner("All sender, calendar & welcome email changes have been published.") 343 setTimeout(() => this.hidePublishNotification(), 3000) 344 } catch (err) { 345 console.error("[lead-automation-engine] publish failed:", err) 346 this.showPublishBanner("We couldn't publish all changes. Please try again.") 347 } 348 } 349 350 351 // --------------------------------------------------------------------------- 352 // TIER SELECTION 353 // --------------------------------------------------------------------------- 354 355 _selectDefaultTier() { 356 if (!this.hasTierListViewTarget) return 357 const hotPill = this.tierListViewTarget.querySelector('[data-tier-id="hot"]') 358 if (hotPill) { 359 this._selectTierByElement(hotPill) 360 } 361 } 362 363 selectTier(event) { 364 const pill = event.currentTarget 365 this._selectTierByElement(pill) 366 } 367 368 _selectTierByElement(pill) { 369 if (!pill) return 370 371 const tierId = pill.dataset.tierId 372 const tierName = pill.dataset.tierName || "Tier" 373 374 this.selectedTierId = tierId 375
376 if (this.hasTierDetailViewTarget) { 377 this.tierDetailViewTarget.classList.remove("hidden") 378 } 379 380 if (this.hasDetailTitleTarget) { 381 this.detailTitleTarget.textContent = tierName 382 } 383 384 if (this.hasTierListViewTarget) { 385 this.tierListViewTarget 386 .querySelectorAll(".tier-pill") 387 .forEach((el) => el.classList.remove("selected")) 388 } 389 pill.classList.add("selected") 390 391 this._renderWelcomeEditor() 392 this._renderActionLibrary() 393 } 394 395 backToList() { 396 if (this.hasTierDetailViewTarget) { 397 this.tierDetailViewTarget.classList.add("hidden") 398 } 399 if (this.hasTierListViewTarget) { 400 this.tierListViewTarget 401 .querySelectorAll(".tier-pill") 402 .forEach((el) => el.classList.remove("selected")) 403 } 404 this.selectedTierId = null 405 } 406 407 // --------------------------------------------------------------------------- 408 // WELCOME MESSAGE EDITOR (middle pane) 409 // --------------------------------------------------------------------------- 410 411 _currentWelcomeState() { 412 const tierId = this.selectedTierId 413 if (!tierId) return { subject: "", body: "" } 414 415 if (!this.welcomeState) this.welcomeState = {} 416 if (!this.welcomeState[tierId]) { 417 const tmpl = this._templateForBand(tierId) 418 this.welcomeState[tierId] = { 419 subject: tmpl?.subject || "", 420 body: tmpl?.html_body || tmpl?.text_body || "" 421 } 422 } 423 return this.welcomeState[tierId] 424 } 425 426 _renderWelcomeEditor() { 427 if (!this.hasTierDetailsPanelTarget || !this.selectedTierId) return 428 429 const tierId = this.selectedTierId 430 const tierName = this._humanTierName(tierId) 431 const welcome = this._currentWelcomeState() 432 433 this.tierDetailsPanelTarget.innerHTML = ` 434 <div style="display:flex;flex-direction:column;gap:0.75rem;"> 435 <p style="font-size:0.9rem;color:#4b5563;margin:0;"> 436 <strong>${this._escape(tierName)} welcome email</strong> â sent immediately after a ${this._escape( 437 tierName.toLowerCase() 438 )} lead submits their contact details. 439 </p> 440 441 <label style="font-size:0.85rem;font-weight:600;color:#374151;margin-top:0.25rem;"> 442 Subject line 443 </label> 444 <input type="text" 445 value="${this._escape(welcome.subject || "")}" 446 data-action="input->lead-automation-engine#updateWelcomeSubject" 447 style="border-radius:10px;border:1px solid #e5e7eb;padding:0.5rem 0.75rem;font-size:0.9rem;width:100%;"> 448 449 <label style="font-size:0.85rem;font-weight:600;color:#374151;margin-top:0.5rem;"> 450 Email body 451 </label> 452 <textarea 453 rows="10" 454 data-action="input->lead-automation-engine#updateWelcomeBody" 455 style="border-radius:10px;border:1px solid #e5e7eb;padding:0.75rem;font-size:0.9rem;width:100%;font-family:inherit;line-height:1.5;">${this._escape( 456 welcome.body || "" 457 )}</textarea> 458 459 <div style="font-size:0.8rem;color:#6b7280;margin-top:0.25rem;"> 460 You can use personalization tags such as <code>{{lead_first_name}}</code> and <code>{{company_name}}</code>. 461 </div> 462 463 <div style="display:flex;justify-content:flex-end;margin-top:0.75rem;gap:0.5rem;"> 464 <button type="button" 465 class="btn-auto" 466 data-action="click->lead-automation-engine#discardTierDraft"> 467 <i class="bx bx-undo"></i> 468 Reset tier message 469 </button> 470 <button type="button" 471 class="btn-auto" 472 data-action="click->lead-automation-engine#saveCurrentWelcome"> 473 <i class="bx bx-save"></i> 474 Save welcome message 475 </button> 476 </div> 477 </div> 478 ` 479 } 480 481 updateWelcomeSubject(event) { 482 const state = this._currentWelcomeState() 483 state.subject = event.currentTarget.value 484 this._markDirty("You have unsaved changes to your welcome emails.") 485 } 486 487 updateWelcomeBody(event) { 488 const state = this._currentWelcomeState() 489 state.body = event.currentTarget.value 490 this._markDirty("You have unsaved changes to your welcome emails.") 491 } 492 493 discardTierDraft() { 494 if (!this.selectedTierId) return 495 if (!this.templates || this.templates.length === 0) return 496 497 const tierId = this.selectedTierId 498 const tmpl = this._templateForBand(tierId) 499 this.welcomeState[tierId] = { 500 subject: tmpl?.subject || "", 501 body: tmpl?.html_body || tmpl?.text_body || "" 502 } 503 this._renderWelcomeEditor() 504 this.dirty = false 505 this.showPublishBanner("Tier message reset to last saved template.") 506 setTimeout(() => this.hidePublishNotification(), 2500) 507 } 508 509 async saveCurrentWelcome() { 510 if (!this.selectedTierId) return 511 await this._saveBandWelcome(this.selectedTierId) 512 } 513 514 async _saveBandWelcome(band) { 515 if (!band) return false 516 517 const tmpl = this._templateForBand(band) 518 if (!tmpl) { 519 console.warn("[lead-automation-engine] No template found for band:", band) 520 this.showPublishBanner("We couldn't find a template for this tier.") 521 return false 522 } 523
524 const key = tmpl.key || tmpl.id || tmpl.slug 525 if (!key) { 526 console.warn("[lead-automation-engine] Template is missing a key/id:", tmpl) 527 this.showPublishBanner("Template key missing; cannot save.") 528 return false 529 } 530 531 const stateForBand = 532 (this.welcomeState && this.welcomeState[band]) || 533 { subject: tmpl.subject || "", body: tmpl.html_body || tmpl.text_body || "" } 534 535 const payload = { 536 key: key, 537 locale: tmpl.locale || "en", 538 subject: stateForBand.subject || "", 539 html_body: stateForBand.body || "", 540 text_body: "", 541 band: tmpl.band || band 542 } 543 544 try { 545 const res = await fetch("/automation_actions/template", { 546 method: "POST", 547 headers: { 548 "Content-Type": "application/json", 549 "Accept": "application/json", 550 "X-CSRF-Token": this._csrfToken() 551 }, 552 body: JSON.stringify(payload) 553 }) 554 555 const data = await res.json().catch(() => ({})) 556 557 if (!res.ok || data.success === false) { 558 console.error("[lead-automation-engine] upsert_action_template failed:", data) 559 const msg = data.message || data.error || "We couldn't publish changes. Please try again." 560 this.showPublishBanner(msg) 561 return false 562 } 563 564 this.dirty = false 565 this.showPublishBanner("Welcome email saved and published.") 566 setTimeout(() => this.hidePublishNotification(), 3000) 567 return true 568 } catch (err) { 569 console.error("[lead-automation-engine] network error saving template:", err) 570 this.showPublishBanner("We couldn't publish changes. Please try again.") 571 return false 572 } 573 } 574 575 // --------------------------------------------------------------------------- 576 // ACTION LIBRARY (right-hand side â reuse templates list) 577 // --------------------------------------------------------------------------- 578 579 _renderActionLibrary() { 580 if (!this.hasEmailTemplatesTarget) return 581 582 const templates = this.templates || [] 583 if (templates.length === 0) { 584 this.emailTemplatesTarget.innerHTML = ` 585 <div style="padding:1rem;text-align:center;color:var(--auto-gray-400);font-size:0.875rem;"> 586 No email templates available yet. 587 </div> 588 ` 589 return 590 } 591 592 this.emailTemplatesTarget.innerHTML = templates 593 .map((tmpl) => { 594 const name = tmpl.name || tmpl.title || tmpl.id || "Untitled" 595 const desc = tmpl.description || tmpl.band || "Email template" 596 const templateId = tmpl.id || tmpl.key || tmpl.slug 597 598 return ` 599 <div class="library-action-template" data-template-id="${this._escape( 600 templateId 601 )}"> 602 <div class="library-action-icon"> 603 <i class='bx bx-envelope'></i> 604 </div> 605 <div class="library-action-info"> 606 <div class="library-action-name">${this._escape(name)}</div> 607 <div class="library-action-desc">${this._escape(desc)}</div> 608 </div> 609 <button class="library-action-add" type="button" 610 data-action="click->lead-automation-engine#applyTemplateToCurrentTier"> 611 + 612 </button> 613 </div> 614 ` 615 }) 616 .join("") 617 } 618 619 applyTemplateToCurrentTier(event) { 620 if (!this.selectedTierId) return 621 const card = event.currentTarget.closest("[data-template-id]") 622 if (!card) return 623 const templateId = card.dataset.templateId 624 const tmpl = (this.templates || []).find( 625 (t) => String(t.id || t.key || t.slug) === templateId 626 ) 627 if (!tmpl) return 628 629 const state = this._currentWelcomeState() 630 state.subject = tmpl.subject || tmpl.title || state.subject 631 state.body = tmpl.html_body || tmpl.text_body || state.body 632 633 this._renderWelcomeEditor() 634 this._markDirty("You have unsaved changes to your welcome emails.") 635 } 636 637 // --------------------------------------------------------------------------- 638 // MODAL STUBS 639 // --------------------------------------------------------------------------- 640 641 closeActionModal() { 642 if (!this.hasActionModalTarget) return 643 this.actionModalTarget.classList.remove("show") 644 } 645 646 openActionModal() { 647 if (!this.hasActionModalTarget) return 648 this.actionModalTarget.classList.add("show") 649 } 650 651 // --------------------------------------------------------------------------- 652 // HELPERS 653 // --------------------------------------------------------------------------- 654 655 _humanTierName(id) { 656 switch (String(id)) { 657 case "hot": 658 return "Hot Leads" 659 case "warm": 660 return "Warm Leads" 661 case "lukewarm": 662 return "Lukewarm Leads" 663 case "cold": 664 return "Cold / Unqualified Leads" 665 default: 666 return "This tier" 667 } 668 } 669 670 _escape(str) { 671 if (str == null) return "" 672 return String(str) 673 .replace(/&/g, "&") 674 .replace(/</g, "<") 675 .replace(/>/g, ">") 676 .replace(/"/g, """) 677 .replace(/'/g, "'") 678 } 679 680 _csrfToken() { 681 if (this.hasCsrfValue && this.csrfValue) return this.csrfValue 682 const meta = document.querySelector("meta[name='csrf-token']") 683 return meta ? meta.content : "" 684 } 685 686 // --------------------------------------------------------------------------- 687 // SENDER CONFIG SAVE 688 // --------------------------------------------------------------------------- 689 690 _saveSenderConfig(form) { 691 const url = form.action 692 const method = (form.method || "post").toUpperCase() 693 const body = new FormData(form) 694 695 return fetch(url, { 696 method: method, 697 headers: { 698 "X-CSRF-Token": this._csrfToken(), 699 "Accept": 700 "text/vnd.turbo-stream.html, text/html, application/json;q=0.9,*/*;q=0.8" 701 }, 702 body 703 }).then((res) => { 704 if (!res.ok) { 705 throw new Error(`Sender config save failed with HTTP ${res.status}`) 706 } 707 return res 708 }) 709 } 710 711 _saveCalendarConfig(form) { 712 const url = form.action 713 const method = (form.method || "post").toUpperCase() 714 const body = new FormData(form) 715 716 return fetch(url, { 717 method: method, 718 headers: { 719 "X-CSRF-Token": this._csrfToken(), 720 "Accept": 721 "text/vnd.turbo-stream.html, text/html, application/json;q=0.9,*/*;q=0.8" 722 }, 723 body 724 }).then((res) => { 725 if (!res.ok) { 726 throw new Error(`Calendar config save failed with HTTP ${res.status}`) 727 } 728 return res 729 }) 730 } 731 732 733 // --------------------------------------------------------------------------- 734 // RESTORE DEFAULT EMAIL TEMPLATES 735 // --------------------------------------------------------------------------- 736 737 async restoreDefaults(event) { 738 event.preventDefault() 739 740 const url = 741 this.hasRestoreUrlValue && this.restoreUrlValue 742 ? this.restoreUrlValue 743 : "/lead_automation/restore_email_templates_defaults" 744 745 const confirmed = window.confirm( 746 "This will reset your lead email templates back to the default set for this company.\n\n" + 747 "It will NOT change your scoring bands or sender configuration.\n\nContinue?" 748 ) 749 750 if (!confirmed) return 751 752 try { 753 const res = await fetch(url, { 754 method: "POST", 755 headers: { 756 "X-CSRF-Token": this._csrfToken(), 757 "Accept": "application/json", 758 "Content-Type": "application/json" 759 }, 760 body: JSON.stringify({}) 761 }) 762 763 if (!res.ok) { 764 throw new Error(`Restore email templates failed with HTTP ${res.status}`) 765 } 766 767 const data = await res.json().catch(() => ({})) 768 const templates = Array.isArray(data.email_templates) 769 ? data.email_templates 770 : [] 771 772 this.templates = this._normalizeTemplates(templates) 773 774 this.welcomeState = {} 775 this.dirty = false 776 this.senderDirty = false 777 778 const el = document.getElementById("automation-email-templates") 779 if (el) { 780 el.textContent = JSON.stringify(templates) 781 } 782 783 this._renderActionLibrary() 784 if (this.selectedTierId) { 785 this._renderWelcomeEditor() 786 } 787 788 const msg = 789 data.message || 790 data.notice || 791 "Email templates restored to defaults for this company." 792 793 this.showPublishBanner(msg) 794 setTimeout(() => this.hidePublishNotification(), 3000) 795 } catch (err) { 796 console.error("[lead-automation-engine] restoreDefaults error:", err) 797 this.showPublishBanner( 798 "We couldn't restore your email templates. Please try again." 799 ) 800 } 801 } 802};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.