View our other brands
// Configuration object for centralized settings
const TabbedNavConfig = {
  DESKTOP_BREAKPOINT: 1201,
  DEBOUNCE_DELAY: 250,
  OPEN_FIRST_TAB: true,
  CLOSE_TAB_ON_MOUSEOUT: false
};

// Custom config for tabbed hero containers
const tabbedHeroConfig = {
  ...TabbedNavConfig,
  OPEN_FIRST_TAB: false,
  CLOSE_TAB_ON_MOUSEOUT: true
};

// Function to determine the appropriate config based on container context
function getConfigForContainer(container) {
  // Check if container has closest ancestor with [data-tabbed-hero="true"]
  const heroAncestor = container.getAttribute('data-tabbed-hero') === 'true'   ? container : container.closest('[data-tabbed-dropdown="true"]');
  
  if (heroAncestor) {
    return tabbedHeroConfig;
  }
  
  // Check for specific data-instance attributes (if you still want to support them)
  // const instance = container.getAttribute('data-instance');
  // if (instance === 'two') {
  //   // You can still have specific instance configs if needed
  //   return {
  //     ...TabbedNavConfig,
  //     OPEN_FIRST_TAB: false,
  //     CLOSE_TAB_ON_MOUSEOUT: true
  //   };
  // }
  
  // Default config
  return TabbedNavConfig;
}

class TabbedNavigation {
  constructor(container, config = TabbedNavConfig) {
    this.container = container;
    this.config = config;
    this.activeItem = null;
    this.isDesktop = window.innerWidth >= this.config.DESKTOP_BREAKPOINT;
    this.hasMouseListeners = false;
    this.megamenuParent = null;
    this.megamenuObserver = null;
    
    // Initialize bound handlers - these will be reused across reinitializations
    this.mouseEnterHandler = this.handleMouseEnter.bind(this);
    this.mouseLeaveHandler = this.handleMouseLeave.bind(this);
    this.resizeHandler = this.debounce(this.handleResize.bind(this), this.config.DEBOUNCE_DELAY);
    this.handleMegamenuToggle = this.handleMegamenuToggle.bind(this);
    
    this.init();
  }

  init() {
    // Cache DOM elements fresh each time
    this.navItems = [...this.container.querySelectorAll('.dwc-tabbed-nav-list__li')];
    this.elementsCache = new Map();
    
    this.cacheElements();
    this.setupAccessibility();
    this.setupMegamenuObserver();
    this.bindEvents();

    // Set initial state
    this.updateLayout();
    this.setBackText()

    // Show first item on page load only if desktop, not nested, and OPEN_FIRST_TAB is true
    if (this.navItems.length > 0 && this.isDesktop && this.config.OPEN_FIRST_TAB) {
      setTimeout(() => {
        const firstItem = this.navItems[0];

        // Count how many .dwc-tabbed-nav-container ancestors this.container has
        let count = 0;
        let current = this.container.parentElement;
        while (current) {
          if (current.classList?.contains('dwc-tabbed-nav-container')) {
            count++;
          }
          current = current.parentElement;
        }

        const isNested = count >= 1;

        if (!isNested && this.elementsCache.has(firstItem)) {
          this.showTab(firstItem);
        }
      }, 0);
    }
  }

