caterium-app/public/core/trial-demo.js

99 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

(()=>{
'use strict';
if(window.CateriumTrialDemo)return;
const KEY='sunTrialDemoV1',BATCH='trial-demo-v1';
const $=id=>document.getElementById(id);
const esc=x=>String(x??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
const money=x=>new Intl.NumberFormat('ru-RU',{maximumFractionDigits:2}).format(Number(x)||0)+' ₽';
const num=x=>new Intl.NumberFormat('ru-RU',{maximumFractionDigits:3}).format(Number(x)||0);
const read=(key,fallback)=>{try{return JSON.parse(localStorage.getItem(key))??fallback}catch(_){return fallback}};
const cloud=()=>window.SunCloudV2;
const data=()=>window.CateriumDataV1773;
const catalog=()=>data()?.catalog.list()||read('sunBoxes',[]);
const installed=()=>read(KEY,{})?.version===1;
const signedIn=()=>Boolean(cloud()?.getSession?.()?.user&&cloud()?.getWorkspace?.()?.id);
const writable=()=>signedIn()&&!cloud()?.isSupportMode?.()&&cloud()?.hasPermission?.('orders.create')&&window.SunSaaSV16?.isWritable?.()!==false;
let status=null,statusWorkspace='',pending=null,epoch=0,busy=false;
function navigate(id){const label={stock:'Склад',shopping:'Закупки'}[id];const nav=[...document.querySelectorAll('header nav button')].find(b=>String(b.dataset.navLabel||b.textContent||'').trim()===label);if(nav)nav.click();else window.show?.(id);}
function message(text){const el=$('ctDemoMessage');if(el)el.textContent=text;}
async function refreshStatus(){
const c=cloud(),ws=c?.getWorkspace?.()?.id;
if(!signedIn()||c?.isSupportMode?.()){status=null;statusWorkspace='';render();return;}
if(statusWorkspace===ws||pending)return;
const ticket=epoch;pending=ws;
try{const result=await c.getClient?.()?.rpc('caterium_trial_demo_status',{p_workspace:ws});
if(ticket!==epoch||c.getWorkspace?.()?.id!==ws)return;
if(result?.error)throw result.error;
status=result?.data||null;statusWorkspace=ws;
}catch(_){status=null;}finally{if(ticket===epoch){pending=null;render();}}
}
function render(){
const host=$('new');if(!host)return;
const items=catalog().filter(b=>b.demo&&b.ttk),active=installed()&&items.length>0;
if(!signedIn()||cloud()?.isSupportMode?.()||(!active&&!status?.canInstall)){$('ctTrialDemo')?.remove();return;}
let card=$('ctTrialDemo');if(!card){card=document.createElement('section');card.id='ctTrialDemo';card.className='card';host.prepend(card);}
const signature=JSON.stringify({active,items:items.map(x=>[x.id,x.name]),can:writable()});
if(card.dataset.signature===signature)return;card.dataset.signature=signature;
card.innerHTML=active?`<div class="ct-demo-heading"><div><strong>Демонстрационная база</strong><p>${items.length} боксов с фото и ТТК · учебные остатки и поставщики</p></div><span class="ct-demo-badge">Можно пробовать</span></div>
<p>Создайте пробный заказ, посмотрите нехватку в закупках, оформите приход на складе и спишите продукты по заказу. Все данные можно редактировать.</p>
<div class="ct-demo-actions"><button class="primary" data-demo-order ${writable()?'':'disabled'}>Создать пробный заказ</button><button class="outline" data-demo-go="stock">Склад</button><button class="outline" data-demo-go="shopping">Закупки</button></div>
<details><summary>Технологические карты боксов</summary><div class="ct-demo-actions"><label>Бокс<select id="ctDemoBox">${items.map(b=>`<option value="${esc(b.id)}">${esc(b.name)}</option>`).join('')}</select></label><button class="outline" data-demo-ttk>Открыть ТТК</button></div></details><p id="ctDemoMessage" role="status" aria-live="polite"></p>`:
`<div class="ct-demo-heading"><div><strong>Попробуйте приложение на готовых боксах</strong><p>10 боксов с фото и ТТК, 43 продукта, 5 учебных поставщиков и начальные остатки.</p></div></div><button class="primary" data-demo-install>Загрузить демо</button><p id="ctDemoMessage" role="status" aria-live="polite"></p>`;
card.querySelector('[data-demo-install]')?.addEventListener('click',install);
card.querySelector('[data-demo-order]')?.addEventListener('click',createOrder);
card.querySelector('[data-demo-ttk]')?.addEventListener('click',()=>showTTK($('ctDemoBox').value));
card.querySelectorAll('[data-demo-go]').forEach(b=>b.addEventListener('click',()=>navigate(b.dataset.demoGo)));
}
async function install(){
if(busy)return;busy=true;const btn=$('ctTrialDemo')?.querySelector('[data-demo-install]');if(btn)btn.disabled=true;
message('Загружаю демонстрационную базу…');
try{await cloud().installTrialDemo();status=null;statusWorkspace='';render();message('Готово. Создайте пробный заказ или выберите боксы самостоятельно.');await refreshStatus();}
catch(error){message(error?.message||'Не удалось загрузить демо. Повторите попытку.');}
finally{busy=false;if(btn?.isConnected)btn.disabled=false;}
}
function createOrder(){
if(!writable()||!installed())return;
const repo=data()?.orders;if(!repo){message('Приложение ещё загружается. Повторите через несколько секунд.');return;}
const all=repo.list(),existing=all.find(o=>o.demoBatch===BATCH);
if(existing){message(`Пробный заказ №${existing.id} уже создан. Его можно открыть во вкладке «Заказы».`);return;}
const spec=read(KEY,{}).scenario?.lines,items=catalog();
if(!Array.isArray(spec)||spec.length!==3){message('Сценарий недоступен. Добавьте боксы в заказ самостоятельно.');return;}
const lines=spec.map(l=>{const box=items.find(b=>String(b.id)===String(l.id));return box?{id:box.id,name:box.name,qty:l.qty,price:Number(box.price)||0}:null});
if(lines.some(l=>!l)){message('Некоторые демо-боксы удалены. Добавьте оставшиеся в заказ самостоятельно.');return;}
const total=lines.reduce((s,l)=>s+l.price*l.qty,0),day=new Date();day.setDate(day.getDate()+1);
const date=`${day.getFullYear()}-${String(day.getMonth()+1).padStart(2,'0')}-${String(day.getDate()).padStart(2,'0')}`;
const id=Math.max(0,...all.map(o=>Number(o.id)||0))+1;
repo.replace([...all,{id,event:'Демо: фуршет',date,time:'12:00',contact:'',phone:'',address:'',note:'Учебный заказ для проверки склада и закупки. Автозавершение отключено.',status:'Новый',lines,total,prepayment:0,balance:total,demo:true,demoBatch:BATCH,autoCompletionDisabled:true}],{reason:'trial-demo.order'});
window.renderOrders?.();window.sunRefreshStock?.();
message(`Заказ №${id} на ${money(total)} создан на завтра. Откройте «Закупки», чтобы увидеть нехватку продуктов.`);
}
function showTTK(id){
const box=catalog().find(b=>String(b.id)===String(id));if(!box?.ttk)return;
$('ctDemoTtk')?.remove();const t=box.ttk,dialog=document.createElement('dialog');dialog.id='ctDemoTtk';
const changed=JSON.stringify(box.ingredients)!==JSON.stringify(t.rows.map(r=>[r.name,r.gross,r.unit]));
dialog.innerHTML=`<div class="ct-demo-heading"><div><small>${esc(t.number)} · ${esc(t.basis)}</small><h2>${esc(box.name)}</h2></div><button class="outline" data-ttk-close aria-label="Закрыть ТТК">×</button></div>
<p>${changed?'Исходный выход':'Выход'}: <b>${num(t.outputGrams)} г${t.pieces?` · ${num(t.pieces)} шт.`:''}</b> · Цена бокса: <b>${money(box.price)}</b></p>
${changed?'<p class="ct-demo-notice">Состав изменён. Закупка и списание используют текущий состав из редактора. Ниже показана исходная учебная ТТК.</p>':''}
<div class="ct-demo-table"><table><thead><tr><th>Продукт / упаковка</th><th>Ед.</th><th>Брутто</th><th>Нетто</th><th>Цена за ед.</th><th>Сумма</th></tr></thead><tbody>${t.rows.map(r=>`<tr><td>${esc(r.name)}</td><td>${esc(r.unit)}</td><td>${num(r.gross)}</td><td>${num(r.net)}</td><td>${money(r.unitCost)}</td><td>${money(r.gross*r.unitCost)}</td></tr>`).join('')}</tbody></table></div>
<p><b>Продукты и упаковка: ${money(t.ingredientCost)}</b></p><p class="hint">Учебные закупочные цены. Работа, доставка и накладные расходы не включены. Расход со склада считается по брутто.</p>
<h3>Приготовление и сборка</h3><ol>${t.steps.map(s=>`<li>${esc(s)}</li>`).join('')}</ol><p><b>Аллергены:</b> ${t.allergens.map(esc).join(', ')}.</p><p class="hint">${esc(t.note)}</p>
<div class="ct-demo-actions"><button class="outline" data-ttk-edit>Изменить состав и цену</button><button class="primary" data-ttk-close>Закрыть</button></div>`;
document.body.append(dialog);dialog.querySelectorAll('[data-ttk-close]').forEach(b=>b.onclick=()=>dialog.close());
dialog.querySelector('[data-ttk-edit]').onclick=()=>{dialog.close();window.editBox?.(box.id)};
dialog.addEventListener('close',()=>dialog.remove(),{once:true});dialog.showModal();
}
function reset(){epoch++;pending=null;status=null;statusWorkspace='';$('ctTrialDemo')?.remove();$('ctDemoTtk')?.remove();}
function boot(){
const style=document.createElement('style');style.textContent=`#ctTrialDemo{margin:0 0 18px;padding:18px 22px}#ctTrialDemo p{font-size:13px;line-height:1.5;margin:6px 0 12px;max-width:900px}.ct-demo-heading{display:flex;justify-content:space-between;gap:14px;align-items:flex-start}.ct-demo-heading strong{font-size:18px}.ct-demo-heading h2{font-size:22px;margin:8px 0}.ct-demo-badge{border:1px solid #c3d4bd;border-radius:18px;padding:5px 10px;font-size:12px;white-space:nowrap}.ct-demo-actions{display:flex;gap:10px;align-items:end;flex-wrap:wrap;margin:12px 0}.ct-demo-actions label{min-width:0;max-width:100%;flex:1}.ct-demo-actions select{width:100%;font:inherit;padding:9px}.ct-demo-actions button{max-width:100%}#ctTrialDemo details{font-size:13px;margin-top:14px}#ctTrialDemo summary{cursor:pointer}#ctDemoMessage:empty{display:none}#ctDemoTtk{width:min(860px,calc(100vw - 24px));max-height:90dvh;box-sizing:border-box;overflow:auto;border:1px solid #d4c8b0;border-radius:18px;padding:24px;background:var(--card,#fff);color:var(--text,#27241e)}#ctDemoTtk::backdrop{background:#0008}.ct-demo-table{overflow-x:auto}.ct-demo-table table{border-collapse:collapse;min-width:580px;width:100%;font-size:13px}.ct-demo-table td,.ct-demo-table th{text-align:right;padding:9px 7px;border-bottom:1px solid #ddd}.ct-demo-table th:first-child,.ct-demo-table td:first-child{text-align:left}#ctDemoTtk li{margin:8px 0;line-height:1.5}.ct-demo-notice{background:#fff3d6;color:#604918;padding:12px;border-radius:8px}@media(max-width:600px){#ctTrialDemo{padding:14px}.ct-demo-heading{flex-wrap:wrap}.ct-demo-actions>button{flex:1 1 auto}.ct-demo-actions label{flex-basis:100%}#ctDemoTtk{padding:16px}}@media print{#ctTrialDemo{display:none!important}}`;
document.head.append(style);
window.addEventListener('sun:cloud-tenant-changing',reset);
window.addEventListener('sun:cloud-permissions-changed',()=>{render();refreshStatus()});
window.addEventListener('sun:cloud-state-applied',()=>{render();refreshStatus()});
window.addEventListener('sun:subscription-changed',()=>{statusWorkspace='';render();refreshStatus()});
document.addEventListener('click',e=>{if(e.target.closest('header nav button')){render();refreshStatus()}});
data()?.subscribe('catalog',render);setTimeout(()=>{render();refreshStatus()},1400);
}
window.CateriumTrialDemo={render,refreshStatus,showTTK};
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',boot,{once:true});else boot();
})();