/* Catalog prices are derived, never overwritten by a timer. Stored order lines keep their own price snapshots. Promotion dates are absolute UTC instants. */ (()=>{ 'use strict'; const DAY=86400000,MAX_TIMER=2147480000; const number=v=>typeof v==='number'?v:Number(String(v??'').trim().replace(',','.')); const cents=v=>Math.round((v+Number.EPSILON)*100)/100; const validPrice=v=>Number.isFinite(number(v))&&number(v)>=0; const money=v=>`${Number(v).toLocaleString('ru-RU',{maximumFractionDigits:2})} ₽`; const stamp=v=>typeof v==='string'&&v.trim()?Date.parse(v):NaN; const $=id=>document.getElementById(id); function basePrice(item){ const current=validPrice(item?.price)?cents(number(item.price)):0; // Compatibility only: old records stored the discounted price in price. return !item?.promotion&&validPrice(item?.oldPrice)&&number(item.oldPrice)>current?cents(number(item.oldPrice)):current; } function promotion(item){ if(item?.promotion&&typeof item.promotion==='object')return item.promotion; const base=basePrice(item),current=validPrice(item?.price)?cents(number(item.price)):0; return base>current?{type:'amount',value:cents(base-current),startsAt:null,endsAt:null}:null; } function quote(item,now=Date.now()){ const base=basePrice(item),p=promotion(item),value=number(p?.value); const start=p?.startsAt==null?-Infinity:stamp(p.startsAt),end=p?.endsAt==null?Infinity:stamp(p.endsAt); const valid=Boolean(p&&['percent','amount'].includes(p.type)&&Number.isFinite(value)&&value>0&&base>0&&value<=(p.type==='percent'?100:base)&&!Number.isNaN(start)&&!Number.isNaN(end)&&end>start); const active=valid&&now>=start&&now=end}; } const price=(item,now)=>quote(item,now).price; function linePrice(line,item,now){return line?.price!==undefined&&line.price!==null&&String(line.price).trim()!==''&&validPrice(line.price)?cents(number(line.price)):price(item,now)} const badge=(item)=>quote(item).active?'Акция':''; const localTime=iso=>{const d=new Date(iso);if(!Number.isFinite(d.getTime()))return '';return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}T${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`}; let editorOriginal=null,getter=null,timer=0,lastSignature=''; function loadEditor(item){ const p=promotion(item),q=quote(item); editorOriginal=p?{...p}:null; if(!$('ctPromotionEnabled'))return; $('boxPrice').value=basePrice(item); $('ctPromotionEnabled').checked=Boolean(p&&!q.expired); $('ctDiscountType').value=p?.type==='amount'?'amount':'percent'; $('ctDiscountValue').value=p?.value??''; const keep=$('ctPromotionDuration').querySelector('[value="keep"]'); keep.hidden=!(p?.endsAt&&!q.expired); $('ctPromotionDuration').value=p&&!q.expired?(p.endsAt?'keep':'none'):'7'; $('ctPromotionEnd').value=p?.endsAt?localTime(p.endsAt):''; $('ctPromotionError').textContent=''; updateEditor(); } function readEditor(now=Date.now()){ const base=number($('boxPrice')?.value); if(!Number.isFinite(base)||base<0)return {error:'Укажите обычную цену: число не меньше нуля.',field:'boxPrice'}; if(!$('ctPromotionEnabled')?.checked)return {base:cents(base),promotion:null}; const type=$('ctDiscountType').value,raw=$('ctDiscountValue').value,value=number(raw); if(!raw.trim()||!Number.isFinite(value)||cents(value)<=0)return {error:'Укажите скидку больше нуля.',field:'ctDiscountValue'}; if(base<=0||value>(type==='percent'?100:base))return {error:type==='percent'?'Скидка должна быть не больше 100%, а обычная цена — больше нуля.':'Скидка не может превышать обычную цену.',field:'ctDiscountValue'}; const duration=$('ctPromotionDuration').value; let startsAt=editorOriginal?.startsAt||new Date(now).toISOString(),endsAt=null; if(duration==='keep')endsAt=editorOriginal?.endsAt; else if(duration==='custom'){ const rawEnd=$('ctPromotionEnd').value,t=new Date(rawEnd).getTime(); if(!rawEnd||!Number.isFinite(t))return {error:'Выберите дату и время окончания акции.',field:'ctPromotionEnd'}; endsAt=new Date(t).toISOString();startsAt=new Date(now).toISOString(); }else if(duration!=='none'){ if(!['1','7','14','30'].includes(duration))return {error:'Выберите срок акции.',field:'ctPromotionDuration'}; startsAt=new Date(now).toISOString();endsAt=new Date(now+Number(duration)*DAY).toISOString(); } if(endsAt&&stamp(endsAt)<=now)return {error:'Акция уже закончилась. Выберите новый срок или выключите её.',field:'ctPromotionDuration'}; return {base:cents(base),promotion:{type,value:cents(value),startsAt,endsAt}}; } function updateEditor(){ const enabled=$('ctPromotionEnabled')?.checked,fields=$('ctPromotionFields');if(!fields)return; fields.hidden=!enabled; $('ctPromotionEndLabel').hidden=$('ctPromotionDuration').value!=='custom'; const type=$('ctDiscountType').value; $('ctDiscountCaption').textContent=type==='percent'?'Скидка, %':'Скидка, ₽'; $('ctDiscountValue').max=type==='percent'?'100':String(number($('boxPrice').value)||0); const data=readEditor(),preview=$('ctPromotionPreview'); if(data.error){preview.textContent=enabled?data.error:'';return;} const q=quote({price:data.base,promotion:data.promotion}); const ending=data.promotion?.endsAt?` До ${new Date(data.promotion.endsAt).toLocaleString('ru-RU',{day:'numeric',month:'long',hour:'2-digit',minute:'2-digit'})}. Затем снова ${money(data.base)}.`:' Без ограничения срока.'; preview.textContent=enabled?`Цена по акции: ${money(q.price)}.${ending}`:`Обычная цена: ${money(data.base)}.`; } function saveEditor(item){ const data=readEditor(); if(data.error){$('ctPromotionError').textContent=data.error;$(data.field)?.focus();return false;} item.price=data.base; if(data.promotion)item.promotion=data.promotion;else delete item.promotion; delete item.oldPrice;delete item.sale; $('ctPromotionError').textContent='';return true; } function refresh(){ clearTimeout(timer);timer=0;if(!getter)return; const now=Date.now(),items=getter()||[]; const signature=JSON.stringify(items.map(i=>[String(i.id),price(i,now),quote(i,now).active])); if(signature!==lastSignature){lastSignature=signature;window.dispatchEvent(new CustomEvent('caterium:catalog-prices-changed'));} let next=Infinity; for(const item of items){const p=promotion(item);for(const t of [stamp(p?.startsAt),stamp(p?.endsAt)])if(t>now)next=Math.min(next,t);} if(Number.isFinite(next))timer=setTimeout(refresh,Math.min(MAX_TIMER,Math.max(1,next-Date.now()+10))); } function watch(catalogGetter){getter=catalogGetter;refresh();} function boot(){ $('ctPromotionEditor')?.addEventListener('input',()=>{if($('ctPromotionError'))$('ctPromotionError').textContent='';updateEditor();}); $('ctPromotionEditor')?.addEventListener('change',updateEditor); $('boxPrice')?.addEventListener('input',updateEditor); window.addEventListener('sun:cloud-state-applied',refresh); window.addEventListener('focus',refresh); document.addEventListener('visibilitychange',()=>{if(!document.hidden)refresh();}); window.addEventListener('sun:cloud-tenant-changing',()=>{clearTimeout(timer);timer=0;lastSignature='';editorOriginal=null;}); } window.CateriumPricing=Object.freeze({basePrice,promotion,quote,price,linePrice,badge,loadEditor,saveEditor,refresh,watch}); if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',boot,{once:true});else boot(); })();