/** * Levelup Pharmacy Flow Integration System v2.0 * For Boots Apotek - Regulated Product Purchase Flows * * This script manages special purchase flows for regulated pharmacy products, * including Viagra/Sildenafil (requiring pharmacist approval) and other * categories requiring post-purchase consultation. */ (function () { "use strict"; // ===================================== // CONFIGURATION // ===================================== // Edit this section to add new products, modify flows, or change settings // Debug mode detection // Priority: 1. Global flag, 2. localStorage, 3. Default (false) const isDebugEnabled = function () { // Check global flag (set by test page) if (typeof window.__lvlDebugMode !== "undefined") { return window.__lvlDebugMode; } // Check localStorage (for manual debugging) // Set via console: localStorage.setItem('lvl_debug', 'true') // Remove via console: localStorage.removeItem('lvl_debug') try { return localStorage.getItem("lvl_debug") === "true"; } catch (e) { // localStorage might not be available return false; } }; const FLOW_CONFIG = { flows: { restrictedMedications: { skus: ["498195", "056285"], flowType: "requiresChatApproval", liveChatLicense: 11781603, liveChatGroup: 1, requiresPharmacistApproval: true, approvalMessage: "Produktet blir nå lagt i handlekurven", }, categoryA: { skus: ["425288", "428133"], flowType: "immediateAdd", liveChatLicense: 11781603, liveChatGroup: 4, requiresPharmacistApproval: false, }, categoryB: { skus: [], flowType: "immediateAdd", liveChatLicense: 11781603, liveChatGroup: 5, requiresPharmacistApproval: false, }, }, pharmacyHours: { openTime: 8, closeTime: 16, openDays: [1, 2, 3, 4, 5], // Monday-Friday }, debug: isDebugEnabled(), // Dynamically determined }; // ===================================== // MAIN FLOW MANAGER // ===================================== // This is the main controller that detects SKUs and initiates appropriate flows class FlowManager { constructor() { this.logger = new Logger("FlowManager"); this.overlayManager = new OverlayManager(); this.liveChatManager = new LiveChatManager(); this.currentFlow = null; this.interceptedButton = null; this.originalButton = null; this.logger.info("FlowManager initialized"); this.initialize(); } initialize() { const self = this; // Wait for DOM ready if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", function () { self.setup(); }); } else { this.setup(); } } setup() { this.logger.debug("Setting up flow detection"); // Clean up any existing flow first this.cleanup(); // Try to detect SKU and setup appropriate flow const sku = this.detectCurrentSKU(); if (!sku) { this.logger.debug("No SKU detected, waiting for changes"); this.observeForSKUChanges(); return; } this.setupFlowForSKU(sku); } cleanup() { // If we have an intercepted button, restore the original if (this.interceptedButton && this.originalButton) { this.logger.debug("Restoring original buy button"); // Remove the intercepted button if (this.interceptedButton.parentNode) { this.interceptedButton.parentNode.removeChild( this.interceptedButton, ); } // Show the original button again this.originalButton.style.display = ""; this.interceptedButton = null; this.originalButton = null; } // Clear current flow this.currentFlow = null; // Hide any open overlays this.overlayManager.hide(); } /** * External API for Magento integration * Called by Magento widget system to convert a specific button * @param {string} sku - Product SKU * @param {HTMLElement} buttonElement - The button to intercept */ convertButton(sku, buttonElement) { this.logger.info("convertButton called (Magento integration)", { sku: sku, }); if (!buttonElement) { this.logger.error("No button element provided"); return; } // Check if this SKU requires validation const requiresValidation = this.isRequireValidation(sku); if (!requiresValidation) { this.logger.debug( "SKU does not require validation, enabling button", sku, ); this.enableButton(buttonElement); return; } // Find flow config for this SKU const flowConfig = this.findFlowForSKU(sku); if (!flowConfig) { this.logger.warn( "SKU requires validation but no flow config found", sku, ); // Still intercept, but use a default flow this.initializeDefaultFlow(buttonElement); return; } this.initializeFlow(flowConfig, buttonElement); } /** * Check if SKU requires pharmacist validation * Checks both local config and window.checkout.pharmacistValidationSkus * @param {string} sku * @returns {boolean} */ isRequireValidation(sku) { // Check local flow config const hasLocalConfig = this.findFlowForSKU(sku) !== null; if (hasLocalConfig) { return true; } // Check Magento's pharmacistValidationSkus if ( window.checkout && window.checkout.pharmacistValidationSkus && Array.isArray(window.checkout.pharmacistValidationSkus) ) { return window.checkout.pharmacistValidationSkus.includes(sku); } return false; } /** * Enable a button that was disabled by validation logic * @param {HTMLElement} element */ enableButton(element) { if (element.classList.contains("live-chat-validation")) { element.disabled = false; element.classList.remove("live-chat-validation"); } } /** * Initialize a default flow for SKUs in pharmacistValidationSkus * but not in our local config * @param {HTMLElement} buttonElement */ initializeDefaultFlow(buttonElement) { // Use requiresChatApproval as default for pharmacy validation SKUs const defaultConfig = { flowType: "requiresChatApproval", liveChatLicense: 11781603, liveChatGroup: 1, requiresPharmacistApproval: true, approvalMessage: "Produktet blir nå lagt i handlekurven", }; this.initializeFlow(defaultConfig, buttonElement); } detectCurrentSKU() { // Multiple strategies to detect SKU // Strategy 1: Check URL parameters const urlParams = new URLSearchParams(window.location.search); const skuFromUrl = urlParams.get("sku") || urlParams.get("product"); if (skuFromUrl) { this.logger.debug("SKU from URL", skuFromUrl); return skuFromUrl; } // Strategy 2: Check data attributes (prioritize specific elements) // First check for data-current-sku (our test page uses this) const currentSkuElement = document.querySelector("[data-current-sku]"); if ( currentSkuElement && currentSkuElement.dataset.currentSku && currentSkuElement.dataset.currentSku !== "" ) { this.logger.debug( "SKU from current-sku attribute", currentSkuElement.dataset.currentSku, ); return currentSkuElement.dataset.currentSku; } // Then check product-specific elements const productElement = document.querySelector( "[data-sku], [data-product-sku], [data-product-id]", ); if (productElement) { const sku = productElement.dataset.sku || productElement.dataset.productSku || productElement.dataset.productId; if (sku && sku !== "") { this.logger.debug("SKU from data attribute", sku); return sku; } } // Strategy 3: Check meta tags const metaSku = document.querySelector( 'meta[property="product:sku"], meta[name="product:sku"]', ); if (metaSku && metaSku.content) { this.logger.debug("SKU from meta tag", metaSku.content); return metaSku.content; } // Strategy 4: Check for SKU in specific product display element // Only look in actual product display areas, not the entire page const productDisplay = document.querySelector( ".product-sku, #product-sku-display", ); if (productDisplay) { const skuPattern = /SKU:\s*([A-Z0-9_-]+)/i; const displayText = productDisplay.innerText || productDisplay.textContent; if (displayText) { const match = displayText.match(skuPattern); if (match && match[1] && match[1] !== "-") { this.logger.debug("SKU from product display", match[1]); return match[1]; } } } return null; } observeForSKUChanges() { const self = this; // Watch for dynamic content changes that might indicate product page load const observer = new MutationObserver(function () { const sku = self.detectCurrentSKU(); if (sku) { observer.disconnect(); self.setupFlowForSKU(sku); } }); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: [ "data-sku", "data-product-sku", "data-product-id", ], }); } setupFlowForSKU(sku) { const flowConfig = this.findFlowForSKU(sku); if (!flowConfig) { this.logger.debug( "No flow configured for SKU - regular product", sku, ); // Important: Don't intercept button for regular products return; } this.logger.info("Flow found for SKU", { sku: sku, flowType: flowConfig.flowType, }); // Find buy button const buyButton = this.findBuyButton(); if (!buyButton) { this.logger.warn("Buy button not found, waiting for it"); this.waitForBuyButton(flowConfig); return; } this.initializeFlow(flowConfig, buyButton); } findFlowForSKU(sku) { for (const key in FLOW_CONFIG.flows) { if (FLOW_CONFIG.flows.hasOwnProperty(key)) { const config = FLOW_CONFIG.flows[key]; if (config.skus.indexOf(sku) !== -1) { return config; } } } return null; } findBuyButton() { // Multiple strategies to find the buy button const selectors = [ 'button[data-action="add-to-cart"]', "button.add-to-cart", "button.btn-add-to-cart", 'button[type="submit"][name="add"]', 'button:contains("Legg i handlekurv")', 'button:contains("Kjøp")', '.product-add-form button[type="submit"]', "#product-addtocart-button", "#add-to-cart-button", // Added for test page ]; for (let i = 0; i < selectors.length; i++) { const selector = selectors[i]; // Handle :contains pseudo-selector manually if (selector.indexOf(":contains") !== -1) { const text = selector.match(/:contains\("([^"]+)"\)/); if (text && text[1]) { const buttons = document.querySelectorAll("button"); for (let j = 0; j < buttons.length; j++) { const button = buttons[j]; if ( button.textContent && button.textContent.indexOf(text[1]) !== -1 ) { this.logger.debug( "Buy button found with text", text[1], ); return button; } } } } else { const button = document.querySelector(selector); if (button) { this.logger.debug( "Buy button found with selector", selector, ); return button; } } } return null; } waitForBuyButton(flowConfig) { const self = this; const observer = new MutationObserver(function () { const buyButton = self.findBuyButton(); if (buyButton) { observer.disconnect(); self.initializeFlow(flowConfig, buyButton); } }); observer.observe(document.body, { childList: true, subtree: true, }); } initializeFlow(config, buyButton) { let flow; switch (config.flowType) { case "requiresChatApproval": flow = new RequiresChatApprovalFlow( config, this.overlayManager, this.liveChatManager, ); break; case "immediateAdd": flow = new ImmediateAddFlow( config, this.overlayManager, this.liveChatManager, ); break; default: this.logger.error("Unknown flow type", config.flowType); return; } // Store references for cleanup this.originalButton = buyButton; this.currentFlow = flow; // Intercept the button and get the new button reference const newButton = flow.interceptBuyButton(buyButton); this.interceptedButton = newButton; this.logger.info("Flow initialized and buy button intercepted"); } } // ===================================== // FLOW IMPLEMENTATIONS // ===================================== // Base Flow Class - All flows extend this class BaseFlow { constructor(config, overlayManager, liveChatManager) { this.config = config; this.overlay = overlayManager; this.liveChat = liveChatManager; this.logger = new Logger(this.constructor.name); this.originalButton = null; } interceptBuyButton(button) { this.logger.info("Intercepting buy button"); this.originalButton = button; // Store reference in global scope for compatibility window.__lvl = window.__lvl || {}; window.__lvl.vgrChatbot = window.__lvl.vgrChatbot || {}; window.__lvl.vgrChatbot.buttonToClick = button; // Clone button for our interceptor const newButton = button.cloneNode(true); // Mark the new button with our class newButton.classList.add("lvlbtn"); // Assign a new ID if the original had one (avoid duplicate IDs) if (button.id) { newButton.id = "lvl-" + button.id; } // Remove any existing onclick handler newButton.onclick = null; // Add our interceptor const self = this; newButton.addEventListener("click", function (e) { e.preventDefault(); e.stopPropagation(); self.handleBuyClick(); return false; // Extra prevention }); // IMPORTANT: Hide original button but keep it in DOM // This preserves Magento form functionality - the original button // remains part of the form and can be clicked for actual submission button.style.display = "none"; // Append new button as sibling after the hidden original button.parentNode.appendChild(newButton); // Return the new button for reference return newButton; } handleBuyClick() { // Override in subclasses this.logger.warn("handleBuyClick not implemented"); } executeOriginalPurchase() { this.logger.info("Executing original purchase"); if (this.originalButton) { this.originalButton.click(); } } } // Requires Chat Approval Flow - For products requiring pharmacist approval before purchase class RequiresChatApprovalFlow extends BaseFlow { handleBuyClick() { const isOpen = PharmacyHours.isOpen(); this.logger.info("Viagra flow triggered", { pharmacyOpen: isOpen }); const template = Templates.requiresChatApproval(isOpen); this.overlay.show(template); if (isOpen) { this.setupOpenHours(); } else { this.setupClosedHours(); } } setupOpenHours() { const self = this; // Start chat button const startChatBtn = document.querySelector(".lvl-start-chat"); if (startChatBtn) { startChatBtn.addEventListener("click", function () { self.logger.info("User clicked start chat"); // Show loading state startChatBtn.classList.add("lvl-button-loading"); startChatBtn.disabled = true; // Initialize LiveChat only when user clicks the button self.liveChat.initialize( self.config.liveChatLicense, self.config.liveChatGroup, ); // Setup message interception self.liveChat.onMessage(function (data) { if ( data.agent_login && data.text && data.text.includes(self.config.approvalMessage) ) { self.logger.info("Approval message received"); self.overlay.hide(); self.liveChat.hide(); self.executeOriginalPurchase(); } }); // Show the chat window self.liveChat.show(); // Remove loading state after a short delay (chat should be opening) setTimeout(function () { startChatBtn.classList.remove("lvl-button-loading"); startChatBtn.disabled = false; }, 1000); }); } } setupClosedHours() { const self = this; const checkbox = document.querySelector(".lvl-gender-checkbox"); const clickCollectBtn = document.querySelector(".lvl-click-collect"); if (checkbox && clickCollectBtn) { checkbox.addEventListener("change", function (e) { clickCollectBtn.disabled = !e.target.checked; }); clickCollectBtn.addEventListener("click", function () { self.logger.info("Click & Collect initiated"); // Implementation for click & collect flow // This would integrate with the pharmacy's ordering system }); } } } // Immediate Add Flow - For products requiring post-purchase consultation class ImmediateAddFlow extends BaseFlow { handleBuyClick() { this.logger.info("Immediate add flow triggered"); // Get template based on config let template; if ( this.config.flowType === "immediateAdd" && this.config.liveChatGroup === 4 ) { template = Templates.categoryA(); } else if ( this.config.flowType === "immediateAdd" && this.config.liveChatGroup === 3 ) { template = Templates.categoryB(); } else { this.logger.error("Unknown template for config", this.config); return; } this.overlay.show(template); this.setupButtons(); } setupButtons() { const self = this; const continueBtn = document.querySelector(".lvl-continue"); const cancelBtn = document.querySelector(".lvl-cancel"); if (cancelBtn) { cancelBtn.addEventListener("click", function () { self.logger.info("User cancelled"); self.overlay.hide(); }); } if (continueBtn) { continueBtn.addEventListener("click", function () { self.logger.info( "User continued - adding to cart and opening chat", ); // Show loading state continueBtn.classList.add("lvl-button-loading"); continueBtn.disabled = true; // Small delay to show the spinner before actions setTimeout(function () { // Initialize LiveChat with appropriate group self.liveChat.initialize( self.config.liveChatLicense, self.config.liveChatGroup, ); // Hide overlay self.overlay.hide(); // Execute original purchase (add to cart) self.executeOriginalPurchase(); // Open chat after a brief delay to ensure cart update setTimeout(function () { self.liveChat.show(); }, 500); }, 300); }); } } } // ===================================== // UTILITIES AND HELPERS // ===================================== // Logger Utility class Logger { constructor(prefix) { this.prefix = prefix; this.enabled = FLOW_CONFIG.debug; this.providerName = "Levelup"; } _log(level, message, data) { if (!this.enabled) return; const colors = { debug: "#9aba35", info: "#199", warn: "#ff9800", error: "#f44336", }; const args = [ "%c" + this.providerName + "%c>%c" + this.prefix + "%c:%c" + message, "color: #da55ba; padding: 2px; border-radius: 2px 0 0 2px;", "color: #777; padding: 2px;", "color: " + colors[level] + "; padding: 2px; border-radius: 0 2px 2px 0; margin-right: 2px;", "color: #199; margin-right: 8px;", "color: inherit;", ]; if (data !== undefined) { args.push(data); } console.log.apply(console, args); } debug(message, data) { this._log("debug", message, data); } info(message, data) { this._log("info", message, data); } warn(message, data) { this._log("warn", message, data); } error(message, data) { this._log("error", message, data); } } // Pharmacy Hours Utility class PharmacyHours { static isOpen() { const now = new Date(); const day = now.getDay(); const hour = now.getHours(); const config = FLOW_CONFIG.pharmacyHours; const isOpenDay = config.openDays.includes(day); const isOpenHour = hour >= config.openTime && hour < config.closeTime; return isOpenDay && isOpenHour; } } // Overlay Manager class OverlayManager { constructor() { this.logger = new Logger("OverlayManager"); this.container = null; this.closeCallbacks = []; } show(content) { this.logger.debug("Showing overlay"); // Remove existing overlay if any this.hide(); // Create overlay container this.container = document.createElement("div"); this.container.className = "lvl-overlay-container active"; this.container.innerHTML = content; // Add event listeners this._setupEventListeners(); // Add to DOM document.body.appendChild(this.container); } hide() { if (this.container) { this.logger.debug("Hiding overlay"); this.container.remove(); this.container = null; this._triggerCloseCallbacks(); } } onClose(callback) { this.closeCallbacks.push(callback); } _setupEventListeners() { const self = this; // Close on background click this.container.addEventListener("click", function (e) { if (e.target === self.container) { self.hide(); } }); // Close button const closeBtn = this.container.querySelector(".lvl-overlay-close"); if (closeBtn) { closeBtn.addEventListener("click", function () { self.hide(); }); } } _triggerCloseCallbacks() { this.closeCallbacks.forEach(function (cb) { cb(); }); this.closeCallbacks = []; } } // LiveChat Manager class LiveChatManager { constructor() { this.logger = new Logger("LiveChatManager"); this.initialized = false; this.messageCallbacks = []; } initialize(license, group) { if (this.initialized) return; this.logger.info("Initializing LiveChat", { license: license, group: group, }); window.__lc = window.__lc || {}; window.__lc.license = license; window.__lc.group = group; // LiveChat initialization code (function (n, t, c) { function i(n) { return e._h ? e._h.apply(null, n) : e._q.push(n); } var e = { _q: [], _h: null, _v: "2.0", on: function () { i(["on", c.call(arguments)]); }, once: function () { i(["once", c.call(arguments)]); }, off: function () { i(["off", c.call(arguments)]); }, get: function () { if (!e._h) throw new Error( "[LiveChatWidget] You can't use getters before load.", ); return i(["get", c.call(arguments)]); }, call: function () { i(["call", c.call(arguments)]); }, init: function () { var n = t.createElement("script"); n.async = !0; n.type = "text/javascript"; n.src = "https://cdn.livechatinc.com/tracking.js"; t.head.appendChild(n); }, }; !n.__lc.asyncInit && e.init(); n.LiveChatWidget = n.LiveChatWidget || e; })(window, document, [].slice); // Setup message listener const self = this; window.LC_API = window.LC_API || {}; window.LC_API.on_message = function (data) { self.logger.debug("LiveChat message received", data); self._handleMessage(data); }; this.initialized = true; } show() { this.logger.debug("Showing LiveChat"); if (window.LC_API && window.LC_API.open_chat_window) { window.LC_API.open_chat_window(); } } hide() { this.logger.debug("Hiding LiveChat"); if (window.LC_API && window.LC_API.hide_chat_window) { window.LC_API.hide_chat_window(); } } onMessage(callback) { this.messageCallbacks.push(callback); } _handleMessage(data) { this.messageCallbacks.forEach(function (cb) { cb(data); }); } } // ===================================== // TEMPLATES // ===================================== class Templates { static requiresChatApproval(isOpen) { const logoUrl = "https://www.boots.no/static/version1675334844/frontend/Ateles/boots/nb_NO/images/logo.svg"; if (isOpen) { return ( '
' + '
' + 'Boots Apotek' + "
" + '
×
' + '
' + "

Egenerklæring for kjøp av produkt uten resept

" + "

" + "Dette produktet er et legemiddel som krever ekstra " + "veiledning fra farmasøyt før kjøp, du må derfor svare på " + "noen spørsmål i chatvinduet. Hensikten er å avklare om " + "du har sykdommer eller bruker medisiner som gjør at du " + "ikke kan bruke produktet, og ellers redusere faren for " + "feilbruk og bivirkninger." + "

" + "

" + "Farmasøyten vil deretter sende deg videre til " + "handlekurven." + "

" + '
' + '' + "
" + "
" + "
" ); } else { return ( '
' + '
' + 'Boots Apotek' + "
" + '
×
' + '
' + "

Bestilling med Klikk og Hent

" + "

" + "Dette produktet er et legemiddel som krever ekstra " + "veiledning fra farmasøyt før kjøp. Mellom kl 16.00 og " + "08.00, samt i helger, er produktet derfor kun " + "tilgjengelig for bestilling med Klikk og hent i apotek." + "

" + "

" + "Ordren vil være klart for henting på ditt valgte Boots " + "Apotek innen 1 time i åpningstiden til apoteket." + "

" + '
' + "
Dette produktet kan kun kjøpes av menn over 18 år. Kan du bekrefte at du er mann?
" + "" + "
" + '
' + '' + "
" + "

" + '' + "Finn ditt apotek og se åpningstider" + "" + " (åpnes i nytt vindu)" + "

" + "
" + "
" ); } } static categoryA() { const logoUrl = "https://www.boots.no/static/version1675334844/frontend/Ateles/boots/nb_NO/images/logo.svg"; return `
Boots Apotek
×

Egenerklæring før kjøp av produkt

For å sikre trygg bruk av dette produktet, er vi pålagt å gi deg obligatorisk veiledning og informasjon, samt stille noen kontrollspørsmål før kjøp.

Høykonsentrert fluoridtannpasta

Tannpasta med 5 mg/g natriumfluorid

Er dette legemidlet egnet for deg?

Høykonsentrert fluoridtannpasta skal kun brukes av voksne og ungdom over 16 år som har økt risiko for å få hull i tennene eller tannrøttene. Dette gjelder deg hvis du har minst en av disse tilstandene:

Viktig informasjon:

  1. Denne tannpastaen har et høyt innhold av fluorid​. ​Det er derfor viktig at du gjør det følgende:

    • Spytt ut tannpastaen, ikke svelg
    • Oppbevar tannpastaen slik at ingen i husholdningen forveksler den med vanlig tannkrem.
    • Unngå andre kilder til fluorid mens du bruker høyfluorid tannpasta: fluortabletter, fluortyggegummi, fluormunnskyll.
    • Begrens bruk av drikkevann med høyt fluorinnhold. Bruker du brønnvann eller oppholder deg i land med høyt fluorinnhold i drikkevannet over tid, bør du sjekke fluorinnholdet.
  2. For at denne tannpastaen skal tilføre tennene nok fluorid, må du følge doseringen nøye.

    • Påfør en 2 cm lang strimmel tannpasta på tannbørsten for hver tannpuss (2 cm gir mellom 3 mg og 5 mg fluorid).
    • Puss tennene 3 ganger daglig etter måltider i omtrent 3 minutter.
    • Ikke skyll munnen etter at du har pusset tennene.
  3. Gå jevnlig til kontroll hos tannlege/tannpleier​. Bruk av høykonsentrert tannpasta erstatter ikke besøk hos tannlege eller tannpleier.

Les pakningsvedlegget​ nøye før bruk.

  • Du vil bli sendt videre til vår chat i åpningstiden, som er hverdager kl. 08–16. Farmasøyten vil deretter hjelpe deg videre til handlekurven.
  • Utenfor åpningstid vil produktet legges i handlekurven for Klikk og Hent i apotek. Du vil få veiledning når du henter varen.

Informasjonen lagres ikke, men brukes kun for å sikre at produktet tilbys trygt. Dersom du har spørsmål eller trenger hjelp, kan du kontakte vår kundeservice på 23 37 66 01 eller via chat.

`; } static categoryB() { const logoUrl = "https://www.boots.no/static/version1675334844/frontend/Ateles/boots/nb_NO/images/logo.svg"; return ( '
' + '
' + 'Boots Apotek' + "
" + '
×
' + '
' + "

Farmasøytveiledning påkrevd

" + "

" + "For din sikkerhet krever dette legemiddelet en samtale " + "med farmasøyt. Produktet vil bli lagt i handlekurven, " + "og en chat vil åpnes hvor du kan stille spørsmål og " + "få veiledning." + "

" + '
' + '' + '' + "
" + "
" + "
" ); } } // ===================================== // STYLES // ===================================== const styles = ` /* LiveChat widget visibility */ iframe#chat-widget { display: block !important; } /* Overlay styles */ .lvl-overlay-container { display: none; } .lvl-overlay-container.active { position: fixed; top: 0; left: 0; bottom: 0; right: 0; z-index: 1000; display: flex; align-items: center; justify-content: center; background-color: rgba(0, 0, 0, 0.3); } .lvl-overlay-inner { position: relative; max-width: 40rem; max-height: 80vh; padding: 2rem 3rem 3rem; background-color: #fff; overflow-y: auto; box-shadow: 0 0 15px #555; border-radius: 5px; border: 1px solid #ddd; } .lvl-overlay-header { display: flex; align-items: center; margin-bottom: 2rem; height: 50px; } .lvl-overlay-header img { height: 100%; } .lvl-overlay-close { position: absolute; top: 0.5rem; right: 0.5rem; width: 50px; height: 50px; border-radius: 2rem; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 22px; } .lvl-overlay-close:hover { background-color: rgba(0, 0, 0, 0.03); } .lvl-overlay-content h3 { color: #004990; font-size: 1.5rem; margin-top: 0; } .lvl-overlay-content h4 { color: #004990; font-size: 1.2rem; margin-top: 0; } .lvl-overlay-content p { margin-bottom: 1rem; } .lvl-overlay-content .mt { margin-top: 1rem; } .lvl-overlay-content .mb { margin-bottom: 1rem; } .lvl-overlay-content .m0 { margin-top: 0; margin-bottom: 0; } .lvl-overlay-content .mt0 { margin-top: 0; } .lvl-overlay-content .mb0 { margin-bottom: 0; } .lvl-overlay-content .lvl-important { background-color: #e9f0f7; border-radius: 8px; padding: 1rem; } .lvl-overlay-content ul { list-style-position: outside; margin-left: 1.2rem; } .lvl-overlay-content ol { margin-left: 1.2rem; } .lvl-overlay-actions { margin-top: 2rem; display: flex; gap: 1rem; } .lvl-overlay-actions button { padding: 0.75rem 1.5rem; border: 1px solid #004990; border-radius: 4px; cursor: pointer; font-size: 1rem; transition: all 0.2s; } .lvl-overlay-actions button.primary { background-color: #004990; color: white; } .lvl-overlay-actions button.primary:hover { background-color: #003570; } .lvl-overlay-actions button.secondary { background-color: white; color: #004990; } .lvl-overlay-actions button.secondary:hover { background-color: #f0f5fa; } .lvl-overlay-actions button:disabled { opacity: 0.5; cursor: not-allowed; } /* Generic loading state for buttons */ .lvl-button-loading { position: relative; color: transparent !important; } .lvl-button-loading::after { content: ''; position: absolute; width: 16px; height: 16px; top: 50%; left: 50%; margin: -8px 0 0 -8px; border: 2px solid #ffffff; border-top-color: transparent; border-radius: 50%; animation: lvl-spin 0.6s linear infinite; } /* Spinner color for secondary (white) buttons */ .lvl-button-loading.secondary::after { border-color: #004990; border-top-color: transparent; } @keyframes lvl-spin { to { transform: rotate(360deg); } } .lvl-gender-confirm { margin: 1.5rem 0; background-color: #fafcff; padding: 1rem; border-radius: 4px; border: 1px solid #ddd; } .lvl-gender-confirm label { display: flex; align-items: center; margin-top: 0.5rem; cursor: pointer; } .lvl-gender-confirm input[type="checkbox"] { margin-right: 0.5rem; } @media (max-width: 480px) { .lvl-overlay-inner { position: fixed; top: 0.5rem; left: 0.5rem; bottom: 0.5rem; right: 0.5rem; max-width: unset; max-height: unset; padding: 1.5rem; } .lvl-overlay-header { display: none; } .lvl-overlay-content h3 { font-size: 1.2rem; } .lvl-overlay-actions { flex-direction: column; } } `; // Inject styles into head const styleElement = document.createElement("style"); styleElement.type = "text/css"; styleElement.textContent = styles; // Wait for DOM to be ready before injecting styles if (document.head) { document.head.appendChild(styleElement); } else { document.addEventListener("DOMContentLoaded", function () { document.head.appendChild(styleElement); }); } // ===================================== // INITIALIZE SYSTEM // ===================================== // Store reference in global scope for debugging window.__lvl = window.__lvl || {}; window.__lvl.flowManager = new FlowManager(); // Log initialization const initLogger = new Logger("INIT"); initLogger.info("Levelup Pharmacy Flow System v2.0 initialized"); // Log debug status if enabled if (FLOW_CONFIG.debug) { initLogger.info("Debug mode enabled", { source: window.__lvlDebugMode ? "global flag" : "localStorage", }); initLogger.info( 'To disable debug: localStorage.removeItem("lvl_debug")', ); } // ===================================== // MAGENTO / BOOTS WEBSHOP COMPATIBILITY // ===================================== // This section provides integration with Boots' Magento e-commerce platform. // It exposes the necessary APIs for Magento's widget system to call. /** * Live Chat Product API * Compatible with Boots' existing Magento widget: Ateles_LiveChat/js/model/live-chat-product */ const liveChatProduct = { /** * Convert a button for pharmacist validation flow * Called by Magento widget: $.widget('mage.addtoLiveChat') * @param {string} sku - Product SKU * @param {HTMLElement} element - The button element to convert */ convertButton: function (sku, element) { window.__lvl.flowManager.convertButton(sku, element); }, /** * Check if SKU requires validation * @param {string} sku * @returns {boolean} */ isRequireValidatoin: function (sku) { // Note: keeping original typo for backwards compatibility return window.__lvl.flowManager.isRequireValidation(sku); }, /** * Enable button if it was disabled by validation logic * @param {HTMLElement} element */ enableButton: function (element) { window.__lvl.flowManager.enableButton(element); }, }; // Expose liveChatProduct globally for direct access window.__lvl.liveChatProduct = liveChatProduct; /** * RequireJS Module Definition * Makes this script compatible with Magento's RequireJS system. * Can be used as a drop-in replacement for the old live-chat-product module. * * Usage in Magento: * define(['Ateles_LiveChat/js/model/live-chat-product'], function(liveChatProduct) { * liveChatProduct.convertButton(sku, element); * }); * * Or with our script: * define(['path/to/pharmacy-flow-integration'], function(liveChatProduct) { * liveChatProduct.convertButton(sku, element); * }); */ if (typeof define === "function" && define.amd) { define("Ateles_LiveChat/js/model/live-chat-product", [], function () { return liveChatProduct; }); // Also register under a generic name for flexibility define("levelup-pharmacy-flow", [], function () { return liveChatProduct; }); initLogger.info("RequireJS modules registered", { modules: [ "Ateles_LiveChat/js/model/live-chat-product", "levelup-pharmacy-flow", ], }); } /** * jQuery Widget Compatibility * If jQuery is available, register the addtoLiveChat widget * This is a drop-in replacement for Boots' existing widget */ if (typeof jQuery !== "undefined" && jQuery.widget) { jQuery.widget("mage.addtoLiveChat", { options: { sku: "", }, /** * Widget initialization * @private */ _create: function () { liveChatProduct.convertButton( this.options.sku, this.element[0], ); }, }); initLogger.info("jQuery widget registered: mage.addtoLiveChat"); } /** * Global function for enabling VGR LiveChat * Compatible with the old implementation */ if (typeof window.enableVgrLiveChat === "undefined") { window.enableVgrLiveChat = function (callback) { // Initialize LiveChat if not already done const flowManager = window.__lvl.flowManager; if (flowManager && flowManager.liveChatManager) { flowManager.liveChatManager.initialize( FLOW_CONFIG.flows.restrictedMedications.liveChatLicense, FLOW_CONFIG.flows.restrictedMedications.liveChatGroup, ); } if (typeof callback === "function") { // Small delay to ensure LiveChat is ready setTimeout(callback, 100); } }; } })();

Søkeresultat for: "dr. greve pharma tørr hud"

Handle etter
Velg filter
PSO tiden er over, vennligst logg på igjen
Boots Norway © 2025