  // Setup observer for megamenu parent
  setupMegamenuObserver() {
    // Find the .brx-has-megamenu parent
    this.megamenuParent = this.container.closest('.brx-has-megamenu');
    
    if (this.megamenuParent) {
      // Set initial inert state
      this.updateInertState();
      
      // Create a MutationObserver to watch for class changes
      this.megamenuObserver = new MutationObserver((mutations) => {
        mutations.forEach((mutation) => {
          if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
            this.updateInertState();
          }
        });
      });
      
      // Start observing the megamenu parent for class changes
      this.megamenuObserver.observe(this.megamenuParent, {
        attributes: true,
        attributeFilter: ['class']
      });
    }
  }

  // Update inert state based on megamenu open/closed state
  updateInertState() {
    if (!this.megamenuParent) return;
    
    const isOpen = this.megamenuParent.classList.contains('open');
    
    if (isOpen) {
      this.container.removeAttribute('inert');
    } else {
      this.container.setAttribute('inert', '');
    }
  }

  // Handle megamenu toggle (for manual triggering if needed)
  handleMegamenuToggle() {
    this.updateInertState();
  }

  // Method to clean up event listeners and reset state
  cleanup() {
    // Remove all event listeners
    this.container.removeEventListener('click', this.handleClick);
    this.container.removeEventListener('keydown', this.handleKeydown);
    this.container.removeEventListener('focus', this.handleFocus);
    
    // Remove mouse listeners if they exist
    if (this.hasMouseListeners) {
      this.container.removeEventListener('mouseenter', this.mouseEnterHandler, true);
      this.container.removeEventListener('mouseleave', this.mouseLeaveHandler, true);
    }
    
    // Remove resize listener
    window.removeEventListener('resize', this.resizeHandler);
    
    // Clean up megamenu observer
    if (this.megamenuObserver) {
      this.megamenuObserver.disconnect();
      this.megamenuObserver = null;
    }
    
    // Reset state
    this.activeItem = null;
    this.hasMouseListeners = false;
    this.megamenuParent = null;
    
    // Clear caches
    if (this.elementsCache) {
      this.elementsCache.clear();
    }
    this.navItems = [];
    
    // Reset all tab states and accessibility attributes
    const allNavItems = this.container.querySelectorAll('.dwc-tabbed-nav-list__li');
    allNavItems.forEach(item => {
      const button = item.querySelector('.dwc-tabbed-nav-list__li__button') || 
                    item.querySelector('.dwc-tabbed-nav-list__li__btn-txt');
      const content = item.querySelector('.dwc-tabbed-nav-list__li__content');
      
      // Remove classes
      item.classList.remove('active');
      
      // Reset button attributes
      if (button) {
        button.removeAttribute('role');
        button.removeAttribute('aria-controls');
        button.removeAttribute('aria-expanded');
        button.removeAttribute('aria-selected');
        button.removeAttribute('tabindex');
        button.removeAttribute('id');
      }
      
      // Reset content attributes
      if (content) {
        content.removeAttribute('role');
        content.removeAttribute('aria-labelledby');
        content.removeAttribute('id');
        content.removeAttribute('aria-hidden');
        content.style.maxHeight = '';
      }
      
      // Remove data attributes
      item.removeAttribute('data-tab-index');
    });
    
    // Reset nav list role
    const navList = this.container.querySelector('.dwc-tabbed-nav-list');
    if (navList) {
      navList.removeAttribute('role');
    }
    
    // Remove inert attribute
    this.container.removeAttribute('inert');
  }

  // Public method to reinitialize the navigation
  reinitialize() {
    this.cleanup();
    this.isDesktop = window.innerWidth >= this.config.DESKTOP_BREAKPOINT; // Use config breakpoint
    this.init();
  }

  // Cache DOM elements to avoid repeated queries
  cacheElements() {
    const uniqueIdBase = `tab-${Date.now()}`;
    
    this.navItems.forEach((item, index) => {
      const button = item.querySelector('.dwc-tabbed-nav-list__li__button') || 
                    item.querySelector('.dwc-tabbed-nav-list__li__btn-txt');
      const content = item.querySelector('.dwc-tabbed-nav-list__li__content');
      
      this.elementsCache.set(item, {
        button,
        content,
        uniqueId: `${uniqueIdBase}-${index}`,
        index
      });
    });
  }

  setupAccessibility() {
    const navList = this.container.querySelector('.dwc-tabbed-nav-list');
    if (!navList) return;
    
    navList.setAttribute('role', 'tablist');
    
    for (const [item, cache] of this.elementsCache) {
      const { button, content, uniqueId, index } = cache;
      
      if (button) {
        button.setAttribute('role', 'tab');
        button.setAttribute('aria-controls', `${uniqueId}-panel`);
        button.setAttribute('aria-expanded', 'false');
        button.setAttribute('aria-selected', 'false');
        button.setAttribute('tabindex', '0');
        button.setAttribute('id', `${uniqueId}-tab`);
      }
      
      if (content) {
        content.setAttribute('role', 'tabpanel');
        content.setAttribute('aria-labelledby', `${uniqueId}-tab`);
        content.setAttribute('id', `${uniqueId}-panel`);
        content.setAttribute('aria-hidden', 'true');
      }
      
      item.setAttribute('data-tab-index', index);
    }
  }

  bindEvents() {
    // Store bound handlers for cleanup
    this.handleClick = this.handleClick.bind(this);
    this.handleKeydown = this.handleKeydown.bind(this);
    this.handleFocus = this.handleFocus.bind(this);
    
    // Single event delegation for better performance
    this.container.addEventListener('click', this.handleClick);
    this.container.addEventListener('keydown', this.handleKeydown);
    this.container.addEventListener('focus', this.handleFocus, true);
    
    // Passive resize listener for better performance
    window.addEventListener('resize', this.resizeHandler, { passive: true });
    
    this.updateMouseListeners();
  }

  updateMouseListeners() {
    if (this.isDesktop && !this.hasMouseListeners) {
      this.container.addEventListener('mouseenter', this.mouseEnterHandler, true);
      this.container.addEventListener('mouseleave', this.mouseLeaveHandler, true);
      this.hasMouseListeners = true;
    } else if (!this.isDesktop && this.hasMouseListeners) {
      this.container.removeEventListener('mouseenter', this.mouseEnterHandler, true);
      this.container.removeEventListener('mouseleave', this.mouseLeaveHandler, true);
      this.hasMouseListeners = false;
    }
  }

  // Helper method to get sibling nav items (nav items in the same container level)
  getSiblingNavItems(navItem) {
    // Find the direct parent container of this nav item
    const navItemContainer = navItem.closest('.dwc-tabbed-nav-list');
    if (!navItemContainer) return [];
    
    // Get all nav items that are direct children of the same container
    const siblingItems = [...navItemContainer.querySelectorAll('.dwc-tabbed-nav-list__li')];
    
    // Filter to only include items that are siblings (not the navItem itself)
    // and check if they belong to any TabbedNavigation instance
    return siblingItems.filter(item => item !== navItem);
  }

  handleFocus(e) {
    if (!this.isDesktop) return;

    const button = e.target.closest('[role="tab"]');
    if (!button) return;
    
    const navItem = button.closest('.dwc-tabbed-nav-list__li');
    if (!navItem || !this.elementsCache.has(navItem)) return;
    
    this.handleTabInteraction(navItem, 'focus');
  }

  handleClick(e) {
    const navItem = e.target.closest('.dwc-tabbed-nav-list__li');
    if (!navItem || !this.elementsCache.has(navItem)) return;
    
    const button = e.target.closest('.dwc-tabbed-nav-list__li__button, .dwc-tabbed-nav-list__li__btn-txt');
    if (!button) return;
    
    e.preventDefault();
    this.handleTabInteraction(navItem, 'click');
  }

  handleKeydown(e) {
    const button = e.target.closest('[role="tab"]');
    if (!button) return;
    
    const navItem = button.closest('.dwc-tabbed-nav-list__li');
    if (!navItem || !this.elementsCache.has(navItem)) return;
    
    const { index: currentIndex } = this.elementsCache.get(navItem);
    
    switch (e.key) {
      case 'Enter':
      case ' ':
        e.preventDefault();
        this.handleTabInteraction(navItem, 'keyboard');
        break;
      case 'ArrowRight':
      case 'ArrowDown':
        e.preventDefault();
        this.focusNextTab(currentIndex);
        break;
      case 'ArrowLeft':
      case 'ArrowUp':
        e.preventDefault();
        this.focusPrevTab(currentIndex);
        break;
      case 'Home':
        e.preventDefault();
        this.focusTab(0);
        break;
      case 'End':
        e.preventDefault();
        this.focusTab(this.navItems.length - 1);
        break;
    }
  }
  
  handleMouseEnter(e) {
    if (!this.isDesktop) return;
    
    const navItem = e.target.closest('.dwc-tabbed-nav-list__li') || 
                   (e.target.matches('.dwc-tabbed-nav-list__li') ? e.target : null);
    if (!navItem || !this.elementsCache.has(navItem)) return;
    
    this.handleTabInteraction(navItem, 'hover');
  }

  handleMouseLeave(e) {
    if (!this.isDesktop) return;
    
    // Only close tabs if CLOSE_TAB_ON_MOUSEOUT is enabled
    if (this.config.CLOSE_TAB_ON_MOUSEOUT) {
      // Check if mouse is leaving the entire container
      const rect = this.container.getBoundingClientRect();
      const { clientX, clientY } = e;
      
      // Check if mouse position is outside the container bounds
      const isOutsideContainer = (
        clientX < rect.left || 
        clientX > rect.right || 
        clientY < rect.top || 
        clientY > rect.bottom
      );
      
      if (isOutsideContainer) {
        this.hideAllTabs();
      }
    }
    // If CLOSE_TAB_ON_MOUSEOUT is false, keep content visible (original behavior)
  }

  // Unified method to handle all tab interactions with nesting support
  handleTabInteraction(navItem, interactionType) {
    if (!this.elementsCache.has(navItem)) return;
    
    if (this.isDesktop) {
      // Simply hide siblings and show the target - works for all scenarios
      this.hideSiblingTabs(navItem);
      this.showTab(navItem);
    } else {
      // Mobile accordion behavior
      if (interactionType === 'click' || interactionType === 'keyboard') {
        this.toggleTabMobile(navItem);
      }
    }
  }

  // Hide only sibling tabs (same container level)
  hideSiblingTabs(navItem) {
   // console.log('Nav Item: ', navItem)
    const siblings = this.getSiblingNavItems(navItem);
    
    siblings.forEach(sibling => {
      
         //console.log('sibling: ', sibling)
      // Only hide siblings that have the active class to avoid affecting other instances
      if (sibling.classList.contains('active')) {
        this.hideSiblingTab(sibling);
      }
    });
  }

  // Hide a sibling tab (used when we know it's a sibling from another instance)
  hideSiblingTab(navItem) {
    const button = navItem.querySelector('.dwc-tabbed-nav-list__li__button') || 
                  navItem.querySelector('.dwc-tabbed-nav-list__li__btn-txt');
    const content = navItem.querySelector('.dwc-tabbed-nav-list__li__content');
    
    if (content && button) {
      content.setAttribute('aria-hidden', 'true');
      button.setAttribute('aria-expanded', 'false');
      button.setAttribute('aria-selected', 'false');
      navItem.classList.remove('active');
      
      // Also handle mobile accordion collapse
      if (!this.isDesktop) {
        content.style.maxHeight = '0';
      }
    }
  }

  // Mobile-specific toggle behavior with proper nesting support
  toggleTabMobile(navItem) {
    if (!this.elementsCache.has(navItem)) return;
    
    const isActive = navItem.classList.contains('active');
    
    if (isActive) {
      this.collapseTab(navItem);
    } else {
      // Close sibling tabs and open this one
      this.hideSiblingTabs(navItem);
      this.expandTab(navItem);
    }
  }

  toggleTab(navItem) {
    if (!this.elementsCache.has(navItem)) return;
    
    if (this.isDesktop) {
      this.handleTabInteraction(navItem, 'click');
    } else {
      this.toggleTabMobile(navItem);
    }
  }

  showTab(navItem) {
    if (!this.elementsCache.has(navItem)) return;
    
    const { button, content } = this.elementsCache.get(navItem);
    
    if (content && button) {
      content.setAttribute('aria-hidden', 'false');
      button.setAttribute('aria-expanded', 'true');
      button.setAttribute('aria-selected', 'true');
      navItem.classList.add('active');
    }
    
    this.setActiveTab(navItem);
  }

  hideTab(navItem) {
    if (!this.elementsCache.has(navItem)) return;
    
    const { button, content } = this.elementsCache.get(navItem);
    
    if (content && button) {
      content.setAttribute('aria-hidden', 'true');
      button.setAttribute('aria-expanded', 'false');
      button.setAttribute('aria-selected', 'false');
      navItem.classList.remove('active');
    }
    
    // Only clear activeItem if it belongs to THIS navigation instance
    // and it's the exact item being hidden
    if (this.activeItem === navItem && this.navItems.includes(navItem)) {
      this.activeItem = null;
    }
  }

  expandTab(navItem) {
    if (!this.elementsCache.has(navItem)) return;
    
    // Close sibling tabs
    this.hideSiblingTabs(navItem);
    
    this.showTab(navItem);
    
    // Smooth height animation
    const { content } = this.elementsCache.get(navItem);
    if (content) {
      const height = content.scrollHeight;
      content.style.maxHeight = `${height}px`;
    }
  }

  collapseTab(navItem) {
    if (!this.elementsCache.has(navItem)) return;
    
    this.hideTab(navItem);
    
    const { content } = this.elementsCache.get(navItem);
    if (content) {
      content.style.maxHeight = '0';
    }
  }

  hideAllTabs() {
    this.navItems.forEach(item => this.hideTab(item));
  }

  setActiveTab(navItem) {
    // Only update aria-selected for tabs in THIS navigation instance
    this.navItems.forEach(item => {
      const cache = this.elementsCache.get(item);
      if (cache?.button) {
        cache.button.setAttribute('aria-selected', item === navItem ? 'true' : 'false');
      }
    });
    
    // Only set activeItem if this navItem belongs to THIS navigation instance
    if (this.navItems.includes(navItem)) {
      this.activeItem = navItem;
    }
  }

  focusTab(index) {
    if (index >= 0 && index < this.navItems.length) {
      const cache = this.elementsCache.get(this.navItems[index]);
      if (cache?.button) {
        cache.button.focus();
      }
    }
  }

  focusNextTab(currentIndex) {
    const nextIndex = (currentIndex + 1) % this.navItems.length;
    this.focusTab(nextIndex);
  }

  focusPrevTab(currentIndex) {
    const prevIndex = currentIndex === 0 ? this.navItems.length - 1 : currentIndex - 1;
    this.focusTab(prevIndex);
  }

  handleResize() {
    const wasDesktop = this.isDesktop;
    this.isDesktop = window.innerWidth >= this.config.DESKTOP_BREAKPOINT; // Use config breakpoint
    
    if (wasDesktop !== this.isDesktop) {
      this.updateMouseListeners();
      this.updateLayout();
    }
  }

  updateLayout() {
    const currentlyActive = this.activeItem;

    // Reset all states
    this.hideAllTabs();

    // Clear inline styles
    this.navItems.forEach(item => {
      const cache = this.elementsCache.get(item);
      if (cache?.content) {
        cache.content.style.maxHeight = '';
      }
    });

    if (this.isDesktop) {
      // Determine if this tab instance is nested
      let count = 0;
      let current = this.container.parentElement;
      while (current) {
        if (current.classList?.contains('dwc-tabbed-nav-container')) {
          count++;
        }
        current = current.parentElement;
      }
      const isNested = count >= 1;

      if (!isNested && this.config.OPEN_FIRST_TAB) {
        if (currentlyActive) {
          this.showTab(currentlyActive);
        } else if (this.navItems.length > 0) {
          this.showTab(this.navItems[0]);
        }
      }
    }
  }

  setBackText(){
    let tabElement = document.querySelector('.dwc-tabbed-nav-container');
    let tabBackText = tabElement.getAttribute('data-back-text')
    document.querySelectorAll('.dwc-tabbed-nav-list__li__button').forEach(tabBtn => {
        const textContent = tabBtn.textContent.trim();
        const tabIcon = tabBtn.querySelector('.dwc-tabbed-nav-list__li__arrow-icon');
        if (tabIcon) {
            tabIcon.setAttribute('data-text', textContent);
            tabIcon.setAttribute('data-back-text', tabBackText);
        }
    });
  }

  // Simple debounce for resize
  debounce(func, wait) {
    let timeout;
    return function executedFunction(...args) {
      const later = () => {
        clearTimeout(timeout);
        func(...args);
      };
      clearTimeout(timeout);
      timeout = setTimeout(later, wait);
    };
  }

  // Public method to destroy the instance completely
  destroy() {
    this.cleanup();
    // Additional cleanup can be added here if needed
  }

  // Public method to manually update inert state (if needed)
  updateInert() {
    this.updateInertState();
  }
}

