/* Optional learning examples. Visibility belongs to a profile in a workspace; records stay available for existing orders, recipes and document snapshots. */ (()=>{ 'use strict'; if(window.CateriumTrainingCatalog)return; const KEY='sunTrialDemoV1',EVENT='caterium:training-catalog-changed'; const IDS={"catalog":["demo-v1-bruschetta-tomato","demo-v1-salmon-cream","demo-v1-chicken-sandwich","demo-v1-caprese","demo-v1-mushroom-tartlet","demo-v1-turkey-wrap","demo-v1-cheese-fruit","demo-v1-meat-assortment","demo-v1-vegetables-hummus","demo-v1-berry-dessert","demo-banquet-v1-caprese","demo-banquet-v1-roastbeef","demo-banquet-v1-salmon-roll","demo-banquet-v1-hummus","demo-banquet-v1-caesar","demo-banquet-v1-olivier","demo-banquet-v1-greek","demo-banquet-v1-julienne","demo-banquet-v1-stuffed-mushrooms","demo-banquet-v1-chicken","demo-banquet-v1-cod","demo-banquet-v1-beef-hot","demo-banquet-v1-mash","demo-banquet-v1-rice","demo-banquet-v1-berry-cream","demo-banquet-v1-cheese-honey","demo-banquet-v1-fruit","demo-banquet-v1-bread","demo-extras-v1-salmon-caprese","demo-extras-v1-meat-cheese","demo-extras-v1-mini-buffet","demo-extras-v1-water","demo-extras-v1-sparkling","demo-extras-v1-juice","demo-extras-v1-mors","demo-extras-v1-plate","demo-extras-v1-fork","demo-extras-v1-glass","demo-extras-v1-napkin","demo-extras-v1-ice","demo-extras-v1-tablecloth","demo-extras-v1-serving-kit","demo-extras-v1-delivery-city","demo-extras-v1-delivery-outer"],"stock":["demo-v1-stock-baguette","demo-v1-stock-tomato","demo-v1-stock-basil","demo-v1-stock-oil","demo-v1-stock-rye","demo-v1-stock-salmon","demo-v1-stock-cream","demo-v1-stock-cucumber","demo-v1-stock-dill","demo-v1-stock-toast","demo-v1-stock-chicken","demo-v1-stock-lettuce","demo-v1-stock-mozzarella","demo-v1-stock-cherry","demo-v1-stock-tartlet","demo-v1-stock-mushroom","demo-v1-stock-cooking-cream","demo-v1-stock-gouda","demo-v1-stock-onion","demo-v1-stock-tortilla","demo-v1-stock-turkey","demo-v1-stock-pepper","demo-v1-stock-brie","demo-v1-stock-blue","demo-v1-stock-grape","demo-v1-stock-walnut","demo-v1-stock-honey","demo-v1-stock-cracker","demo-v1-stock-salami","demo-v1-stock-beef","demo-v1-stock-pickle","demo-v1-stock-olive","demo-v1-stock-mustard","demo-v1-stock-carrot","demo-v1-stock-hummus","demo-v1-stock-biscuit","demo-v1-stock-strawberry","demo-v1-stock-blueberry","demo-v1-stock-sugar","demo-v1-stock-box","demo-v1-stock-skewer","demo-v1-stock-cup","demo-v1-stock-sauce-cup","demo-banquet-v1-stock-potato","demo-banquet-v1-stock-butter","demo-banquet-v1-stock-milk","demo-banquet-v1-stock-egg","demo-banquet-v1-stock-peas","demo-banquet-v1-stock-chicken-raw","demo-banquet-v1-stock-whitefish","demo-banquet-v1-stock-lemon","demo-banquet-v1-stock-rice","demo-extras-v1-stock-water","demo-extras-v1-stock-sparkling","demo-extras-v1-stock-juice","demo-extras-v1-stock-mors","demo-extras-v1-stock-plate","demo-extras-v1-stock-fork","demo-extras-v1-stock-glass","demo-extras-v1-stock-napkin","demo-extras-v1-stock-ice","demo-extras-v1-stock-tablecloth","demo-extras-v1-stock-serving-kit"],"suppliers":["demo-v1-supplier-fresh","demo-v1-supplier-protein","demo-v1-supplier-bakery","demo-v1-supplier-grocery","demo-v1-supplier-pack"]}; const sets=Object.fromEntries(Object.entries(IDS).map(([k,v])=>[k,new Set(v)])); const $=id=>document.getElementById(id),copy=v=>JSON.parse(JSON.stringify(v)); const cloud=()=>window.SunCloudV2,repo=()=>window.CateriumDataV1773; let changing=false,epoch=0,busy=false,desired=null,bundlePromise=null,lastState='',note=''; function read(key,fallback){const raw=localStorage.getItem(key);if(raw===null)return copy(fallback);return JSON.parse(raw);} function metadata(){try{const m=read(KEY,{});return m&&typeof m==='object'&&!Array.isArray(m)?m:{}}catch(_){return {}}} function context(){const c=cloud();return {user:String(c?.getSession?.()?.user?.id||''),workspace:String(c?.getWorkspace?.()?.id||'')};} function enabled(){ if(changing)return false; const c=context(),s=metadata().trainingCatalog; return Boolean(c.user&&c.workspace&&s?.workspaceId===c.workspace&&s?.profiles?.[c.user]===true); } function isSample(item,kind='catalog'){ return Boolean(item&&((sets[kind]||sets.catalog).has(String(item.id))||(kind==='stock'&&item.trainingCatalog===true))); } function visible(item,kind='catalog'){return item?.hidden!==true&&(!isSample(item,kind)||enabled());} function badge(item){return isSample(item)&&enabled()?'Учебное':'';} function canManage(){ const c=cloud(),who=context(); return !changing&&Boolean(who.user&&who.workspace)&&!c?.isSupportMode?.()&&!c?.getSupportMode?.()&& (c?.hasPermission?.('catalog.edit')===true||c?.hasPermission?.('app.write')===true)&&window.SunSaaSV16?.isWritable?.()!==false; } async function loadBundle(){ if(bundlePromise)return bundlePromise; bundlePromise=(async()=>{ const controller=new AbortController(),timeout=setTimeout(()=>controller.abort(),15000); try{ const [base,banquet,extras]=await Promise.all(['catalog-v1','banquet-v1','extras-v1'].map(async name=>{ const r=await fetch(`demo/${name}.json?v=20260920-training`,{signal:controller.signal,credentials:'same-origin'}); if(!r.ok)throw new Error('Не удалось загрузить учебный каталог. Проверьте интернет и повторите.'); const value=await r.json();if(value?.version!==1)throw new Error('Неизвестная версия учебного каталога.');return value; })); if(![base.boxes,base.stock,base.suppliers,banquet.banquet,banquet.stock,extras.items,extras.stock].every(Array.isArray))throw new Error('Учебный каталог повреждён.'); const result={boxes:[...base.boxes,...banquet.banquet,...extras.items],stock:[...base.stock,...banquet.stock,...extras.stock],suppliers:base.suppliers,scenario:base.scenario,batch:base.batch}; for(const [domain,rows] of Object.entries({catalog:result.boxes,stock:result.stock,suppliers:result.suppliers})){ if(rows.length!==sets[domain].size||new Set(rows.map(x=>x?.id)).size!==rows.length||rows.some(x=>!sets[domain].has(x?.id)||typeof x?.name!=='string'))throw new Error('Неполный учебный каталог.'); } return result; }finally{clearTimeout(timeout);} })().catch(error=>{bundlePromise=null;throw error;}); return bundlePromise; } // Pure additive merge: never overwrite a user's prices, recipes, inventory, // supplier contacts or orders. New inventory has distinct names so a test // recipe does not resolve to an existing real product with the same name. function merge(seed,current){ for(const domain of ['boxes','stock','suppliers'])if(!Array.isArray(current[domain]))throw new Error('Рабочие данные ещё не загружены. Повторите после загрузки.'); const next=copy(current),maps=Object.fromEntries(['boxes','stock','suppliers'].map(k=>[k,new Map(next[k].map(x=>[String(x.id),x]))])); const sku=(name,unit)=>`${String(name).trim().toLowerCase()}|${String(unit).trim().toLowerCase()}`; const names=new Set(next.stock.map(p=>sku(p.name,p.unit))); for(const source of seed.suppliers)if(!maps.suppliers.has(source.id)){ const row={...copy(source),trainingCatalog:true};next.suppliers.push(row);maps.suppliers.set(row.id,row); } for(const source of seed.stock)if(!maps.stock.has(source.id)){ let name=`Учебное: ${source.name.replace(/^Демо:\s*/,'')}`,n=2; while(names.has(sku(name,source.unit)))name=`Учебное: ${source.name} (${n++})`; const row={...copy(source),name,qty:0,min:0,trainingCatalog:true};next.stock.push(row);maps.stock.set(row.id,row);names.add(sku(name,row.unit)); } const byIngredient=new Map(seed.stock.map(s=>[sku(s.name,s.unit),maps.stock.get(s.id)])); for(const source of seed.boxes)if(!maps.boxes.has(source.id)){ const row=copy(source);row.demo=true; row.ingredients=(row.ingredients||[]).map(part=>{const p=byIngredient.get(sku(part[0],part[2]));return p?[p.name,...part.slice(1)]:part;}); if(row.ttk?.rows)row.ttk.rows=row.ttk.rows.map(r=>{const p=maps.stock.get(r.productId);return p?{...r,name:p.name}:r;}); next.boxes.push(row);maps.boxes.set(row.id,row); } return next; } function liveCatalog(){return repo()?.catalog?.list?.()||read('sunBoxes',[]);} function notify(){ renderSettings();window.CateriumTrialDemo?.render?.(); window.dispatchEvent(new CustomEvent(EVENT)); // Only selection UI is refreshed. Stored orders, their line prices and the // editor node are not filtered, deleted, regenerated or repriced here. window.render?.();window.sunRenderSuppliers?.(); } async function setEnabled(value){ if(busy)throw new Error('Дождитесь завершения загрузки учебного каталога.'); if(typeof value!=='boolean')throw new Error('Некорректное значение переключателя.'); if(!canManage())throw new Error('Для изменения учебного каталога нужны права редактирования каталога в своей компании.'); const ticket=epoch,who=context();busy=true;desired=value;note=value?'Загружаю учебные примеры…':'';renderSettings(); try{ const seed=value?await loadBundle():null; if(ticket!==epoch||JSON.stringify(context())!==JSON.stringify(who))throw new Error('Профиль или компания сменились. Изменение отменено.'); if(!canManage())throw new Error('Права изменились. Учебный каталог не изменён.'); const meta=read(KEY,{});if(!meta||typeof meta!=='object'||Array.isArray(meta))throw new Error('Не удалось прочитать настройки. Ничего не изменено.'); const oldMeta=meta.trainingCatalog; const nextMeta={...meta,trainingCatalog:{workspaceId:who.workspace,profiles:{...(oldMeta?.workspaceId===who.workspace?oldMeta.profiles:{}),[who.user]:value}}}; const previous={sunBoxes:localStorage.getItem('sunBoxes'),sunStock:localStorage.getItem('sunStock'),sunSuppliers:localStorage.getItem('sunSuppliers'),[KEY]:localStorage.getItem(KEY)}; const before=liveCatalog();let next=null; if(value){ next=merge(seed,{boxes:before,stock:read('sunStock',[]),suppliers:read('sunSuppliers',[])}); nextMeta.version=1;nextMeta.batch=meta.batch||seed.batch;nextMeta.scenario=meta.scenario||seed.scenario; } try{ if(next){ // Commit synchronously before cloud autosync takes its next snapshot. // persistLocal:false writes only catalog, not unrelated order domains. localStorage.setItem('sunStock',JSON.stringify(next.stock)); localStorage.setItem('sunSuppliers',JSON.stringify(next.suppliers)); repo().catalog.replace(next.boxes,{persistLocal:false,reason:'training-catalog.enable'}); } localStorage.setItem(KEY,JSON.stringify(nextMeta)); }catch(error){ for(const [key,raw] of Object.entries(previous)){try{if(raw===null)localStorage.removeItem(key);else localStorage.setItem(key,raw);}catch(_){}} try{if(typeof boxes!=='undefined'){boxes.length=0;boxes.push(...copy(before));}}catch(_){} throw new Error('Не удалось сохранить учебный каталог. Проверьте свободное место и повторите.'); } note=value?'Учебный каталог включён. Откройте «Меню» или «Новый заказ».':'Учебные позиции скрыты. Ваши данные и созданные заказы сохранены.'; lastState=stateSignature();notify();return true; }catch(error){if(ticket===epoch)note=error?.name==='AbortError'?'Загрузка заняла слишком долго. Проверьте интернет и повторите.':error.message;throw error;} finally{if(ticket===epoch){busy=false;desired=null;renderSettings();}} } function stateSignature(){const c=context();return JSON.stringify([c.user,c.workspace,enabled(),canManage()]);} function renderSettings(){ const host=$('enterprise-settings')?.querySelector('.enterprise-grid')||$('enterprise-settings'); const who=context();if(!host)return; if(changing||!who.user||!who.workspace){$('ctTrainingSettings')?.remove();return;} let card=$('ctTrainingSettings'); if(!card){ card=document.createElement('section');card.id='ctTrainingSettings';card.className='enterprise-card'; card.innerHTML='
Фотографии, составы, технологические карты, учебные цены и поставщики. Можно собрать пробный заказ, посмотреть предложение клиенту, заготовки и закупки — без ручного заполнения каталога.
Снимите галочку, чтобы скрыть учебные позиции. Свои блюда, изменения учебных карточек и созданные заказы не удаляются. Переключатель действует для вашего профиля в этой компании. Складские операции выполняются только по вашей команде.
'; host.prepend(card); card.querySelector('input').addEventListener('change',e=>{setEnabled(e.target.checked).catch(()=>{});}); } const check=$('ctTrainingEnabled');check.checked=busy?desired:enabled();check.disabled=busy||!canManage()||!repo()?.catalog; card.setAttribute('aria-busy',String(busy)); const access=!canManage()?'Для включения нужны права редактирования каталога. Режим просмотра не даёт дополнительных прав.':''; if($('ctTrainingAccess').textContent!==access)$('ctTrainingAccess').textContent=access; if($('ctTrainingStatus').textContent!==note)$('ctTrainingStatus').textContent=note; } function refresh(){ const signature=stateSignature();renderSettings(); if(signature!==lastState){lastState=signature;window.CateriumTrialDemo?.render?.();window.dispatchEvent(new CustomEvent(EVENT));window.render?.();} } function boot(){ const style=document.createElement('style');style.textContent=` #ctTrainingSettings{min-width:0;padding:18px;border:1px solid var(--sun-ui-border,#deded6);border-radius:14px;background:var(--sun-ui-card,#fff);color:var(--sun-ui-text,#27241e)} #ctTrainingSettings h2{margin:0 0 14px;font-size:18px}#ctTrainingSettings p{font-size:12px;line-height:1.5;margin:10px 0 0} #ctTrainingSettings .ct-training-toggle{display:flex!important;flex-direction:row!important;align-items:flex-start;gap:12px;margin:0;cursor:pointer} #ctTrainingSettings #ctTrainingEnabled{width:20px!important;height:20px!important;min-height:20px;margin:2px 0 0;flex:0 0 20px;accent-color:#b88c2e} #ctTrainingSettings .ct-training-toggle b{display:block;font-size:15px;line-height:1.4}#ctTrainingSettings .ct-training-toggle small{display:block;font-size:12px;line-height:1.5;color:var(--sun-ui-muted,#72766e)} #ctTrainingSettings input:focus-visible{outline:2px solid #b88c2e;outline-offset:3px}#ctTrainingStatus:empty,#ctTrainingAccess:empty{display:none} .ct-training-badge{display:inline-block!important;width:auto!important;max-width:100%;padding:2px 5px;margin:3px 0;border-radius:5px;background:#ece6d7;color:#695a37!important;font:700 10px/1.4 Arial,sans-serif!important;white-space:nowrap} @media print{#ctTrainingSettings{display:none!important}} `;document.head.append(style); window.addEventListener('sun:cloud-tenant-changing',()=>{changing=true;epoch++;busy=false;desired=null;note='';lastState='';$('ctTrainingSettings')?.remove();}); window.addEventListener('sun:cloud-state-applied',()=>{changing=false;refresh();}); window.addEventListener('sun:cloud-permissions-changed',()=>{changing=false;refresh();}); window.addEventListener('sun:subscription-changed',refresh); window.addEventListener('storage',e=>{if(e.key===KEY)refresh();}); document.addEventListener('click',e=>{if(e.target.closest?.('header nav button'))renderSettings();}); const attach=()=>{const host=$('enterprise-settings');if(!host)return false;renderSettings();new MutationObserver(()=>{if(!$('ctTrainingSettings'))renderSettings();}).observe(host,{childList:true,subtree:true});return true;}; if(!attach()){const waiting=new MutationObserver(()=>{if(attach())waiting.disconnect();});waiting.observe(document.body,{childList:true});} refresh(); } window.CateriumTrainingCatalog=Object.freeze({enabled,visible,isSample,badge,canManage,setEnabled,merge,refresh,renderSettings}); if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',boot,{once:true});else boot(); })();