FoxyTeam

Custom Calculator

Sep 27th, 2025
221
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
CSS 32.60 KB | Source Code | 0 0
  1. <!-- === КОНФИГУРАЦИЯ (редактирай САМО стойностите; без коментари вътре) === -->
  2. <script type="application/json" id="win-calc-config">
  3. {
  4.   "MODELS": [
  5.     {"id":"win_fixed","label":"Прозорец глух","img":"линк към снимката за дограма"},
  6.     {"id":"win_single","label":"Прозорец еднокрил","img":"линк към снимката за дограма"},
  7.     {"id":"win_double","label":"Прозорец двукрил","img":"линк към снимката за дограма"},
  8.     {"id":"win_divider","label":"Прозорец с делител","img":"линк към снимката за дограма"},
  9.     {"id":"door_balcony","label":"Балконска врата","img":"линк към снимката за дограма"},
  10.     {"id":"door_balcony_div","label":"Балконска врата с делител","img":"линк към снимката за дограма"},
  11.     {"id":"win_tilt","label":"Прозорец tilt & turn","img":"линк към снимката за дограма"},
  12.     {"id":"win_panorama","label":"Панорамен прозорец","img":"линк към снимката за дограма"}
  13.   ],
  14.   "PRICING": {
  15.     "eurRate": 1.95583,
  16.     "vatRate": 0.2,
  17.  
  18.     "profilePerMeter": { "pvc":45, "alu":70, "wood":85 },
  19.     "colorMultiplier": { "white":1, "oneSideColor":1.15, "bothSidesColor":1.25 },
  20.  
  21.     "glazingPerM2": { "double24":85, "triple36":120 },
  22.     "glazingAddersPerM2": { "lowE":25, "fourSeasons":40, "argon":10 },
  23.  
  24.     "hardware": { "fixed":0, "single":55, "double":95, "tiltTurn":75 },
  25.  
  26.     "mosquito": 35,
  27.     "rollerShutterPerM2": 120,
  28.     "innerSillPerMeter": 18,
  29.     "dripEdgePerMeter": 15,
  30.  
  31.     "uninstallPerWindow": 20,
  32.     "installPerWindow": 45,
  33.     "finishingPerMeter": 14,
  34.     "transportPerKm": 1.96,
  35.  
  36.     "labels": {
  37.       "profile": "Профили (периметър)",
  38.       "glazing": "Стъклопакет",
  39.       "hardware": "Обков",
  40.       "mosquito": "Комарници",
  41.       "rollerShutter": "Ролетна щора",
  42.       "innerSill": "Вътрешен перваз",
  43.       "dripEdge": "Външен водобран",
  44.       "uninstall": "Демонтаж",
  45.       "install": "Монтаж",
  46.       "finishing": "Обръщане",
  47.       "transport": "Транспорт",
  48.       "vat": "ДДС (20%)",
  49.       "discount": "Отстъпка"
  50.     },
  51.  
  52.     "enabled": {
  53.       "mosquito": false,
  54.       "rollerShutter": true,
  55.       "innerSill": true,
  56.       "dripEdge": true,
  57.       "uninstall": true,
  58.       "install": true,
  59.       "finishing": false,
  60.       "transport": true
  61.     },
  62.  
  63.     "fixedAdders": []
  64.   }
  65. }
  66. </script>
  67.  
  68. <div id="win-calc" class="idi-win">
  69.   <style>
  70.     .idi-win{--c:#0ea5e9;--bg:#0b1220;--panel:#121a2a;--muted:#8aa0bd;font-family:Inter,system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;color:#e6eefb;border-radius:18px;}
  71.     .idi-grid{display:grid;grid-template-columns:1.1fr .9fr;gap:18px}
  72.     @media(max-width:980px){.idi-grid{grid-template-columns:1fr}}
  73.     .idi-card{background:var(--panel);border:1px solid rgba(255,255,255,.08);border-radius:16px;padding:16px}
  74.     .idi-title{font-size:22px;font-weight:800;margin:0 0 12px}
  75.     .idi-sub{font-size:12px;color:var(--muted);margin:-4px 0 10px}
  76.     .idi-row{display:grid;grid-template-columns:1fr auto;gap:10px;align-items:center;margin:10px 0}
  77.     .idi-row label{font-size:14px}
  78.     .idi-row input[type="number"],.idi-row select{width:190px;background:#0e1626;color:#e6eefb;border:1px solid rgba(255,255,255,.1);border-radius:10px;padding:8px 10px}
  79.     .idi-check{display:flex;align-items:center;gap:10px;margin:6px 0}
  80.     .idi-check input{transform:scale(1.15)}
  81.     .idi-divider{height:1px;background:linear-gradient(90deg,transparent,rgba(255,255,255,.14),transparent);margin:14px 0}
  82.     .idi-models{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px}
  83.     @media(max-width:980px){.idi-models{grid-template-columns:repeat(2,1fr)}}
  84.     .idi-model{background:#0e1626;border:1px solid rgba(255,255,255,.08);border-radius:12px;padding:10px;cursor:pointer;text-align:center;font-size:13px;line-height:1.2}
  85.     .idi-model:hover{border-color:rgba(255,255,255,.25)}
  86.     .idi-model.active{outline:2px solid var(--c);box-shadow:0 0 0 2px rgba(14,165,233,.25) inset}
  87.     .idi-model img{width:100%;max-height:90px;object-fit:contain;margin-bottom:6px;display:block}
  88.     .idi-model.idi-img-error::before{content:"(снимката не се зареди)";display:block;font-size:11px;color:#ffb4b4;margin-bottom:6px}
  89.     .idi-badges{display:flex;gap:8px;flex-wrap:wrap;margin:8px 0}
  90.     .idi-badge{background:#0e1626;border:1px solid rgba(255,255,255,.08);padding:4px 8px;border-radius:999px;font-size:12px;color:#9fb2d0}
  91.     .idi-breakdown{list-style:none;margin:8px 0 0;padding:0}
  92.     .idi-breakdown li{display:flex;justify-content:space-between;border-bottom:1px dashed rgba(255,255,255,.1);padding:7px 0}
  93.     .idi-total{font-size:28px;font-weight:900}
  94.     .idi-actions{display:flex;gap:10px;flex-wrap:wrap;margin-top:12px}
  95.     .idi-btn{background:var(--c);border:none;color:#021322;font-weight:800;padding:10px 14px;border-radius:12px;cursor:pointer}
  96.     .idi-btn.secondary{background:#0e1626;color:#e6eefb;border:1px solid rgba(255,255,255,.15)}
  97.     .idi-note{font-size:12px;color:#8aa0bd;margin-top:6px}
  98.     .idi-model-preview{margin-top:10px;background:#0e1626;border:1px solid rgba(255,255,255,.08);border-radius:12px;padding:12px}
  99.     .idi-model-preview img{max-width:100%;height:auto;display:block;margin:0 auto}
  100.     .idi-model-caption{font-size:12px;color:#9fb2d0;text-align:center;margin-top:6px}
  101.   </style>
  102.  
  103.   <div class="idi-grid">
  104.     <!-- ЛЯВО: Модели + Параметри -->
  105.     <div class="idi-card">
  106.       <h3 class="idi-title">Калкулатор за дограма</h3>
  107.       <p class="idi-sub">1) Изберете модел → 2) Попълнете параметрите → 3) Вижте разбивката и тотала (BGN/EUR).</p>
  108.  
  109.       <div class="idi-sub">Модел</div>
  110.       <div class="idi-models" id="modelsWrap"></div>
  111.  
  112.       <div id="modelPreview" class="idi-model-preview" style="display:none">
  113.         <img id="modelImg" src="" alt="Преглед на модел">
  114.         <div id="modelCaption" class="idi-model-caption"></div>
  115.       </div>
  116.  
  117.       <div id="formWrap" style="display:none">
  118.         <div class="idi-divider"></div>
  119.         <div class="idi-row"><label>Материал</label>
  120.           <select id="material">
  121.             <option value="" selected disabled hidden>— Изберете —</option>
  122.             <option value="pvc">PVC</option><option value="alu">Алуминий</option><option value="wood">Дърво</option>
  123.           </select>
  124.         </div>
  125.         <div class="idi-row"><label>Цвят</label>
  126.           <select id="color">
  127.             <option value="" selected disabled hidden>— Изберете —</option>
  128.             <option value="white">Бял</option><option value="oneSideColor">Едностранно цветен</option><option value="bothSidesColor">Двустранно цветен</option>
  129.           </select>
  130.         </div>
  131.         <div class="idi-row"><label>Ширина (см)</label><input id="width" type="number" min="30" max="350" step="1" placeholder="напр. 120"></div>
  132.         <div class="idi-row"><label>Височина (см)</label><input id="height" type="number" min="30" max="350" step="1" placeholder="напр. 140"></div>
  133.         <div class="idi-row"><label>Разделител (делител)</label>
  134.           <select id="divider">
  135.             <option value="" selected disabled hidden>— Изберете —</option>
  136.             <option value="none">Без</option><option value="vertical">Вертикален</option><option value="horizontal">Хоризонтален</option>
  137.           </select>
  138.         </div>
  139.         <div class="idi-row"><label>Стъклопакет (база)</label>
  140.           <select id="glazingBase">
  141.             <option value="" selected disabled hidden>— Изберете —</option>
  142.             <option value="double24">Двоен 24 мм</option><option value="triple36">Троен 36 мм</option>
  143.           </select>
  144.         </div>
  145.         <div class="idi-check"><input id="gLowE" type="checkbox"><label for="gLowE">Low-E добавка</label></div>
  146.         <div class="idi-check"><input id="gFour" type="checkbox"><label for="gFour">4 сезона добавка</label></div>
  147.         <div class="idi-check"><input id="gArgon" type="checkbox"><label for="gArgon">Аргон добавка</label></div>
  148.         <div class="idi-row"><label>Отваряемост / Обков</label>
  149.           <select id="hardware">
  150.             <option value="" selected disabled hidden>— Изберете —</option>
  151.             <option value="fixed">Глух</option><option value="single">Еднокрила</option><option value="double">Двукрила</option><option value="tiltTurn">Падащо-отваряема</option>
  152.           </select>
  153.         </div>
  154.  
  155.         <div class="idi-divider"></div>
  156.         <div class="idi-sub">Екстри</div>
  157.         <div class="idi-row"><label>Комарници (бр.)</label><input id="mosquitoQty" type="number" min="0" step="1" value="0"></div>
  158.         <div class="idi-row"><label>Външна ролетна щора</label>
  159.           <select id="shutterYes"><option value="" selected disabled hidden>— Изберете —</option><option value="no">Не</option><option value="yes">Да</option></select>
  160.         </div>
  161.         <div class="idi-row"><label>Вътрешен перваз</label>
  162.           <select id="innerSillYes"><option value="" selected disabled hidden>— Изберете —</option><option value="no">Не</option><option value="yes">Да</option></select>
  163.         </div>
  164.         <div class="idi-row"><label>Външен водобран</label>
  165.           <select id="dripYes"><option value="" selected disabled hidden>— Изберете —</option><option value="no">Не</option><option value="yes">Да</option></select>
  166.         </div>
  167.  
  168.         <div class="idi-divider"></div>
  169.         <div class="idi-sub">Услуги</div>
  170.         <div class="idi-check"><input id="uninstallOld" type="checkbox"><label for="uninstallOld">Демонтаж на стара дограма</label></div>
  171.         <div class="idi-check"><input id="installNew" type="checkbox"><label for="installNew">Монтаж</label></div>
  172.         <div class="idi-check"><input id="finishing" type="checkbox"><label for="finishing">Обръщане/довършителни работи</label></div>
  173.         <div class="idi-row"><label>Транспорт (км)</label><input id="distanceKm" type="number" min="0" step="1" value="0"></div>
  174.  
  175.         <div class="idi-divider"></div>
  176.         <div class="idi-row"><label>Отстъпка (%)</label><input id="discount" type="number" min="0" max="100" step="1" value="0"></div>
  177.         <div class="idi-check"><input id="vatOn" type="checkbox" checked><label for="vatOn">Включи ДДС (20%)</label></div>
  178.       </div>
  179.     </div>
  180.  
  181.     <!-- ДЯСНО: Обобщение -->
  182.     <div class="idi-card">
  183.       <h3 class="idi-title">Обобщение</h3>
  184.       <div class="idi-badges" id="badges"></div>
  185.       <ul class="idi-breakdown" id="breakdown"></ul>
  186.       <div class="idi-divider"></div>
  187.       <div class="idi-row">
  188.         <div><div class="idi-sub">Общо (BGN)</div><div class="idi-total" id="totalBGN">—</div></div>
  189.         <div><div class="idi-sub">Общо (EUR)</div><div class="idi-total" id="totalEUR">—</div></div>
  190.       </div>
  191.       <div class="idi-actions">
  192.         <button class="idi-btn" id="copyQuote">Копирай офертата</button>
  193.         <button class="idi-btn secondary" id="pdfExport" type="button">Експорт PDF</button>
  194.         <button class="idi-btn secondary" id="resetForm" type="button">Нулирай</button>
  195.       </div>
  196.       <p class="idi-note">* Резултатът е ориентировъчен и зависи от точните размери и спецификация.</p>
  197.       <p class="idi-note">* EUR при курс 1 EUR = 1.95583 BGN.</p>
  198.     </div>
  199.   </div>
  200.  
  201.   <script>
  202.     (function(){
  203.       // -- Четене и валидация на конфигурацията
  204.       let CONFIG;
  205.       try{
  206.         CONFIG = JSON.parse(document.getElementById('win-calc-config').textContent);
  207.       }catch(e){
  208.         alert('Грешка в CONFIG JSON: ' + e.message);
  209.         return;
  210.       }
  211.       let MODELS  = CONFIG.MODELS;
  212.       let PRICING = CONFIG.PRICING;
  213.  
  214.       // -- DOM елементи
  215.       const $ = id => document.getElementById(id);
  216.       const modelsWrap=$('modelsWrap'), formWrap=$('formWrap'), modelPreview=$('modelPreview'),
  217.             modelImg=$('modelImg'), modelCaption=$('modelCaption');
  218.       const material=$('material'), color=$('color'), width=$('width'), height=$('height');
  219.       const divider=$('divider'), glazingBase=$('glazingBase');
  220.       const gLowE=$('gLowE'), gFour=$('gFour'), gArgon=$('gArgon');
  221.       const hardware=$('hardware'), mosquitoQty=$('mosquitoQty');
  222.       const shutterYes=$('shutterYes'), innerSillYes=$('innerSillYes'), dripYes=$('dripYes');
  223.       const uninstallOld=$('uninstallOld'), installNew=$('installNew'), finishing=$('finishing');
  224.       const distanceKm=$('distanceKm'), discount=$('discount'), vatOn=$('vatOn');
  225.       const badges=$('badges'), breakdown=$('breakdown'), totalBGN=$('totalBGN'), totalEUR=$('totalEUR');
  226.       const copyBtn=$('copyQuote'), pdfBtn=$('pdfExport'), resetBtn=$('resetForm');
  227.  
  228.       // -- Помощни
  229.       const fmtBGN = n => new Intl.NumberFormat('bg-BG',{style:'currency',currency:'BGN',maximumFractionDigits:0}).format(n);
  230.       const fmtEUR = n => new Intl.NumberFormat('de-DE',{style:'currency',currency:'EUR',maximumFractionDigits:0}).format(n);
  231.       const toEUR  = bgn => bgn/PRICING.eurRate;
  232.       const clamp  = (v,min,max)=>Math.min(Math.max(v,min),max);
  233.       const badge  = txt => `<span class="idi-badge">${txt}</span>`;
  234.       const m2     = (wcm,hcm)=> (wcm/100)*(hcm/100);
  235.       const perim  = (wcm,hcm)=> 2*((wcm/100)+(hcm/100));
  236.       const L   = (k, def) => (PRICING.labels && PRICING.labels[k]) || def;
  237.       const ON  = (k) => !PRICING.enabled || PRICING.enabled[k] !== false;
  238.  
  239.       // -- Рендер на моделите (с cache-buster и fallback)
  240.       let selectedModel = '';
  241.       function renderModels(){
  242.         modelsWrap.innerHTML='';
  243.         MODELS.forEach(m=>{
  244.           const d=document.createElement('div'); d.className='idi-model'; d.dataset.id=m.id;
  245.           const src = (m.img||'').trim();
  246.           const imgTag = src
  247.             ? `<img loading="lazy" src="${src}${src.includes('?')?'&':'?'}v=${Date.now()}" alt="${m.label}" onerror="this.closest('.idi-model').classList.add('idi-img-error'); this.remove();">`
  248.             : '';
  249.           d.innerHTML = imgTag + `<div>${m.label}</div>`;
  250.           d.addEventListener('click',()=>{
  251.             selectedModel=m.id;
  252.             [...modelsWrap.children].forEach(x=>x.classList.remove('active'));
  253.             d.classList.add('active');
  254.             const previewSrc = src ? src + (src.includes('?')?'&':'?') + 'v='+Date.now() : '';
  255.             modelImg.src = previewSrc;
  256.             modelImg.alt = m.label; modelCaption.textContent = m.label;
  257.             modelPreview.style.display = 'block';
  258.             formWrap.style.display='block';
  259.             calculate();
  260.           });
  261.           modelsWrap.appendChild(d);
  262.         });
  263.       }
  264.       renderModels();
  265.  
  266.       // -- Скриване/показване на полета според PRICING.enabled
  267.       function toggleRow(idOrEl, on) {
  268.         const el = typeof idOrEl === 'string' ? document.getElementById(idOrEl) : idOrEl;
  269.         if (!el) return;
  270.         const wrap = el.closest('.idi-row') || el.closest('.idi-check');
  271.         if (!wrap) return;
  272.         wrap.style.display = on ? '' : 'none';
  273.         if (!on) {
  274.           if (el.tagName === 'SELECT') el.selectedIndex = 0;
  275.           else if (el.type === 'checkbox') el.checked = false;
  276.           else if (el.type === 'number') el.value = '';
  277.         }
  278.       }
  279.       function applyEnabledVisibility(){
  280.         const e = PRICING.enabled || {};
  281.         toggleRow('mosquitoQty',   e.mosquito       !== false);
  282.         toggleRow('shutterYes',    e.rollerShutter !== false);
  283.         toggleRow('innerSillYes',  e.innerSill     !== false);
  284.         toggleRow('dripYes',       e.dripEdge      !== false);
  285.         toggleRow('uninstallOld',  e.uninstall     !== false);
  286.         toggleRow('installNew',    e.install       !== false);
  287.         toggleRow('finishing',     e.finishing     !== false);
  288.         toggleRow('distanceKm',    e.transport     !== false);
  289.       }
  290.       applyEnabledVisibility();
  291.  
  292.       // -- Готовност на формата
  293.       function ready(){
  294.         if(!selectedModel) return false;
  295.         if(!material.value || !color.value || !glazingBase.value || !hardware.value) return false;
  296.         const W=parseFloat(width.value||0), H=parseFloat(height.value||0);
  297.         return (W>0 && H>0);
  298.       }
  299.  
  300.       // -- Последна калкулация (за PDF/копиране)
  301.       let LAST = null;
  302.  
  303.       function gatherSelections(W,H,discP){
  304.         return {
  305.           Модел: (MODELS.find(x=>x.id===selectedModel)?.label || ''),
  306.           Материал: {pvc:'PVC', alu:'Алуминий', wood:'Дърво'}[material.value] || '',
  307.           Цвят: {white:'Бял', oneSideColor:'Едностранно цветен', bothSidesColor:'Двустранно цветен'}[color.value] || '',
  308.           Размер: `${W} × ${H} см`,
  309.           Делител: {none:'Без', vertical:'Вертикален', horizontal:'Хоризонтален'}[divider.value||'none'] || '—',
  310.           'Стъклопакет (база)': {double24:'Двоен 24 мм', triple36:'Троен 36 мм'}[glazingBase.value] || '',
  311.           'Добавки към стъклопакет': [
  312.             gLowE.checked?'Low-E':null,
  313.             gFour.checked?'4 сезона':null,
  314.             gArgon.checked?'Аргон':null
  315.           ].filter(Boolean).join(', ') || '—',
  316.           Обков: {fixed:'Глух', single:'Еднокрила', double:'Двукрила', tiltTurn:'Падащо-отваряема'}[hardware.value] || '',
  317.           'Комарници (бр.)': parseInt(mosquitoQty.value||'0',10),
  318.           'Ролетна щора': {yes:'Да', no:'Не'}[shutterYes.value||'no'] || '—',
  319.           'Вътрешен перваз': {yes:'Да', no:'Не'}[innerSillYes.value||'no'] || '—',
  320.           'Външен водобран': {yes:'Да', no:'Не'}[dripYes.value||'no'] || '—',
  321.           Демонтаж: uninstallOld.checked ? 'Да' : 'Не',
  322.           Монтаж: installNew.checked ? 'Да' : 'Не',
  323.           'Обръщане/довършителни': finishing.checked ? 'Да' : 'Не',
  324.           'Транспорт (км)': parseFloat(distanceKm.value||'0'),
  325.           'Отстъпка (%)': discP,
  326.           'ДДС включено': vatOn.checked ? 'Да' : 'Не'
  327.         };
  328.       }
  329.  
  330.       // -- Калкулация
  331.       function calculate(){
  332.         if(!ready()){
  333.           breakdown.innerHTML='';
  334.           badges.innerHTML = selectedModel ? badge(MODELS.find(x=>x.id===selectedModel)?.label||'') : '';
  335.           totalBGN.textContent='—'; totalEUR.textContent='—';
  336.           LAST=null;
  337.           return;
  338.         }
  339.  
  340.         const W = clamp(parseFloat(width.value),30,350);
  341.         const H = clamp(parseFloat(height.value),30,350);
  342.         const area = m2(W,H);
  343.         const perimeter = perim(W,H);
  344.  
  345.         const profPrice = PRICING.profilePerMeter[material.value];
  346.         const colorK    = PRICING.colorMultiplier[color.value] || 1;
  347.         let profileCost = perimeter * profPrice * colorK;
  348.         if(divider.value==='vertical'){ profileCost += (H/100) * profPrice * colorK; }
  349.         else if(divider.value==='horizontal'){ profileCost += (W/100) * profPrice * colorK; }
  350.  
  351.         let glazingCost = area * PRICING.glazingPerM2[glazingBase.value];
  352.         if(gLowE.checked)  glazingCost += area * PRICING.glazingAddersPerM2.lowE;
  353.         if(gFour.checked)  glazingCost += area * PRICING.glazingAddersPerM2.fourSeasons;
  354.         if(gArgon.checked) glazingCost += area * PRICING.glazingAddersPerM2.argon;
  355.  
  356.         const hardwareCost = PRICING.hardware[hardware.value];
  357.  
  358.         const e = PRICING.enabled || {};
  359.         const widthMeters  = W/100;
  360.  
  361.         const mosquitoCost = (e.mosquito === false) ? 0
  362.           : (parseInt(mosquitoQty.value||'0',10)) * PRICING.mosquito;
  363.  
  364.         const shutterCost  = (e.rollerShutter === false) ? 0
  365.           : (shutterYes.value==='yes' ? (area * PRICING.rollerShutterPerM2) : 0);
  366.  
  367.         const innerSillCost= (e.innerSill === false) ? 0
  368.           : (innerSillYes.value==='yes' ? (widthMeters * PRICING.innerSillPerMeter) : 0);
  369.  
  370.         const dripCost     = (e.dripEdge === false) ? 0
  371.           : (dripYes.value==='yes' ? (widthMeters * PRICING.dripEdgePerMeter) : 0);
  372.  
  373.         const uninstallCost = (e.uninstall === false) ? 0
  374.           : (uninstallOld.checked ? PRICING.uninstallPerWindow : 0);
  375.  
  376.         const installCost   = (e.install === false) ? 0
  377.           : (installNew.checked ? PRICING.installPerWindow : 0);
  378.  
  379.         const finishingCost = (e.finishing === false) ? 0
  380.           : (finishing.checked ? (perimeter * PRICING.finishingPerMeter) : 0);
  381.  
  382.         const transportCost = (e.transport === false) ? 0
  383.           : ((parseFloat(distanceKm.value||'0')>0) ? (parseFloat(distanceKm.value)*PRICING.transportPerKm) : 0);
  384.  
  385.         // Редове за разбивката
  386.         const items = [];
  387.         if (ON('profile'))    items.push([L('profile','Профили (периметър)'), Math.round(profileCost)]);
  388.         if (ON('glazing'))    items.push([L('glazing','Стъклопакет'),        Math.round(glazingCost)]);
  389.         if (ON('hardware'))   items.push([L('hardware','Обков'),             Math.round(hardwareCost)]);
  390.         if (mosquitoCost && ON('mosquito'))     items.push([L('mosquito','Комарници'),        Math.round(mosquitoCost)]);
  391.         if (shutterCost && ON('rollerShutter')) items.push([L('rollerShutter','Ролетна щора'), Math.round(shutterCost)]);
  392.         if (innerSillCost && ON('innerSill'))   items.push([L('innerSill','Вътрешен перваз'),  Math.round(innerSillCost)]);
  393.         if (dripCost && ON('dripEdge'))         items.push([L('dripEdge','Външен водобран'),   Math.round(dripCost)]);
  394.         if (uninstallCost && ON('uninstall'))   items.push([L('uninstall','Демонтаж'),         Math.round(uninstallCost)]);
  395.         if (installCost && ON('install'))       items.push([L('install','Монтаж'),             Math.round(installCost)]);
  396.         if (finishingCost && ON('finishing'))   items.push([L('finishing','Обръщане'),         Math.round(finishingCost)]);
  397.         if (transportCost && ON('transport'))   items.push([L('transport','Транспорт'),        Math.round(transportCost)]);
  398.  
  399.         if (Array.isArray(PRICING.fixedAdders)) {
  400.           for (const adder of PRICING.fixedAdders) {
  401.             const amt = Number(adder && adder.amount);
  402.             if (!isNaN(amt) && amt !== 0) items.push([adder.label || 'Допълнителна позиция', Math.round(amt)]);
  403.           }
  404.         }
  405.  
  406.         let subtotal = items.reduce((s, it)=> s + (typeof it[1]==='number' ? it[1] : 0), 0);
  407.         const discP = clamp(parseFloat(discount.value||'0'),0,100);
  408.         const discAmount = Math.round(subtotal * (discP/100));
  409.         let afterDisc = subtotal - discAmount;
  410.         const vat = vatOn.checked ? Math.round(afterDisc * PRICING.vatRate) : 0;
  411.         const total = afterDisc + vat;
  412.  
  413.         // Баджове
  414.         const modelObj = MODELS.find(x=>x.id===selectedModel) || {};
  415.         const badgeList = [];
  416.         badgeList.push(badge(modelObj.label || ''));
  417.         badgeList.push(badge((material.value||'').toUpperCase()));
  418.         badgeList.push(badge(color.value==='white'?'бял':(color.value==='oneSideColor'?'едностранно':'двустранно')));
  419.         badgeList.push(badge((glazingBase.value||'').replace('double24','двоен 24').replace('triple36','троен 36')));
  420.         if(gLowE.checked)  badgeList.push(badge('Low-E'));
  421.         if(gFour.checked)  badgeList.push(badge('4 сезона'));
  422.         if(gArgon.checked) badgeList.push(badge('аргон'));
  423.         badgeList.push(badge({fixed:'Глух', single:'Еднокрила', double:'Двукрила', tiltTurn:'Падащо-отваряема'}[hardware.value]||''));
  424.         badges.innerHTML = badgeList.join('');
  425.  
  426.         // Рендер на разбивката
  427.         breakdown.innerHTML =
  428.           items.map(([t,v])=>`<li><span>${t}</span><strong>${fmtBGN(v)}</strong></li>`).join('') +
  429.           (discAmount? `<li><span>${L('discount','Отстъпка')} (${discP}%)</span><strong>- ${fmtBGN(discAmount)}</strong></li>`:'') +
  430.           (vat? `<li><span>${L('vat','ДДС (20%)')}</span><strong>${fmtBGN(vat)}</strong></li>`:'');
  431.  
  432.         totalBGN.textContent = fmtBGN(total);
  433.         totalEUR.textContent = fmtEUR(toEUR(total));
  434.  
  435.         // Копиране на офертата
  436.         const selections = gatherSelections(W,H,discP);
  437.         copyBtn.onclick = () => {
  438.           const txt = makePlainText(items, discP, discAmount, vat, total, selections);
  439.           navigator.clipboard.writeText(txt).then(()=>{
  440.             copyBtn.textContent='Копирано!';
  441.             setTimeout(()=>copyBtn.textContent='Копирай офертата',1200);
  442.           });
  443.         };
  444.  
  445.         // Запази за PDF
  446.         LAST = {
  447.           items, discP, discAmount, vat, total,
  448.           eur: Math.round(total/PRICING.eurRate),
  449.           selections,
  450.           modelLabel: modelObj.label || '',
  451.           modelSrc: (modelObj.img||'')
  452.         };
  453.       }
  454.  
  455.       function makeEmailBody(items, discP, discAmount, vat, total, selections){
  456.         const lines = [];
  457.         lines.push('Здравей, Красимир,','', 'Калкулация за дограма:','', 'Параметри:');
  458.         for(const [k,v] of Object.entries(selections)){ lines.push(`• ${k}: ${v}`); }
  459.         lines.push('', 'Разбивка:');
  460.         for(const [t,v] of items){ lines.push(`• ${t}: ${v} лв`); }
  461.         if(discAmount) lines.push(`• ${L('discount','Отстъпка')} (${discP}%): -${discAmount} лв`);
  462.         if(vat) lines.push(`• ${L('vat','ДДС (20%)')}: ${vat} лв`);
  463.         lines.push('', `ОБЩО: ${total} лв (≈ ${Math.round(total/PRICING.eurRate)} EUR)`);
  464.         return lines.join('\n');
  465.       }
  466.  
  467.       function makePlainText(items, discP, discAmount, vat, total, selections){
  468.         const lines = [];
  469.         lines.push('Ориентировъчна оферта – дограма','Параметри:');
  470.         for(const [k,v] of Object.entries(selections)){ lines.push(`${k}: ${v}`); }
  471.         lines.push('----------------------');
  472.         for(const [t,v] of items){ lines.push(`${t}: ${v} лв`); }
  473.         if(discAmount) lines.push(`${L('discount','Отстъпка')} (${discP}%): -${discAmount} лв`);
  474.         if(vat) lines.push(`${L('vat','ДДС (20%)')}: ${vat} лв`);
  475.         lines.push('----------------------', `ОБЩО: ${total} лв (≈ ${Math.round(total/PRICING.eurRate)} EUR)`);
  476.         return lines.join('\n');
  477.       }
  478.  
  479.       // -- Експорт в PDF (Print → Save as PDF) — без <script> в темплейта
  480.       function exportPDF(){
  481.         if(!LAST){ alert('Попълнете параметрите и генерирайте калкулацията преди експорт.'); return; }
  482.  
  483.         const now = new Date();
  484.         const dateStr = new Intl.DateTimeFormat('bg-BG', {
  485.           year:'numeric', month:'2-digit', day:'2-digit',
  486.           hour:'2-digit', minute:'2-digit'
  487.         }).format(now);
  488.  
  489.         let rowsHTML = LAST.items.map(([t,v])=>`<tr><td>${t}</td><td class="r">${v} лв</td></tr>`).join('');
  490.         if(LAST.discAmount) rowsHTML += `<tr><td>${L('discount','Отстъпка')} (${LAST.discP}%)</td><td class="r">- ${LAST.discAmount} лв</td></tr>`;
  491.         if(LAST.vat)        rowsHTML += `<tr><td>${L('vat','ДДС (20%)')}</td><td class="r">${LAST.vat} лв</td></tr>`;
  492.  
  493.         const selHTML = Object.entries(LAST.selections)
  494.           .map(([k,v])=>`<div class="row"><span class="k">${k}</span><span class="v">${v}</span></div>`).join('');
  495.  
  496.         const logoOrModel = LAST.modelSrc
  497.           ? `<img class="hero" src="${LAST.modelSrc}" alt="${LAST.modelLabel}">`
  498.           : '';
  499.  
  500.         const html = `
  501. <!doctype html>
  502. <html lang="bg">
  503. <head>
  504. <meta charset="utf-8">
  505. <meta name="viewport" content="width=device-width,initial-scale=1">
  506. <title>Оферта – дограма</title>
  507. <style>
  508.   *{box-sizing:border-box}
  509.   body{font-family:Inter,system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;margin:0;padding:24px;background:#fff;color:#111}
  510.   .wrap{max-width:780px;margin:0 auto}
  511.   .head{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}
  512.   .brand{font-weight:800;font-size:20px}
  513.   .meta{color:#666;font-size:12px}
  514.   .hero{display:block;max-width:260px;max-height:160px;object-fit:contain;margin:8px 0}
  515.   h1{font-size:24px;margin:12px 0 8px}
  516.   h2{font-size:16px;margin:18px 0 8px;color:#444}
  517.   .grid{display:grid;grid-template-columns:1fr 1fr;gap:18px}
  518.   .card{border:1px solid #e6e6e6;border-radius:12px;padding:16px}
  519.   .row{display:flex;justify-content:space-between;border-bottom:1px dashed #e6e6e6;padding:6px 0}
  520.   .row .k{color:#444}
  521.   .row .v{font-weight:600}
  522.   table{width:100%;border-collapse:collapse;margin-top:4px}
  523.   td{padding:8px 6px;border-bottom:1px solid #eee}
  524.   td.r{text-align:right}
  525.   .total{font-size:20px;font-weight:900;margin-top:10px}
  526.   .muted{color:#666;font-size:12px;margin-top:6px}
  527.   .footer{margin-top:24px;font-size:12px;color:#666}
  528.   @media print {.no-print{display:none}}
  529. </style>
  530. </head>
  531. <body onload="window.print()">
  532.   <div class="wrap">
  533.     <div class="head">
  534.       <div>
  535.         <div class="brand">Оферта – дограма</div>
  536.         <div class="meta">Генерирана: ${dateStr} • ${location.host}</div>
  537.       </div>
  538.       ${logoOrModel}
  539.     </div>
  540.  
  541.     <div class="grid">
  542.       <div class="card">
  543.         <h2>Параметри</h2>
  544.         ${selHTML}
  545.       </div>
  546.       <div class="card">
  547.         <h2>Разбивка</h2>
  548.         <table>${rowsHTML}</table>
  549.         <div class="total">ОБЩО: ${LAST.total} лв (≈ ${LAST.eur} EUR)</div>
  550.         <div class="muted">Курс: 1 EUR = ${PRICING.eurRate} BGN</div>
  551.       </div>
  552.     </div>
  553.  
  554.     <div class="footer">
  555.       * Офертата е ориентировъчна и подлежи на уточнение след оглед/детайлизиране.
  556.     </div>
  557.  
  558.     <div class="no-print" style="margin-top:16px">
  559.       <button onclick="window.print()">Печат / Запази като PDF</button>
  560.     </div>
  561.   </div>
  562. </body>
  563. </html>`;
  564.  
  565.         const w = window.open('', '_blank', 'width=900,height=900');
  566.         if(!w){ alert('Моля, разрешете pop-up за този сайт, за да се отвори PDF прозорец.'); return; }
  567.         w.document.write(html);
  568.         w.document.close();
  569.         w.focus();
  570.       }
  571.  
  572.       // -- Събития
  573.       ['change','input'].forEach(evt=>{
  574.         document.querySelectorAll('#win-calc select,#win-calc input')
  575.           .forEach(el=>{ el.addEventListener(evt, calculate); el.setAttribute('autocomplete','off'); });
  576.       });
  577.       pdfBtn.addEventListener('click', exportPDF);
  578.  
  579.       resetBtn.addEventListener('click',()=>{
  580.         selectedModel=''; [...modelsWrap.children].forEach(x=>x.classList.remove('active'));
  581.         formWrap.style.display='none'; modelPreview.style.display='none'; modelImg.src=''; modelCaption.textContent='';
  582.         document.querySelectorAll('#win-calc select').forEach(s=>s.selectedIndex=0);
  583.         document.querySelectorAll('#win-calc input[type="number"]').forEach(i=>i.value='');
  584.         [gLowE,gFour,gArgon,uninstallOld,installNew,finishing,vatOn].forEach(c=>c.checked=false);
  585.         applyEnabledVisibility();
  586.         vatOn.checked=true; mosquitoQty.value='0'; distanceKm.value='0';
  587.         breakdown.innerHTML=''; badges.innerHTML=''; totalBGN.textContent='—'; totalEUR.textContent='—';
  588.         LAST=null;
  589.       });
  590.  
  591.       // -- Старт
  592.       breakdown.innerHTML=''; totalBGN.textContent='—'; totalEUR.textContent='—';
  593.     })();
  594.   </script>
  595. </div>
  596.  
Tags: html CSS js
Advertisement
Add Comment
Please, Sign In to add comment