// Simplified initialization on DOM ready
document.addEventListener('DOMContentLoaded', function() {
  const tabbedNavContainers = document.querySelectorAll('.dwc-tabbed-nav-container');
  
  tabbedNavContainers.forEach(container => {
    // Skip if this container is nested inside another dwc-tabbed-nav-container
    const parentNavContainer = container.parentElement.closest('.dwc-tabbed-nav-container');
    if (parentNavContainer) {
      return; // Skip nested containers
    }
    
    // Get the appropriate config for this container
    const config = getConfigForContainer(container);
    
    // Initialize with the determined config
    new TabbedNavigation(container, config);
  });
});

// For dynamic content - Updated version
window.initTabbedNav = function(container, customConfig = null) {
  if (container && container.classList.contains('dwc-tabbed-nav-container')) {
    // Check if this is a nested container
    const parentNavContainer = container.parentElement.closest('.dwc-tabbed-nav-container');
    if (parentNavContainer) {
      return null; // Don't initialize nested containers
    }
    
    // Use custom config if provided, otherwise determine config automatically
    const config = customConfig || getConfigForContainer(container);
    return new TabbedNavigation(container, config);
  }
  return null;
};

/*
// TO REINITIALIZE USE THIS
// Create an instance of TabbedNavigation
const tabbedNav = new TabbedNavigation(document.querySelector('.dwc-tabbed-nav-container'));

// Call reinitialize on the instance
tabbedNav.reinitialize();

// TO USE WITH CUSTOM CONFIG
const customConfig = {
  DESKTOP_BREAKPOINT: 768,
  DEBOUNCE_DELAY: 300,
  OPEN_FIRST_TAB: false,  // This will prevent first tab from opening automatically
  CLOSE_TAB_ON_MOUSEOUT: true  // This will close tabs when mouse leaves container
};
const tabbedNavCustom = new TabbedNavigation(document.querySelector('.dwc-tabbed-nav-container'), customConfig);

// TO UPDATE ON URL CHANGE
function onUrlChange() {
    setTimeout(function () {
const tabbedNav = new TabbedNavigation(document.querySelector('.dwc-tabbed-nav-container'));
tabbedNav.reinitialize();

    }, 1500); // Delay execution by 2 seconds
}

// Listen for popstate event (history navigation)
window.addEventListener('popstate', onUrlChange);

// Listen for hashchange event (URL hash change)
window.addEventListener('hashchange', onUrlChange);

// Handle pushState and replaceState
(function (history) {
    const pushState = history.pushState;
    const replaceState = history.replaceState;

    history.pushState = function (state) {
        if (typeof history.onpushstate == "function") {
            history.onpushstate({ state: state });
        }
        const result = pushState.apply(history, arguments);
        onUrlChange(); // Call the function when pushState is used
        return result;
    };

    history.replaceState = function (state) {
        if (typeof history.onreplacestate == "function") {
            history.onreplacestate({ state: state });
        }
        const result = replaceState.apply(history, arguments);
        onUrlChange(); // Call the function when replaceState is used
        return result;
    };
})(window.history);

*/

