backups/calcbuilder-20260420_0859/CalcBuilder.js

Code: DEV-189C528C Size: 22.2 KB Lines: 510 Path: /home/prodconfig.wenesthosting.com/dev.solargroup.wenest.se/backups/calcbuilder-20260420_0859/CalcBuilder.js

Task / Comment

Open report form
(function(){
  var state = {
    categories: [],
    configurators: [],
    current: null
  };

  function esc(value){
    return String(value == null ? '' : value)
      .replace(/&/g, '&')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;');
  }

  function status(message, type){
    var box = document.getElementById('calcBuilderStatus');
    if(!box) return;
    box.textContent = message || '';
    box.className = 'config-builder-status' + (type ? ' is-' + type : '');
  }

  function blankConfigurator(){
    return {
      id: null,
      title: '',
      version: '1.0.0',
      notes: '',
      category: '',
      builderMode: 'simple',
      headPreset: 'current-default',
      footerMode: 'none',
      sidebarModule: 'price-summary',
      baseSource: 'product-catalog',
      sidebarPosition: 'right',
      regions: deriveRegions({
        builderMode: 'simple',
        headPreset: 'current-default',
        sidebarModule: 'price-summary'
      }),
      blocks: []
    };
  }

  var moduleLabels = {
    'customer-block': 'Kundinformation',
    'prospect-images': 'Prospektbilder',
    'calc-title': 'Kalkylrubrik',
    'category-grid': 'Kategorigrid',
    'config-blocks': 'Konfiguratorblock',
    'overview-block': 'Översikt',
    'price-summary': 'Prissammanställning',
    'savings-box': 'Besparing',
    'actions-box': 'Åtgärder'
  };

  function deriveRegions(source){
    return {
      head: (source.headPreset || 'current-default') === 'current-default'
        ? ['customer-block', 'prospect-images', 'calc-title']
        : [],
      content: ['config-blocks'],
      sidebar: (source.sidebarModule || 'price-summary') === 'none'
        ? []
        : [source.sidebarModule || 'price-summary']
    };
  }

  function normalizeBlock(block, index){
    block = block || {};
    return {
      id: block.id || 'block-' + (index + 1),
      type: block.type || 'select',
      field: block.field || '',
      label: block.label || '',
      description: block.description || '',
      placeholder: block.placeholder || '',
      unit: block.unit || '',
      min: block.min != null ? block.min : '',
      max: block.max != null ? block.max : '',
      step: block.step != null ? block.step : '',
      defaultValue: block.defaultValue != null ? block.defaultValue : '',
      visibleField: block.visibleIf && block.visibleIf.field ? block.visibleIf.field : '',
      visibleOperator: block.visibleIf && block.visibleIf.notEquals != null ? 'notEquals' : 'equals',
      visibleValue: block.visibleIf ? (block.visibleIf.equals != null ? block.visibleIf.equals : (block.visibleIf.notEquals != null ? block.visibleIf.notEquals : '')) : '',
      optionsText: Array.isArray(block.options)
        ? block.options.map(function(option){
            return [option.value || '', option.label || '', option.description || ''].join('|').replace(/\|+$/,'');
          }).join('\n')
        : ''
    };
  }

  function parseOptions(text){
    return String(text || '').split(/\n+/).map(function(line){
      line = line.trim();
      if(!line) return null;
      var parts = line.split('|');
      return {
        value: (parts[0] || '').trim(),
        label: (parts[1] || parts[0] || '').trim(),
        description: (parts[2] || '').trim()
      };
    }).filter(Boolean);
  }

  function serializeBlock(raw){
    var block = {
      id: raw.id || raw.field || ('block-' + Math.random().toString(36).slice(2, 8)),
      type: raw.type,
      field: raw.field,
      label: raw.label,
      description: raw.description
    };
    if(raw.placeholder) block.placeholder = raw.placeholder;
    if(raw.unit) block.unit = raw.unit;
    if(raw.min !== '') block.min = Number(raw.min);
    if(raw.max !== '') block.max = Number(raw.max);
    if(raw.step !== '') block.step = Number(raw.step);
    if(raw.defaultValue !== '') block.defaultValue = raw.type === 'toggle'
      ? (String(raw.defaultValue) === 'true')
      : (/^-?\d+(\.\d+)?$/.test(String(raw.defaultValue)) ? Number(raw.defaultValue) : raw.defaultValue);
    if(raw.type === 'cards' || raw.type === 'select') block.options = parseOptions(raw.optionsText);
    if(raw.visibleField && raw.visibleValue !== ''){
      block.visibleIf = { field: raw.visibleField };
      if(raw.visibleOperator === 'notEquals') block.visibleIf.notEquals = raw.visibleValue;
      else block.visibleIf.equals = raw.visibleValue;
    }
    return block;
  }

  function getCurrentBlocks(){
    return Array.from(document.querySelectorAll('.config-block-card')).map(function(card){
      return {
        id: card.querySelector('[data-key="id"]').value.trim(),
        type: card.querySelector('[data-key="type"]').value,
        field: card.querySelector('[data-key="field"]').value.trim(),
        label: card.querySelector('[data-key="label"]').value.trim(),
        description: card.querySelector('[data-key="description"]').value.trim(),
        placeholder: card.querySelector('[data-key="placeholder"]').value.trim(),
        unit: card.querySelector('[data-key="unit"]').value.trim(),
        min: card.querySelector('[data-key="min"]').value.trim(),
        max: card.querySelector('[data-key="max"]').value.trim(),
        step: card.querySelector('[data-key="step"]').value.trim(),
        defaultValue: card.querySelector('[data-key="defaultValue"]').value.trim(),
        visibleField: card.querySelector('[data-key="visibleField"]').value.trim(),
        visibleOperator: card.querySelector('[data-key="visibleOperator"]').value,
        visibleValue: card.querySelector('[data-key="visibleValue"]').value.trim(),
        optionsText: card.querySelector('[data-key="optionsText"]').value
      };
    });
  }

  function syncCurrentFromForm(){
    if(!state.current) state.current = blankConfigurator();
    state.current.title = document.getElementById('calcBuilderTitle').value.trim();
    state.current.category = document.getElementById('calcBuilderCategory').value;
    state.current.version = document.getElementById('calcBuilderVersion').value.trim() || '1.0.0';
    state.current.notes = document.getElementById('calcBuilderNotes').value.trim();
    state.current.builderMode = document.getElementById('calcBuilderMode').value || 'simple';
    state.current.headPreset = document.getElementById('calcBuilderHeadPreset').value || 'current-default';
    state.current.sidebarModule = document.getElementById('calcBuilderSidebarModule').value || 'price-summary';
    state.current.footerMode = document.getElementById('calcBuilderFooterMode').value || 'none';
    state.current.baseSource = state.current.builderMode === 'advanced' ? 'custom-schema' : 'product-catalog';
    state.current.sidebarPosition = document.getElementById('calcBuilderSidebarPosition').value || 'right';
    state.current.regions = deriveRegions(state.current);
    state.current.blocks = getCurrentBlocks();
  }

  function renderCategoryOptions(){
    var select = document.getElementById('calcBuilderCategory');
    if(!select) return;
    select.innerHTML = '<option value="">Välj kategori...</option>' + state.categories.map(function(cat){
      return '<option value="'+esc(cat.id)+'">'+esc(cat.label || cat.id)+'</option>';
    }).join('');
  }

  function applyCurrentToForm(){
    var current = state.current || blankConfigurator();
    document.getElementById('calcBuilderTitle').value = current.title || '';
    document.getElementById('calcBuilderCategory').value = current.category || '';
    document.getElementById('calcBuilderVersion').value = current.version || '1.0.0';
    document.getElementById('calcBuilderNotes').value = current.notes || '';
    document.getElementById('calcBuilderMode').value = current.builderMode || (current.baseSource === 'custom-schema' ? 'advanced' : 'simple');
    document.getElementById('calcBuilderHeadPreset').value = current.headPreset || 'current-default';
    document.getElementById('calcBuilderSidebarModule').value = current.sidebarModule || 'price-summary';
    document.getElementById('calcBuilderFooterMode').value = current.footerMode || 'none';
    document.getElementById('calcBuilderSidebarPosition').value = current.sidebarPosition || 'right';
    toggleAdvancedMode();
    renderBlocks();
    refreshPreview();
  }

  function toggleAdvancedMode(){
    var currentMode = (document.getElementById('calcBuilderMode').value || 'simple');
    var advancedWrap = document.getElementById('calcBuilderAdvancedWrap');
    var advancedBits = document.querySelectorAll('.config-builder-advanced-only');
    if(advancedWrap) advancedWrap.classList.toggle('is-hidden', currentMode !== 'advanced');
    advancedBits.forEach(function(el){
      el.classList.toggle('is-hidden', currentMode !== 'advanced');
    });
  }

  function renderList(){
    var box = document.getElementById('calcBuilderList');
    if(!box) return;
    var term = (document.getElementById('calcBuilderSearch').value || '').toLowerCase().trim();
    var items = state.configurators.filter(function(item){
      var hay = [item.title, item.category_label, item.category].join(' ').toLowerCase();
      return !term || hay.indexOf(term) !== -1;
    });
    if(!items.length){
      box.innerHTML = '<div class="config-builder-empty">Inga konfiguratorer ännu.</div>';
      return;
    }
    box.innerHTML = items.map(function(item){
      var active = state.current && state.current.category === item.category ? ' is-active' : '';
      return '<div class="config-builder-item'+active+'">'
        + '<button type="button" class="config-builder-item-main" onclick="CalcBuilder.select(\''+esc(item.category)+'\')">'
        + '<strong>'+esc(item.title || item.category_label || item.category)+'</strong>'
        + '<span>'+esc(item.category_label || item.category)+'</span>'
        + '<span>'+esc((item.builder_mode === 'advanced' ? 'Avancerad' : 'Enkel') + ' • v' + (item.version || '1.0.0'))+'</span>'
        + '</button>'
        + '<button type="button" class="config-builder-item-preview" onclick="CalcBuilder.previewSaved(\''+esc(item.category)+'\')">Öppna sida</button>'
        + '</div>';
    }).join('');
  }

  function field(label, key, value){
    return '<label><span>'+label+'</span><input class="config-builder-input" data-key="'+key+'" value="'+esc(value)+'" oninput="CalcBuilder.refreshPreview()"></label>';
  }

  function selectField(label, key, value, options){
    return '<label><span>'+label+'</span><select class="config-builder-input" data-key="'+key+'" onchange="CalcBuilder.renderBlocks()">'
      + options.map(function(opt){
        return '<option value="'+esc(opt.v)+'"' + (String(value) === String(opt.v) ? ' selected' : '') + '>' + esc(opt.l) + '</option>';
      }).join('')
      + '</select></label>';
  }

  function textareaField(label, key, value, hidden){
    return '<label class="is-wide"' + (hidden ? ' style="display:none"' : '') + '><span>'+label+'</span><textarea class="config-builder-textarea" data-key="'+key+'" oninput="CalcBuilder.refreshPreview()">'+esc(value)+'</textarea></label>';
  }

  function blockCard(block, index){
    var showOptions = block.type === 'cards' || block.type === 'select';
    return '<div class="config-block-card" data-index="'+index+'">'
      + '<div class="config-block-head">'
      +   '<div class="config-block-title"><span class="config-block-index">'+(index + 1)+'</span><span>'+esc(block.label || block.field || ('Block ' + (index + 1)))+'</span></div>'
      +   '<div class="config-block-actions">'
      +     '<button type="button" class="config-builder-btn" onclick="CalcBuilder.moveBlock('+index+', -1)">Upp</button>'
      +     '<button type="button" class="config-builder-btn" onclick="CalcBuilder.moveBlock('+index+', 1)">Ner</button>'
      +     '<button type="button" class="config-builder-btn" onclick="CalcBuilder.removeBlock('+index+')">Ta bort</button>'
      +   '</div>'
      + '</div>'
      + '<div class="config-block-fields">'
      + field('ID', 'id', block.id)
      + field('State-fält', 'field', block.field)
      + field('Titel', 'label', block.label)
      + selectField('Typ', 'type', block.type, [{v:'cards',l:'Cards'},{v:'select',l:'Select'},{v:'number',l:'Number'},{v:'toggle',l:'Toggle'},{v:'range',l:'Range'}])
      + textareaField('Beskrivning', 'description', block.description, false)
      + field('Placeholder', 'placeholder', block.placeholder)
      + field('Enhet', 'unit', block.unit)
      + field('Default', 'defaultValue', block.defaultValue)
      + field('Min', 'min', block.min)
      + field('Max', 'max', block.max)
      + field('Step', 'step', block.step)
      + field('Synlig om fält', 'visibleField', block.visibleField)
      + selectField('Villkor', 'visibleOperator', block.visibleOperator, [{v:'equals',l:'='},{v:'notEquals',l:'!='}])
      + field('Villkorsvärde', 'visibleValue', block.visibleValue)
      + textareaField('Val (value|label|description)', 'optionsText', block.optionsText, !showOptions)
      + '</div>'
      + '</div>';
  }

  function renderBlocks(){
    var box = document.getElementById('calcBuilderBlocks');
    if(!box) return;
    var blocks = (state.current && state.current.blocks) || [];
    if(!blocks.length){
      box.innerHTML = '<div class="config-builder-empty">Lägg till första blocket för att börja bygga konfiguratorn.</div>';
      refreshPreview();
      return;
    }
    box.innerHTML = blocks.map(function(block, index){
      return blockCard(normalizeBlock(block, index), index);
    }).join('');
  }

  function refreshPreview(){
    syncCurrentFromForm();
    toggleAdvancedMode();
    var preview = document.getElementById('calcBuilderPreview');
    if(!preview) return;
    var schema = { blocks: state.current.blocks.map(serializeBlock) };
    var regions = deriveRegions(state.current);
    var headModules = (regions.head || []).map(function(item){
      return '<span class="config-preview-module">' + esc(moduleLabels[item] || item) + '</span>';
    }).join('');
    var contentModules = (regions.content || []).map(function(item){
      return '<span class="config-preview-module">' + esc(moduleLabels[item] || item) + '</span>';
    }).join('');
    var sidebarModules = (regions.sidebar || []).map(function(item){
      return '<span class="config-preview-module">' + esc(moduleLabels[item] || item) + '</span>';
    }).join('');
    var configBlocks = state.current.builderMode === 'advanced'
      ? (schema.blocks.length && window.CalcBlocks ? window.CalcBlocks.render(schema.blocks, {}) : '<div class="config-builder-empty">Inga block ännu.</div>')
      : '<div class="config-builder-empty">Enkel kalkylator använder produktkatalogens färdiga kalkyl för vald kategori.</div>';
    var columnsClass = state.current.sidebarPosition === 'left' ? 'config-preview-columns is-left' : 'config-preview-columns';
    preview.innerHTML = ''
      + '<div class="config-preview-layout">'
      +   '<div class="config-preview-head">'
      +     '<div class="config-preview-title"><span>Head</span><span>' + esc(state.current.builderMode === 'advanced' ? 'Avancerad kalkylator' : 'Enkel kalkylator') + '</span></div>'
      +     '<div class="config-preview-module-list">' + (headModules || '<div class="config-builder-empty">Inga head-moduler.</div>') + '</div>'
      +   '</div>'
      +   '<div class="' + columnsClass + '">'
      +     '<div class="config-preview-content">'
      +       '<div class="config-preview-title"><span>Content</span><span>Kategori: ' + esc(state.current.category || 'Ingen vald') + '</span></div>'
      +       '<div class="config-preview-module-list">' + (contentModules || '<div class="config-builder-empty">Inga content-moduler.</div>') + '</div>'
      +       '<div>' + configBlocks + '</div>'
      +     '</div>'
      +     '<div class="config-preview-sidebar">'
      +       '<div class="config-preview-title"><span>Sidebar</span><span>' + esc(state.current.sidebarPosition === 'left' ? 'Vänster' : 'Höger') + '</span></div>'
      +       '<div class="config-preview-module-list">' + (sidebarModules || '<div class="config-builder-empty">Inga sidebar-moduler.</div>') + '</div>'
      +     '</div>'
      +   '</div>'
      +   '<div class="config-preview-head">'
      +     '<div class="config-preview-title"><span>Footer</span><span>' + esc(state.current.footerMode === 'default' ? 'Standard footer' : 'Ingen footer') + '</span></div>'
      +   '</div>'
      + '</div>';
  }

  async function loadCategories(){
    var res = await fetch('/api/categories.php?all=1');
    var data = await res.json();
    state.categories = data.categories || [];
    renderCategoryOptions();
  }

  async function loadConfigurators(){
    var res = await fetch('/api/calc_builder.php');
    var data = await res.json();
    state.configurators = data.configurators || [];
    renderList();
  }

  async function previewSaved(categoryId){
    await select(categoryId);
    openPreview();
  }

  async function select(categoryId){
    if(!categoryId){
      createNew();
      return;
    }
    var res = await fetch('/api/calc_builder.php?category=' + encodeURIComponent(categoryId));
    var data = await res.json();
    if(!data.success || !data.configurator){
      status('Kunde inte läsa konfiguratorn.', 'error');
      return;
    }
    state.current = {
      id: data.configurator.id || null,
      title: data.configurator.title || '',
      version: data.configurator.version || '1.0.0',
      notes: data.configurator.notes || '',
      category: data.configurator.category || '',
      builderMode: data.configurator.builder_mode || (data.configurator.base_source === 'custom-schema' ? 'advanced' : 'simple'),
      headPreset: data.configurator.head_preset || 'current-default',
      footerMode: data.configurator.footer_mode || 'none',
      sidebarModule: data.configurator.sidebar_module || ((data.configurator.regions && data.configurator.regions.sidebar && data.configurator.regions.sidebar[0]) || 'price-summary'),
      baseSource: data.configurator.base_source || 'product-catalog',
      sidebarPosition: data.configurator.sidebar_position || 'right',
      regions: data.configurator.regions || deriveRegions(data.configurator),
      blocks: (data.configurator.blocks || []).map(normalizeBlock)
    };
    applyCurrentToForm();
    renderList();
    status('Konfigurator laddad.', 'success');
  }

  function createNew(){
    state.current = blankConfigurator();
    applyCurrentToForm();
    renderList();
    status('Ny konfigurator klar att byggas.', 'success');
  }

  function addBlock(){
    if(!state.current) createNew();
    syncCurrentFromForm();
    state.current.blocks.push(normalizeBlock({}, state.current.blocks.length));
    renderBlocks();
    refreshPreview();
  }

  function removeBlock(index){
    syncCurrentFromForm();
    state.current.blocks.splice(index, 1);
    renderBlocks();
    refreshPreview();
  }

  function moveBlock(index, direction){
    syncCurrentFromForm();
    var next = index + direction;
    if(next < 0 || next >= state.current.blocks.length) return;
    var temp = state.current.blocks[index];
    state.current.blocks[index] = state.current.blocks[next];
    state.current.blocks[next] = temp;
    renderBlocks();
    refreshPreview();
  }

  function duplicateLastBlock(){
    syncCurrentFromForm();
    if(!state.current || !state.current.blocks.length){
      status('Det finns inga block att duplicera ännu.', 'error');
      return;
    }
    var last = JSON.parse(JSON.stringify(state.current.blocks[state.current.blocks.length - 1]));
    last.id = (last.id || 'block') + '-copy';
    state.current.blocks.push(last);
    renderBlocks();
    refreshPreview();
  }

  function openPreview(){
    syncCurrentFromForm();
    if(!state.current || !state.current.category){
      status('Välj kategori först.', 'error');
      return;
    }
    if(typeof window.navigateTo === 'function') window.navigateTo('konfigurator');
    if(typeof window.showKalkylConfig === 'function') window.showKalkylConfig(false, false, true);
    setTimeout(function(){
      var sel = document.getElementById('categorySelect');
      if(sel) sel.value = state.current.category || '';
      if(typeof window.changeCategory === 'function') window.changeCategory();
    }, 40);
    setTimeout(function(){
      var sel = document.getElementById('categorySelect');
      if(sel && sel.value !== (state.current.category || '')) sel.value = state.current.category || '';
      if(typeof window.changeCategory === 'function') window.changeCategory();
    }, 180);
  }

  async function saveCurrent(){
    syncCurrentFromForm();
    if(!state.current.title || !state.current.category){
      status('Namn och kategori krävs.', 'error');
      return;
    }
    var payload = {
      id: state.current.id,
      title: state.current.title,
      category: state.current.category,
      version: state.current.version || '1.0.0',
      notes: state.current.notes || '',
      builder_mode: state.current.builderMode || 'simple',
      head_preset: state.current.headPreset || 'current-default',
      sidebar_module: state.current.sidebarModule || 'price-summary',
      footer_mode: state.current.footerMode || 'none',
      base_source: state.current.baseSource || 'product-catalog',
      sidebar_position: state.current.sidebarPosition || 'right',
      regions: deriveRegions(state.current),
      blocks: (state.current.builderMode === 'advanced' ? state.current.blocks.map(serializeBlock) : [])
    };
    var res = await fetch('/api/calc_builder.php', {
      method: 'POST',
      headers: {'Content-Type': 'application/json'},
      body: JSON.stringify(payload)
    });
    var data = await res.json();
    if(!data.success){
      status(data.error || 'Kunde inte spara konfiguratorn.', 'error');
      return;
    }
    state.current.id = data.id || state.current.id;
    await loadConfigurators();
    if(typeof window.loadCalcSchemasFromAPI === 'function') window.loadCalcSchemasFromAPI(true);
    status('Konfiguratorn är sparad och kan nu läsas av calc_config.', 'success');
  }

  async function init(){
    if(init._done) return;
    init._done = true;
    await loadCategories();
    await loadConfigurators();
    createNew();
  }

  window.CalcBuilder = {
    init: init,
    createNew: createNew,
    renderList: renderList,
    addBlock: addBlock,
    removeBlock: removeBlock,
    moveBlock: moveBlock,
    duplicateLastBlock: duplicateLastBlock,
    saveCurrent: saveCurrent,
    openPreview: openPreview,
    refreshPreview: refreshPreview,
    renderBlocks: renderBlocks,
    select: select,
    previewSaved: previewSaved
  };
})();