// STOCK // /* ------------------------------------------------------------------ Stock indicators 4+ in stock -> "1,234 In Stock" (unchanged) 1-3 in stock -> "Low Stock" - CURRENTLY SWITCHED OFF (showLowStock: false), so 1-3 show the real number, e.g. "1 In Stock" 0 / out -> "Out of Stock – Check Here" click -> "Stock Due: DD/MM/YYYY" or, no date -> "Chat Now" -> click opens live chat 0 / out, Dams (supplier_1010) -> "Low Stocks - Selling Fast" (no click) - set by sellingFastSuppliers in the settings Classes are kept the same as the original script so existing CSS and chatFunction() keep working: .live-inventory-v2-shipping .in-stock in stock (+ .low-stock when 1-3) .live-inventory-v2-shipping .text-danger .checkstock-btn .defaultbtn out of stock, live-checked (supplier_1001) .live-inventory-v2-shipping .out-stock .defaultbtn out of stock, other suppliers .product-stock-click-here "Out of Stock – Check Here" text small.stock-due-details the due date (shown on click) div.out-stock-chat-checkstock .defaultbtn the "Chat Now" button Each stock element gets a data-om-state attribute so it's only processed once: "check" = out of stock, waiting for the customer to click "Check Here" "loading" = live stock lookup in progress "done" = final state reached, no further clicks handled ------------------------------------------------------------------ */ /* ---------- settings: change labels / thresholds here ---------- */ var omStock = { // "Low Stock" message on/off. Client asked for it to be hidden (Nov 2026) - // set to true to bring it back. When false, 1-3 show as "1 In Stock" etc. showLowStock: false, lowStockMax: 3, // 3 or fewer = "Low Stock" (only used when showLowStock is true) liveSupplierClass: "supplier_1001", // products with this class are checked against the live stock API on click // Suppliers whose out-of-stock items just show labels.sellingFast instead of the // Check Here > Stock Due / Chat Now flow. supplier_1010 = Dams (Nov 2026). // Remove a supplier from this list to give it the normal out-of-stock flow back. sellingFastSuppliers: ["supplier_1010"], apiUrl: "https://stock.officemonster.co.uk/?productCode=", // returns { Stock, NextStockDueDate } pastDueDays: 7, // if a due date has already passed, show today + 7 days instead labels: { lowStock: "Low Stock", outOfStock: "Out of Stock – Check Here", loading: " loading... ", stockDue: "Stock Due: ", chatNow: "Chat Now", sellingFast: "Low Stocks - Selling Fast", // out-of-stock message for sellingFastSuppliers }, // Message sent into the live chat when "Chat Now" is clicked (matches chatFunction's wording) chatMessage: function (sku) { return "Is product: " + sku + " out of stock?"; }, }; /* ---------- helpers ---------- */ // Pulls the number out of the platform's stock text: "31,179 In Stock" -> 31179. // Returns null if there's no number (so we leave the element alone). function omParseQty(text) { var m = String(text).match(/(-?[\d,]+)\s*in\s*stock/i); return m ? parseInt(m[1].replace(/,/g, ""), 10) : null; } // Turns a UK date string into a Date: "05/11/2026" -> 5 Nov 2026. // Returns null if the text isn't a DD/MM/YYYY date. function omParseUKDate(str) { var m = String(str || "").match(/(\d{1,2})\/(\d{1,2})\/(\d{4})/); if (!m) return null; var d = new Date(+m[3], +m[2] - 1, +m[1]); return isNaN(d.getTime()) ? null : d; } // Formats a Date back to UK format with leading zeros: DD/MM/YYYY. function omFormatUKDate(d) { return ( ("0" + d.getDate()).slice(-2) + "/" + ("0" + (d.getMonth() + 1)).slice(-2) + "/" + d.getFullYear() ); } // Cleans up a due date for display. Strips brackets, and if the date has // already passed, pushes it forward to today + pastDueDays. // Returns "" when there's no date, which means "show Chat Now instead". function omDueDate(raw) { raw = $.trim(String(raw || "").replace(/[{()}]/g, "")); if (!raw) return ""; var due = omParseUKDate(raw); if (!due) return raw; // not a date we recognise - show the text as supplied var today = new Date(); today.setHours(0, 0, 0, 0); if (due < today) { due = new Date(today); due.setDate(due.getDate() + omStock.pastDueDays); } return omFormatUKDate(due); } // Gets the product SKU from the nearest parent with a data-sku attribute. function omSku($el) { return $el.closest("[data-sku]").attr("data-sku") || ""; } // True if this product should be checked against the live stock API (supplier_1001). function omIsLive($el) { return ( !!omSku($el) && $el.closest(".product").hasClass(omStock.liveSupplierClass) ); } // Marks an element as finished, so clicks no longer do anything. function omDone($el) { $el.attr("data-om-state", "done").css("cursor", "default"); } /* ---------- states ---------- */ // True if the product's supplier is in sellingFastSuppliers (e.g. Dams, supplier_1010). function omIsSellingFast($el) { var $product = $el.closest(".product"); return omStock.sellingFastSuppliers.some(function (cls) { return $product.hasClass(cls); }); } // Out-of-stock item from a sellingFastSuppliers supplier: show the plain // "Low Stocks - Selling Fast" message. Not clickable - no due date or chat. // Keeps .out-stock (if the platform set it) and adds .low-stock .selling-fast for styling. function omShowSellingFast($el) { $el .removeClass("text-danger checkstock-btn defaultbtn no-stock-no-chat in-stock") .addClass("low-stock selling-fast") .html(omStock.labels.sellingFast); omDone($el); } // Shows "Low Stock". Keeps .in-stock (base styling) and adds .low-stock (for overrides). function omShowLowStock($el) { $el.addClass("in-stock low-stock").html(omStock.labels.lowStock); omDone($el); } // Live check found plenty of stock: switch the button back to a normal in-stock label. function omShowInStock($el, qty) { $el .removeClass("text-danger checkstock-btn defaultbtn out-stock low-stock") .addClass("in-stock") .html(qty.toLocaleString("en-GB") + " In Stock"); omDone($el); } // Shows "Stock Due: DD/MM/YYYY" in place of the button text. function omShowDue($el, due) { $el.html(omStock.labels.stockDue + "" + due + ""); omDone($el); } // No due date: hide the stock button and show the "Chat Now" button next to it // (the same hide/show swap the original script used). function omShowChat($el) { omDone($el); // data-om-chat stops omProcessStock() fading this label back in later // (e.g. when the stock lookup's own AJAX call triggers a re-run) $el .attr("data-om-chat", "1") .html( "" + omStock.labels.outOfStock + "", ) .hide(); $el.next(".out-stock-chat-checkstock").show(); // out-of-stock chat is showing, so hide the default "ask a question" chat $el.closest(".product").find(".askaquestion").hide(); } // Turns an out-of-stock element into the "Out of Stock – Check Here" button. // Sets the same classes the original script did, stores any due date in a hidden // , and adds one hidden "Chat Now" button after it. function omMakeOutOfStock($el) { // Dams (and any other sellingFastSuppliers): show "Low Stocks - Selling Fast" instead if (omIsSellingFast($el)) { omShowSellingFast($el); return; } // the platform puts any due date in a , e.g. "(05/11/2026)" var rawDue = $el.find("small").text(); // Classes - kept identical to the original script for each case if ($el.hasClass("out-stock") && omIsLive($el)) { // supplier_1001 out of stock -> checkstock button (text-danger stays, as before) $el.removeClass("out-stock").addClass("checkstock-btn defaultbtn"); } else if ($el.hasClass("no-stock-no-chat")) { // no stock and no chat flag from the platform -> checkstock button $el.removeClass("no-stock-no-chat").addClass("checkstock-btn defaultbtn"); } else if ($el.hasClass("out-stock")) { // other suppliers out of stock $el.addClass("defaultbtn").removeClass("text-danger"); } else { // "0 In Stock" $el.removeClass("in-stock").addClass("checkstock-btn defaultbtn"); } // Button text, plus the hidden due date for non-live products // (live products get their date from the API on click instead) var due = omDueDate(rawDue); var html = "" + omStock.labels.outOfStock + ""; if (due && !omIsLive($el)) { html += ""; } $el.html(html).attr("data-om-state", "check").css("cursor", "pointer"); // one "Chat Now" button per stock button (hidden until needed) if (!$el.next(".out-stock-chat-checkstock").length) { $el.after( "", ); } } /* ---------- convert the platform's stock labels ---------- */ // Runs over every stock label that hasn't been processed yet and applies the rules: // 4+ left alone, 1-3 -> Low Stock, 0 / out-stock / no-stock-no-chat -> Check Here button. // Safe to call repeatedly - processed elements are skipped via data-om-state. function omProcessStock() { $(".live-inventory-v2-shipping") .not("[data-om-state]") .each(function () { var $el = $(this); // product-ajax.js re-runs stockChecker() when a product variant changes and the // platform re-renders the stock label - remove any "Chat Now" button left over // from the previous variant so it doesn't stay on screen $el.siblings(".out-stock-chat-checkstock").remove(); if ($el.hasClass("in-stock")) { var qty = omParseQty($el.text()); if (qty !== null && qty <= 0) { omMakeOutOfStock($el); } else if ( omStock.showLowStock && qty !== null && qty <= omStock.lowStockMax ) { omShowLowStock($el); } else { $el.attr("data-om-state", "done"); // normal in stock - nothing to change } } else if ( $el.hasClass("out-stock") || $el.hasClass("no-stock-no-chat") ) { omMakeOutOfStock($el); } // anything else (e.g. inventory location links, not populated yet) is left alone }); // labels are hidden on load by stockChecker() - show them once processed, // but leave any label we've swapped for the "Chat Now" button hidden $(".live-inventory-v2-shipping").not("[data-om-chat]").fadeIn(); } /* ---------- click: "Out of Stock – Check Here" ---------- */ // Delegated, so it also works for products loaded later by AJAX. // Non-live products: reveal the due date, or show Chat Now if there isn't one. // Live products (supplier_1001): ask the stock API, then show in stock / low / due date / chat. $(document).on( "click", ".live-inventory-v2-shipping[data-om-state='check']", function (e) { e.preventDefault(); var $el = $(this); // Other suppliers: the due date came with the page var $due = $el.find(".stock-due-details"); if ($due.length) { $el.find(".product-stock-click-here").hide(); $due.show(); omDone($el); return; } if (!omIsLive($el)) { omShowChat($el); return; } // supplier_1001: ask the live stock feed $el .attr("data-om-state", "loading") .css("cursor", "default") .html(omStock.labels.loading); $.getJSON(omStock.apiUrl + encodeURIComponent(omSku($el))) .done(function (data) { var stock = parseInt(data && data.Stock, 10) || 0; // negative/blank stock counts as 0 var due = omDueDate(data && data.NextStockDueDate); if ( stock > omStock.lowStockMax || (stock > 0 && !omStock.showLowStock) ) { // plenty in stock - or Low Stock is switched off, so show the real number omShowInStock($el, stock); } else if (stock > 0) { $el.removeClass("text-danger checkstock-btn defaultbtn"); omShowLowStock($el); } else if (due) { omShowDue($el, due); } else { omShowChat($el); } }) .fail(function () { omShowChat($el); // can't check -> offer chat }); }, ); /* ---------- click: "Chat Now" ---------- */ // Opens the Zendesk (Zopim) live chat and sends the product SKU. // chatFunction() also binds .out-stock-chat-checkstock directly - if it has, // we skip so the chat isn't opened twice. $(document).on("click", ".out-stock-chat-checkstock", function () { var events = $._data(this, "events"); if (events && events.click) return; var sku = omSku($(this)); try { if (window.$zopim && $zopim.livechat) { $zopim.livechat.window.show(); if (sku) $zopim.livechat.say(omStock.chatMessage(sku)); } } catch (err) { console.warn("Stock chat could not be opened", err); } }); /* ---------- layout + start-up ---------- */ // Moves the stock labels into position (unchanged from the original script), // then processes them 1 second after the page is ready, once the platform has filled them in. // NOTE: product-ajax.js also calls stockChecker() on product pages (on load and on every // variant change via evoXProductChangeCallback) - keep this function name the same. function stockChecker() { // hide labels until they've been processed, to avoid a flash of the raw text $(".live-inventory-v2-shipping").hide(); // product tiles (listings): move the stock label to just under the product name $(".tileparent .footer").each(function () { var inventoryWrapper = $(this).find(".live-inventory-v2-wrapper"); var productName = $(this).find(".product-name"); inventoryWrapper.insertAfter(productName); inventoryWrapper.addClass("stock-indicators-wrapper list"); }); // product page (mobile + desktop): move the stock label into the product details $(".mobile .product-shopping-actions-row .live-inventory-v2-wrapper") .appendTo(".mobile .productdetails") .addClass("stock-indicators-wrapper stockCheaker"); $(".desktop .product-shopping-actions-row .live-inventory-v2-wrapper") .appendTo(".desktop .productdetails") .addClass("stock-indicators-wrapper stockCheaker"); $(".mobile .stockCheaker").appendTo(".mobile .productdetails .pdp-prod-info"); $(".desktop .stockCheaker").appendTo( ".desktop .productdetails .pdp-prod-info", ); $(document).ready(function () { setTimeout(omProcessStock, 1000); // adjust the delay as needed }); } stockChecker(); // Re-run for products that load later (filters, paging, live inventory refresh). // Debounced so lots of AJAX calls in a row only trigger one pass. var omStockTimer; $(document).ajaxComplete(function () { clearTimeout(omStockTimer); omStockTimer = setTimeout(omProcessStock, 300); });