console.log('%c<Jamie Sheehan Web Design Mega Menu Pro Tabbed Navigation v1.0>', 'color: #b388eb');

OAS v6 Announcement

September 10, 2024

In this post...

All Articles

Maron Systems Announcement: OAS is Upgrading

Maron Systems is upgrading OAS infrastructure to offer the latest industry-forward technology. Strategically, this upgrade aims to provide users with a greater, competitive edge in the fast-changing digital environment by delivering security enhancements, improved system performance, and maintaining compliance with industry standards.

Migrating to the newest version of OAS has been a multi-year investment with the end-goal of setting up our users for long-term, sustainable growth. Our roll-out model has been carefully considered to facilitate a gradual transition from older versions of OAS to newer features and technologies in key functional areas while minimizing disruption to business operations.

Here are some of the benefits you can expect to see from OAS v6:

  • Modernized interface while carrying forward the best of legacy OAS versions
  • Compliant with the latest security standards allowing integration with 3rd party authentication providers
  • Reduced licensing costs for users with Oracle Forms & WebLogic becoming obsolete with the new architecture
  • System infrastructure is significantly simplified, no longer requiring installation of Java Runtime Environments on Client or Server, and significantly reducing administrative burden
  • Versatility across multiple browsers, supported on mobile and tablet devices

For more information, please see the OAS v6 Product Roadmap below, highlighting the exciting releases that are completed and in progress.

Maron Systems is excited to bring these advancements and are committed to continually enhancing OAS v6 to ensure you’re trading commodities and not your time. Stay tuned for further updates and features as we progress through our development phases.

Unable to read published form data

Additional Benefits

  • Accelerated financial results for real-time decision making
  • Automated hydrocarbon pricing based on quality inspection results for accountants
  • Incorporating historical oil losses per location into transactions to reduce/eliminate oil loss claims
  • Identify differences between test method nominated versus method performed by inspection companies
  • Communicate real-time test results as they are available for prompt trading opportunities
  • Inspection cost savings from use of Navarik Inspection Price Estimator
  • All data centralised with the ability to extract key information for data analytics