Canonical client identity, client data layer, legacy loyalty/communication compatibility, normalized Supabase client RPC, release metadata and tests.
1 line
12 KiB
JavaScript
1 line
12 KiB
JavaScript
const sunEsc=window.SunSafe.escapeHTML,sunAttr=window.SunSafe.escapeAttr,sunId=window.SunSafe.idToken,sunImg=window.SunSafe.safeImageSrc,sunInt=v=>Number.isFinite(Number(v))?Math.trunc(Number(v)):0;let boxes=JSON.parse(localStorage.sunBoxes||'null')||[{id:'1',name:'Фуршетный бокс №1 — канапе мясные',ingredients:[['Хлеб',25,'шт.'],['Салями модеро',10,'кус.'],['Мортаделла',10,'кус.'],['Шейка коппа',10,'кус.'],['Ветчина пармская',10,'кус.'],['Салями в сыре',10,'кус.'],['Фрилис',.33,'пачки'],['Томаты черри жёлтые',2.5,'шт.'],['Томаты черри красные',2.5,'шт.'],['Огурец консервированный',5,'кус.'],['Маслины',2.5,'шт.'],['Слайсы огурца свежего',5,'шт.'],['Фисташка дроблёная',1,'г']]},{id:'2',name:'Фуршетный бокс №2 — канапе рыбные',ingredients:[['Хлеб',15,'шт.'],['Креветка',10,'шт.'],['Тарталетка',10,'шт.'],['Икра красная',20,'г'],['Крем-чиз',30,'г'],['Лосось кубиком',35,'г']]},{id:'3',name:'Фуршетный бокс №3 — микс тарталеток',ingredients:[['Тарталетка',25,'шт.'],['Креветка',5,'шт.'],['Икра щучья',25,'г'],['Икра красная',25,'г'],['Мортаделла',5,'кус.'],['Груша',5,'кус.'],['Крем-чиз',25,'г']]},{id:'4',name:'Фуршетный бокс №4 — премиум морепродукты',ingredients:[['Тарталетка',10,'шт.'],['Ролл из зелёного блинчика',5,'шт.'],['Ролл из чёрного блинчика',5,'шт.'],['Тунец обожжённый',5,'шт.'],['Креветка',5,'шт.'],['Слайс лосося',10,'шт.'],['Икра красная',25,'г']]},{id:'5',name:'Фуршетный бокс №5 — брускетты мясные',ingredients:[['Хлеб',25,'шт.'],['Салями в сыре',10,'шт.'],['Ростбиф',10,'шт.'],['Шейка коппа',10,'шт.'],['Прошутто',10,'шт.'],['Салями модеро',10,'шт.']]},{id:'6',name:'Фуршетный бокс №6 — сыры в шотах',ingredients:[['Моцарелла',15,'шариков'],['Дорблю',15,'кубиков'],['Камамбер',10,'кус.'],['Пармезан',15,'кус.'],['Чеддер',15,'кус.'],['Груша',5,'кус.'],['Виноград',5,'шт.']]}],orders=JSON.parse(localStorage.sunOrders||'[]'),draft={id:0,lines:[]},edited=null;date.value=new Date().toISOString().slice(0,10);for(let i=0;i<48;i++){let t=String(i>>1).padStart(2,'0')+':'+(i%2?'30':'00');time.add(new Option(t,t));}time.value='12:00';function persist(){localStorage.sunBoxes=JSON.stringify(boxes);localStorage.sunOrders=JSON.stringify(orders)}function dataLayer(){return window.CateriumDataV1772||window.CateriumDataV1771||window.CateriumData||window.CateriumDataV1770||null}function persistOrders(reason){let d=dataLayer();if(d?.orders?.replace){d.orders.replace(orders,{persistLocal:true,reason:reason||'legacy.orders'});return}persist()}function persistCatalog(reason){let d=dataLayer();if(d?.catalog?.replace){d.catalog.replace(boxes,{persistLocal:true,reason:reason||'legacy.catalog'});return}persist()}function show(id,b){document.querySelectorAll('.view').forEach(x=>x.classList.remove('on'));document.getElementById(id).classList.add('on');document.querySelectorAll('nav button').forEach(x=>x.classList.remove('on'));b.classList.add('on');renderOrders();renderStats()}function money(n){return Number(n||0).toLocaleString('ru-RU')+' ₽'}function calcOrderTotal(){return draft.lines.reduce((sum,l)=>{let b=boxes.find(x=>x.id==l.id);return sum+(Number(b?.price||0)*Number(l.qty||0))},0)}function updateOrderSummary(){let total=calcOrderTotal(),paid=Math.max(0,Number(prepayment.value||0)),rest=Math.max(0,total-paid);orderTotal.value=total;balance.value=rest;summaryTotal.textContent=money(total);summaryPrepayment.textContent=money(paid);summaryBalance.textContent=money(rest)}function render(){tiles.innerHTML='<button class="tile add" onclick="editBox(null)">+<br>Добавить бокс</button>'+boxes.map(b=>`<button class="tile" onclick="add('${sunId(b.id)}')">${sunImg(b.photo)?`<img loading="lazy" decoding="async" src="${sunAttr(sunImg(b.photo))}">`:'<div class="ph"><img src="sun-logo.png" class="sun-ph-logo" alt="Логотип Солнце Кейтеринг"></div>'}<span>${sunEsc(b.name)}</span><small>${money(b.price||0)}</small></button>`).join('');lines.innerHTML=draft.lines.length?draft.lines.map(l=>{let b=boxes.find(x=>x.id==l.id);return `<div class="line"><span>${sunEsc(b.name)}<small style="display:block;color:#68717a">${money(b.price||0)} × ${l.qty} = ${money(Number(b.price||0)*l.qty)}</small></span><button onclick="qty('${sunId(l.id)}',${l.qty-1})">−</button><input type="number" value="${l.qty}" onchange="qty('${sunId(l.id)}',+this.value)"><button onclick="qty('${sunId(l.id)}',${l.qty+1})">+</button></div>`}).join(''):'<p class="empty">Выберите боксы слева.</p>';updateOrderSummary()}function add(id){let l=draft.lines.find(x=>x.id==id);l?l.qty++:draft.lines.push({id,qty:1});render()}function qty(id,n){if(n<1)draft.lines=draft.lines.filter(x=>x.id!=id);else draft.lines.find(x=>x.id==id).qty=n;render()}function details(on){orderForm.style.display=on?'none':'block';const detailsPanel=document.getElementById('details');detailsPanel.style.display=on?'block':'none';orderTab.classList.toggle('on',!on);detailTab.classList.toggle('on',on)}function saveOrder(){if(!draft.lines.length)return alert('Добавьте хотя бы один бокс.');draft.event=document.getElementById('event').value;draft.date=date.value;draft.time=time.value;draft.contact=contact.value;draft.phone=phone.value;draft.address=address.value;draft.note=note.value;draft.total=calcOrderTotal();draft.prepayment=Math.max(0,Number(prepayment.value||0));draft.balance=Math.max(0,draft.total-draft.prepayment);if(!draft.status)draft.status='Новый';if(!draft.id)draft.id=Math.max(0,...orders.map(x=>x.id))+1;let n=orders.findIndex(x=>x.id==draft.id);n<0?orders.push(structuredClone(draft)):orders[n]=structuredClone(draft);persistOrders('order.save');deleteOrderBtn.style.display='inline-block';renderOrders();updateOrderSummary();alert('Заказ сохранён.');}function orderTotalValue(o){if(Number.isFinite(Number(o.total)))return Number(o.total);return (o.lines||[]).reduce((sum,l)=>{let b=boxes.find(x=>x.id==l.id);return sum+Number(b?.price||0)*Number(l.qty||0)},0)}function renderOrders(){ordersList.innerHTML=orders.length?orders.map(o=>`<div class="row" style="grid-template-columns:65px 1.15fr 145px 1fr 110px 175px 150px"><span>№ ${sunInt(o.id)}</span><span>${sunEsc(o.event)}</span><span>${sunEsc(o.date)} · ${sunEsc(o.time)}</span><span>${sunEsc(o.address||'—')}</span><b>${money(orderTotalValue(o))}</b><select onchange="status(${sunInt(o.id)},this.value)">${['Новый','В работе','Собран','Передан курьеру','Отдан заказчику','Отменён'].map(s=>`<option ${s==(o.status||'Новый')?'selected':''}>${s}</option>`).join('')}</select><span class="order-actions"><button class="mini" onclick="openOrder(${sunInt(o.id)})">Изменить</button><button class="mini danger" onclick="deleteOrderById(${sunInt(o.id)})">Удалить</button></span></div>`).join(''):'<p class="empty">Сохранённых заказов пока нет.</p>'}function status(id,s){orders.find(x=>x.id==id).status=s;persistOrders('order.status')}function openOrder(id){draft=structuredClone(orders.find(x=>x.id==id));document.getElementById('event').value=draft.event;date.value=draft.date;time.value=draft.time;contact.value=draft.contact||'';phone.value=draft.phone||'';address.value=draft.address||'';note.value=draft.note||'';prepayment.value=Number(draft.prepayment||0);deleteOrderBtn.style.display='inline-block';show('new',[...document.querySelectorAll('nav button')].find(x=>x.textContent.trim()==='Новый заказ')||document.querySelector('nav button'));render()}function resetDraft(){draft={id:0,lines:[]};document.getElementById('event').value='';date.value=new Date().toISOString().slice(0,10);time.value='12:00';contact.value='';phone.value='';address.value='';note.value='';prepayment.value=0;deleteOrderBtn.style.display='none';render()}function deleteOrderById(id){let o=orders.find(x=>x.id==id);if(!o||!confirm(`Удалить заказ №${id} «${o.event}»?`))return;orders=orders.filter(x=>x.id!=id);persistOrders('order.delete');renderOrders();if(draft.id==id)resetDraft()}function deleteOrder(){if(!draft.id)return;deleteOrderById(draft.id);show('orders',[...document.querySelectorAll('nav button')].find(x=>x.textContent.trim()==='Заказы')||document.querySelectorAll('nav button')[1])}function renderStats(){statsList.innerHTML=boxes.map(b=>`<p><b>${sunEsc(b.name)}</b> — ${orders.filter(o=>o.status!='Отменён').reduce((n,o)=>n+(o.lines.find(l=>l.id==b.id)?.qty||0),0)} заказано</p>`).join('')}function modal(x,on=true){document.getElementById(x).classList.toggle('on',on)}function closeModal(x){modal(x,false)}function openManager(){modal('manager');managerList.innerHTML=boxes.map(b=>`<button onclick="editBox('${sunId(b.id)}')">${sunImg(b.photo)?`<img loading="lazy" decoding="async" src="${sunAttr(sunImg(b.photo))}">`:'<div class="ph"><img src="sun-logo.png" class="sun-ph-logo" alt="Логотип Солнце Кейтеринг"></div>'}<b>${sunEsc(b.name)}</b><small>${b.ingredients.length} позиций</small></button>`).join('')}function editBox(id){edited=id?structuredClone(boxes.find(x=>x.id==id)):{id:'',name:'Новый бокс',price:0,ingredients:[]};editTitle.textContent=edited.id?'Изменить бокс':'Новый бокс';boxName.value=edited.name;boxPrice.value=Number(edited.price||0);editPhoto.style.display=edited.photo?'block':'none';editPhoto.src=sunImg(edited.photo)||'';deleteBox.style.display=edited.id?'block':'none';modal('editor');renderIngredients()}function readPhoto(x){let r=new FileReader();r.onload=()=>{edited.photo=sunImg(r.result);editPhoto.src=edited.photo||'';editPhoto.style.display='block'};r.readAsDataURL(x.files[0])}function renderIngredients(){ingredients.innerHTML=edited.ingredients.map((x,i)=>`<div class="ingredient"><input value="${sunAttr(x[0])}" onchange="edited.ingredients[${i}][0]=this.value"><input type="number" value="${x[1]}" onchange="edited.ingredients[${i}][1]=+this.value"><input value="${sunAttr(x[2])}" onchange="edited.ingredients[${i}][2]=this.value"><button onclick="edited.ingredients.splice(${i},1);renderIngredients()">×</button></div>`).join('')}function addIngredient(){edited.ingredients.push(['Новый продукт',1,'шт.']);renderIngredients()}function saveBox(){edited.name=boxName.value.trim();edited.price=Math.max(0,Number(boxPrice.value||0));if(!edited.name)return alert('Введите название.');if(!edited.id){edited.id=sunUUID();boxes.push(edited)}else boxes[boxes.findIndex(x=>x.id==edited.id)]=edited;persistCatalog('catalog.save');closeModal('editor');closeModal('manager');render();renderOrders();window.sunClientOfferCatalogChanged?.(edited.id)}function removeBox(){if(confirm('Удалить бокс?')){boxes=boxes.filter(x=>x.id!=edited.id);persistCatalog('catalog.delete');closeModal('editor');closeModal('manager');render()}}function openPreview(){let m={};draft.lines.forEach(l=>boxes.find(b=>b.id==l.id).ingredients.forEach(x=>{let k=x[0]+'|'+x[2];m[k]=(m[k]||0)+x[1]*l.qty}));prepTitle.textContent='Заготовки для: '+document.getElementById('event').value;prep.innerHTML=Object.entries(m).map(([k,v])=>{let [p,u]=k.split('|');return `<p><b>${sunEsc(p)}</b><span>${sunEsc(v)} ${sunEsc(u)}</span></p>`}).join('');modal('preview')}render();updateOrderSummary(); |