From 716a79c58d6d6f687343d7dbdeea52ef1a1d668b Mon Sep 17 00:00:00 2001 From: pavlov346346-source Date: Sun, 20 Sep 2026 12:18:40 +0300 Subject: [PATCH] Optional per-profile training catalog with reversible example visibility (#35) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add Settings > Учебный каталог, off by default and available on ordinary writable profiles. Load ready example boxes, photos, TTKs and linked sample inventory additively; preserve own data, saved orders and edited examples when hiding or re-enabling. Respect company/profile scope, read-only permissions, failed downloads and tenant changes. Preserve the opened recipe guide across real catalog refreshes on iPhone. Integrated feature checks and full pull-request QA passed. Standard main QA and exact-asset production UI verification remain in place. --- .github/workflows/deploy-timeweb.yml | 4 + docs/releases/2026-09-20-TRAINING-CATALOG.md | 36 ++++ public/app-runtime.js | 22 +-- public/core/ops-ux-v1762.js | 5 +- public/core/training-catalog.js | 165 +++++++++++++++++++ public/core/trial-demo.js | 32 ++-- public/index.html | 48 +++--- public/legacy/bootstrap.js | 2 +- public/service-worker.js | 4 +- tests/playwright.config.mjs | 4 +- tests/production-training-catalog.mjs | 47 ++++++ tests/training-catalog-fixture.mjs | 17 ++ tests/training-catalog.spec.mjs | 96 +++++++++++ tests/trial-demo.spec.mjs | 2 +- tests/ui-stability.spec.mjs | 4 +- 15 files changed, 429 insertions(+), 59 deletions(-) create mode 100644 docs/releases/2026-09-20-TRAINING-CATALOG.md create mode 100644 public/core/training-catalog.js create mode 100644 tests/production-training-catalog.mjs create mode 100644 tests/training-catalog-fixture.mjs create mode 100644 tests/training-catalog.spec.mjs diff --git a/.github/workflows/deploy-timeweb.yml b/.github/workflows/deploy-timeweb.yml index 98b6d23..fafc8f4 100644 --- a/.github/workflows/deploy-timeweb.yml +++ b/.github/workflows/deploy-timeweb.yml @@ -54,6 +54,7 @@ jobs: core/banquet-menu.js core/data-layer-v1773.js core/trial-demo.js + core/training-catalog.js legacy/bootstrap.js caterium-mark-light.svg service-worker.js @@ -116,6 +117,9 @@ jobs: - name: Check the published mobile menu form and return arrow timeout-minutes: 4 run: node tests/production-mobile-menu.mjs + - name: Check the published optional training catalog + timeout-minutes: 4 + run: node tests/production-training-catalog.mjs - name: Save production UI verification if: always() uses: actions/upload-artifact@v4 diff --git a/docs/releases/2026-09-20-TRAINING-CATALOG.md b/docs/releases/2026-09-20-TRAINING-CATALOG.md new file mode 100644 index 0000000..c71ba83 --- /dev/null +++ b/docs/releases/2026-09-20-TRAINING-CATALOG.md @@ -0,0 +1,36 @@ +# Optional learning catalog + +Settings → Обучение и знакомство → Учебный каталог. + +Off by default, including previously automatically seeded demo records. Opting in +is not limited to a trial subscription or an empty company. Normal catalog edit +permissions, support read-only and subscription write restrictions still apply. + +Visibility is saved per authenticated user inside the current workspace's +sunTrialDemoV1.trainingCatalog metadata. Other profiles and workspaces do not +inherit the selection. A cloud-synced profile uses the same preference on its +other devices. This is an optional catalog, not a separate isolated account. + +Enabling loads the existing versioned example bundle: ten photographed boxes, +premium sets, eighteen banquet dishes, drinks, supplies, extras and delivery. +Composition, recipe/TTK, prices, sample suppliers and zero-balance inventory are +included. New sample inventory has distinct names to prevent matching a real +product with the same name during stock lookup. Historical seeded inventory is +not renamed and existing recipes are never overwritten. + +Disabling changes only selection visibility; it does not delete records, undo +manual stock operations, remove orders, change saved prices or reset edits. +Catalog/menu/search/stock/supplier browse screens hide the sample records. Lookup +for existing orders, preparation and documents continues using the full data. +Re-enabling is additive and idempotent; user-created products and existing sample +edits, inventory balances and supplier details are preserved. + +There is no automatic creation of orders or inventory movements. The existing +explicit trial-order button keeps autoCompletionDisabled. Stock actions still +require an explicit user command and existing permissions. No production database +migration, account mutation or other-project change is required for this release. + +Regression coverage includes first opt-in on a populated paid profile, switching +off and back on, reload, old seeded records, names/recipes/stock preservation, +TTKs and trial order, per-profile/workspace scope, read-only/support rejection, +failed downloads and late responses after a tenant change. diff --git a/public/app-runtime.js b/public/app-runtime.js index e24cdb5..51510dd 100644 --- a/public/app-runtime.js +++ b/public/app-runtime.js @@ -152,7 +152,7 @@ window.SUN_LEGACY_CATALOG_V175=[]; const buildPdf=pages=>window.SunPdfEngine.fromJpegs(pages); async function createCatalogPdfBlob(cat,progress){ - const items=allBoxes().filter(item=>item.hidden!==true&&Number(item.category||0)===Number(cat)); + const items=allBoxes().filter(item=>(window.CateriumTrainingCatalog?.visible(item)??(item.hidden!==true))&&Number(item.category||0)===Number(cat)); if(!items.length)throw new Error('В этом разделе нет позиций.'); const pages=[],dateText=new Date().toLocaleDateString('ru-RU'),brand=window.CateriumBranding.identity(); for(let i=0;icreateCatalogPdfBlob(currentCategory()); - window.sunCatalogDebugPdfPages=async(cat=currentCategory(),limit=3)=>{const items=allBoxes().filter(item=>item.hidden!==true&&Number(item.category||0)===Number(cat)).slice(0,Math.max(1,Number(limit||3)));const dateText=new Date().toLocaleDateString('ru-RU'),pages=[];for(let i=0;i{const items=allBoxes().filter(item=>(window.CateriumTrainingCatalog?.visible(item)??(item.hidden!==true))&&Number(item.category||0)===Number(cat)).slice(0,Math.max(1,Number(limit||3)));const dateText=new Date().toLocaleDateString('ru-RU'),pages=[];for(let i=0;i{if(e.target.closest('#new .cats button,#new .sun-catalog-top-toolbar .cats button'))setTimeout(installButton,30)},true); const catalog=document.querySelector('#new .catalog');if(catalog&&'MutationObserver'in window)new MutationObserver(()=>{const b=$('sunLiveCatalogPdfButton');if(!b||b.textContent!=='PDF'||b.onclick!==openCatalogPdfViewer)queueMicrotask(installButton)}).observe(catalog,{childList:true,subtree:true}); @@ -356,7 +356,7 @@ window.SUN_LEGACY_CATALOG_V175=[]; function renderCategoryManager(){ ensureCategoryModal();const root=$('sunCategoryRows');if(!root)return; root.innerHTML=catalogCats.slice().sort((a,b)=>a.order-b.order).map(c=>{ - const count=(typeof boxes!=='undefined'?boxes:[]).filter(b=>b.hidden!==true&&Number(b.category||0)===Number(c.id)).length; + const count=(typeof boxes!=='undefined'?boxes:[]).filter(b=>(window.CateriumTrainingCatalog?.visible(b)??(b.hidden!==true))&&Number(b.category||0)===Number(c.id)).length; return `
${count} поз.
`; }).join(''); qa('[data-cat-name]',root).forEach(input=>input.onchange=()=>{const c=catById(input.dataset.catName);if(!c)return;c.name=input.value.trim()||c.name;saveCats();renderCategoryTabs();syncEditorCategoryOptions();renderCatalogV5();renderCategoryManager();}); @@ -421,7 +421,7 @@ window.SUN_LEGACY_CATALOG_V175=[]; const fields=ensureBanquetEditor();if(!fields)return;const isBanquet=Number(category)===BANQUET_CATEGORY; fields.section.style.display=isBanquet?'flex':'none';fields.weight.style.display=isBanquet?'flex':'none'; const caption=$('boxPriceCaption');if(caption)caption.textContent=isBanquet?'Обычная цена на 1 гостя, ₽':'Обычная цена, ₽'; - window.CateriumBanquet?.editor(category,item,boxes.filter(i=>i.hidden!==true&&Number(i.category)===6)); + window.CateriumBanquet?.editor(category,item,boxes.filter(i=>(window.CateriumTrainingCatalog?.visible(i)??(i.hidden!==true))&&Number(i.category)===6)); if(isBanquet){if($('banquetSection'))$('banquetSection').value=item?.catalogSection||'';if($('banquetWeightGrams'))$('banquetWeightGrams').value=parseWeightGrams(item?.weight)||''} } function installIngredientEditorV175(){window.renderIngredients=()=>{if(!edited)return;normalizeBoxItem(edited);const root=$('ingredients');if(!root)return;const head='
Вид / названиеКол-воЕд.Вес 1 шт., г
';root.innerHTML=head+edited.ingredients.map((row,i)=>{const w=ingredientUnitWeight(row);return `
`}).join('')};window.addIngredient=()=>{if(!edited)return;if(!Array.isArray(edited.ingredients))edited.ingredients=[];edited.ingredients.push(['Новая позиция',1,'шт.',0]);window.renderIngredients();};} @@ -437,15 +437,15 @@ window.SUN_LEGACY_CATALOG_V175=[]; function wantedBoxNumber(query){const raw=String(query||'').trim();if(!raw)return null;const match=raw.match(/^(?:бокс\s*)?(?:№\s*)?0*(\d+)\s*$/i);return match?Number(match[1]):null} function itemMatches(item,query){if(!query)return true;if(Number(activeCat)===0){const wanted=wantedBoxNumber(query);return wanted!==null&&boxNumber(item)===wanted}const q=query.trim().toLowerCase();const parts=[item.name,item.catalogSection,item.weight,...(Array.isArray(item.composition)?item.composition:[]),...(Array.isArray(item.ingredients)?item.ingredients.map(x=>x?.[0]):[])];return parts.join(' ').toLowerCase().includes(q)} const BANQUET_CATEGORY=6; - function banquetCatalog(){return boxes.filter(item=>item.hidden!==true&&Number(item.category)===BANQUET_CATEGORY)} + function banquetCatalog(){return boxes.filter(item=>(window.CateriumTrainingCatalog?.visible(item)??(item.hidden!==true))&&Number(item.category)===BANQUET_CATEGORY)} window.toggleBanquetItem=id=>{window.CateriumBanquet.toggle(draft,banquetCatalog(),String(id));renderCatalogV5();}; function renderBanquetView(items){return window.CateriumBanquet.render({items,catalog:banquetCatalog(),draft})} function renderCatalogV5({catalogOnly=false}={}){ $('new')?.classList.toggle('sun-banquet-active',Number(activeCat)===BANQUET_CATEGORY); renderCategoryTabs();ensureCatalogTools();const catalogSearch=$('sunCatalogSearch');if(catalogSearch){const boxMode=[0,5].includes(Number(activeCat));catalogSearch.placeholder=boxMode?'Поиск по № бокса':'Поиск по позиции';catalogSearch.inputMode=boxMode?'numeric':'search';}const title=document.querySelector('#new .catalog h1');if(title)title.textContent=catName(activeCat); - const all=boxes.filter(item=>item.hidden!==true&&Number(item.category||0)===Number(activeCat)),items=all.filter(item=>itemMatches(item,catalogQuery));const cat=catById(activeCat)||{name:'Каталог',prep:true}; + const all=boxes.filter(item=>(window.CateriumTrainingCatalog?.visible(item)??(item.hidden!==true))&&Number(item.category||0)===Number(activeCat)),items=all.filter(item=>itemMatches(item,catalogQuery));const cat=catById(activeCat)||{name:'Каталог',prep:true}; if(Number(activeCat)===BANQUET_CATEGORY){$('tiles').innerHTML=renderBanquetView(items);window.CateriumBanquet.bind($('tiles'),{catalog:banquetCatalog(),draft,rerender:renderCatalogV5});if(!catalogOnly){renderOrderLines();updateOrderSummary();}return;} - const cards=items.map(item=>``).join(''); + const cards=items.map(item=>``).join(''); const addText=Number(activeCat)===0?'Добавить бокс':(Number(activeCat)===5?'Добавить премиум':'Добавить позицию'); const empty=items.length?'':`
${catalogQuery?'По вашему запросу ничего не найдено.':`В разделе «${esc(cat.name)}» пока нет позиций.`}
`; $('tiles').innerHTML=`${cards}${empty}`; @@ -457,7 +457,7 @@ window.SUN_LEGACY_CATALOG_V175=[]; }); window.CateriumPricing?.watch(()=>boxes); window.render=renderCatalogV5; - window.add=id=>{const item=boxes.find(x=>String(x.id)===String(id));if(!item||item.hidden===true)return;let line=(draft.lines||[]).find(x=>String(x.id)===String(id));if(line)line.qty=Math.max(0,Number(line.qty||0))+1;else{if(!Array.isArray(draft.lines))draft.lines=[];draft.lines.push({id:item.id,qty:1,price:Math.max(0,window.CateriumPricing.price(item))})}renderCatalogV5();}; + window.add=id=>{const item=boxes.find(x=>String(x.id)===String(id));if(!item||!(window.CateriumTrainingCatalog?.visible(item)??item.hidden!==true))return;let line=(draft.lines||[]).find(x=>String(x.id)===String(id));if(line)line.qty=Math.max(0,Number(line.qty||0))+1;else{if(!Array.isArray(draft.lines))draft.lines=[];draft.lines.push({id:item.id,qty:1,price:Math.max(0,window.CateriumPricing.price(item))})}renderCatalogV5();}; window.qty=(id,n)=>{if(!Array.isArray(draft.lines))draft.lines=[];const next=Math.max(0,Number(n||0));if(next<1)draft.lines=draft.lines.filter(x=>String(x.id)!==String(id));else{const line=draft.lines.find(x=>String(x.id)===String(id));if(line)line.qty=next}renderCatalogV5();}; function ensureManagerToolbar(){ @@ -466,7 +466,7 @@ window.SUN_LEGACY_CATALOG_V175=[]; } function renderManagerV5(){ ensureManagerToolbar();const managerSearch=$('sunManagerSearch');if(managerSearch){const boxMode=[0,5].includes(Number(activeCat));managerSearch.placeholder=boxMode?'Поиск по № бокса':'Поиск по позиции';managerSearch.inputMode=boxMode?'numeric':'search';}const title=document.querySelector('#manager .dialog-head h2'),add=document.querySelector('#manager .sun-manager-toolbar .primary')||document.querySelector('#manager .primary');if(title)title.textContent=catName(activeCat);if(add){add.textContent=Number(activeCat)===0?'Добавить бокс':(Number(activeCat)===5?'Добавить премиум':'Добавить позицию');add.onclick=()=>window.editBox(null)} - const items=boxes.filter(i=>i.hidden!==true&&Number(i.category||0)===Number(activeCat)&&itemMatches(i,managerQuery)),cat=catById(activeCat)||{prep:true}; + const items=boxes.filter(i=>(window.CateriumTrainingCatalog?.visible(i)??(i.hidden!==true))&&Number(i.category||0)===Number(activeCat)&&itemMatches(i,managerQuery)),cat=catById(activeCat)||{prep:true}; $('managerList').innerHTML=items.length?items.map(item=>``).join(''):'

Ничего не найдено.

'; } window.openManager=()=>{managerQuery='';ensureManagerToolbar();if($('sunManagerSearch'))$('sunManagerSearch').value='';renderManagerV5();window.modal?.('manager');}; @@ -476,7 +476,7 @@ window.SUN_LEGACY_CATALOG_V175=[]; const cat=catById(edited.category)||{name:'Каталог',prep:true};$('editTitle').textContent=edited.id?`Изменить: ${cat.name}`:(Number(edited.category)===0?'Новый бокс':(Number(edited.category)===5?'Новая премиум позиция':'Новая позиция'));const zone=document.querySelector('#editor .ingredient-zone');if(zone)zone.style.display=cat.prep?'block':'none';window.renderIngredients?.();window.modal?.('editor'); }; if($('itemCategory'))$('itemCategory').onchange=function(){if(!edited)return;edited.category=Number(this.value);const c=catById(edited.category)||{name:'Каталог',prep:true};const zone=document.querySelector('#editor .ingredient-zone');if(zone)zone.style.display=c.prep?'block':'none';syncBoxWeightEditor(edited.category,edited);syncBanquetEditor(edited.category,edited);$('editTitle').textContent=edited.id?`Изменить: ${c.name}`:(Number(edited.category)===0?'Новый бокс':(Number(edited.category)===5?'Новая премиум позиция':'Новая позиция'));}; - window.saveBox=()=>{if(!edited)return;edited.name=$('boxName').value.trim();if(!window.CateriumPricing.saveEditor(edited))return false;edited.category=Number($('itemCategory').value);if([0,5].includes(edited.category)){const grams=Math.max(0,Math.round(Number($('boxWeightGrams')?.value||0)));edited.weight=grams?`${grams} г`:'';edited.pieces=Math.max(0,Math.round(Number($('boxPiecesCount')?.value||0)));}if(edited.category===BANQUET_CATEGORY){edited.catalogSection=($('banquetSection')?.value||'').trim();const grams=Math.max(0,Math.round(Number($('banquetWeightGrams')?.value||0)));edited.weight=grams?`${grams} г`:'';}window.CateriumBanquet?.saveEditor(edited,boxes.filter(i=>i.hidden!==true&&Number(i.category)===6));edited.ingredients=Array.isArray(edited.ingredients)?edited.ingredients:[];if([0,5].includes(edited.category)&&$('boxCompositionText')){const next=$('boxCompositionText').value.split(/\r?\n/).map(x=>x.trim()).filter(Boolean);const before=JSON.stringify(Array.isArray(edited.composition)?edited.composition:[]);edited.composition=next;if(JSON.stringify(next)!==before)edited.compositionSource='manual';}normalizeBoxItem(edited,{cleanup:true});if(!edited.name){alert('Введите название.');return false;}if(!edited.id){edited.id=(typeof sunUUID==='function'?sunUUID():`item-${Date.now()}`);boxes.push(edited)}else{const i=boxes.findIndex(x=>String(x.id)===String(edited.id));if(i>=0)boxes[i]=edited}if(!catById(edited.category)?.hidden)activeCat=edited.category;persist();closeModal('editor');closeModal('manager');renderCatalogV5();renderOrders?.();window.sunClientOfferCatalogChanged?.(edited.id);window.CateriumPricing.refresh();return true;}; + window.saveBox=()=>{if(!edited)return;edited.name=$('boxName').value.trim();if(!window.CateriumPricing.saveEditor(edited))return false;edited.category=Number($('itemCategory').value);if([0,5].includes(edited.category)){const grams=Math.max(0,Math.round(Number($('boxWeightGrams')?.value||0)));edited.weight=grams?`${grams} г`:'';edited.pieces=Math.max(0,Math.round(Number($('boxPiecesCount')?.value||0)));}if(edited.category===BANQUET_CATEGORY){edited.catalogSection=($('banquetSection')?.value||'').trim();const grams=Math.max(0,Math.round(Number($('banquetWeightGrams')?.value||0)));edited.weight=grams?`${grams} г`:'';}window.CateriumBanquet?.saveEditor(edited,boxes.filter(i=>(window.CateriumTrainingCatalog?.visible(i)??(i.hidden!==true))&&Number(i.category)===6));edited.ingredients=Array.isArray(edited.ingredients)?edited.ingredients:[];if([0,5].includes(edited.category)&&$('boxCompositionText')){const next=$('boxCompositionText').value.split(/\r?\n/).map(x=>x.trim()).filter(Boolean);const before=JSON.stringify(Array.isArray(edited.composition)?edited.composition:[]);edited.composition=next;if(JSON.stringify(next)!==before)edited.compositionSource='manual';}normalizeBoxItem(edited,{cleanup:true});if(!edited.name){alert('Введите название.');return false;}if(!edited.id){edited.id=(typeof sunUUID==='function'?sunUUID():`item-${Date.now()}`);boxes.push(edited)}else{const i=boxes.findIndex(x=>String(x.id)===String(edited.id));if(i>=0)boxes[i]=edited}if(!catById(edited.category)?.hidden)activeCat=edited.category;persist();closeModal('editor');closeModal('manager');renderCatalogV5();renderOrders?.();window.sunClientOfferCatalogChanged?.(edited.id);window.CateriumPricing.refresh();return true;}; window.removeBox=()=>{if(!edited?.id||!confirm(`Удалить «${edited.name}» из каталога?`))return;boxes=boxes.filter(x=>String(x.id)!==String(edited.id));draft.lines=(draft.lines||[]).filter(x=>String(x.id)!==String(edited.id));persist();closeModal('editor');closeModal('manager');renderCatalogV5();renderOrders?.();}; function fileAsDataUrl(file){return new Promise((resolve,reject)=>{const reader=new FileReader();reader.onerror=()=>reject(reader.error||new Error('FileReader error'));reader.onload=()=>resolve(String(reader.result||''));reader.readAsDataURL(file)})} @@ -1841,7 +1841,7 @@ window.SUN_LEGACY_CATALOG_V175=[]; for(const st of stats.values()){maxOrders=Math.max(maxOrders,st.orders);maxQty=Math.max(maxQty,st.qty)} const out=new Map();for(const [id,st] of stats)out.set(id,(maxOrders?st.orders/maxOrders:0)*.78+(maxQty?st.qty/maxQty:0)*.22);return out; } - function foodCandidates(){const pop=popularityMap();return getBoxes().filter(x=>Number(x?.category||0)===0&&x?.hidden!==true&&window.CateriumPricing.price(x)>0&&parseWeight(x?.weight)>0&&!/\u0433\u043e\u0442\u043e\u0432\u044b\u0435\s+\u043c\u0435\u043d\u044e/i.test(String(x?.catalogSection||''))).map(x=>({raw:x,id:String(x.id),name:String(x.name||''),section:String(x.catalogSection||'\u0411\u043e\u043a\u0441\u044b'),price:window.CateriumPricing.price(x),grams:parseWeight(x.weight),portions:portions(x),photo:String(x.photo||(window.CateriumBranding.documentImage())),popularity:Number(pop.get(String(x.id))||0)}));} + function foodCandidates(){const pop=popularityMap();return getBoxes().filter(x=>Number(x?.category||0)===0&&(window.CateriumTrainingCatalog?.visible(x)??(x?.hidden!==true))&&window.CateriumPricing.price(x)>0&&parseWeight(x?.weight)>0&&!/\u0433\u043e\u0442\u043e\u0432\u044b\u0435\s+\u043c\u0435\u043d\u044e/i.test(String(x?.catalogSection||''))).map(x=>({raw:x,id:String(x.id),name:String(x.name||''),section:String(x.catalogSection||'\u0411\u043e\u043a\u0441\u044b'),price:window.CateriumPricing.price(x),grams:parseWeight(x.weight),portions:portions(x),photo:String(x.photo||(window.CateriumBranding.documentImage())),popularity:Number(pop.get(String(x.id))||0)}));} function hashNoise(text,seed){let h=2166136261^(seed*2654435761);for(const c of String(text)){h^=c.charCodeAt(0);h=Math.imul(h,16777619)}return ((h>>>0)%10000)/10000;} function stateKey(s){return [...s.q.entries()].sort((a,b)=>a[0].localeCompare(b[0])).map(([id,q])=>id+':'+q).join('|')} function score(s,t,seed){ diff --git a/public/core/ops-ux-v1762.js b/public/core/ops-ux-v1762.js index a2e3bec..0df57da 100644 --- a/public/core/ops-ux-v1762.js +++ b/public/core/ops-ux-v1762.js @@ -119,7 +119,7 @@ let originalModal=null,originalCloseModal=null,originalSaveBox=null,originalRemoveBox=null; function menuCanEdit(){if(support())return false;const c=cloud();if(!c?.getSession?.()?.user)return true;return can('catalog.edit')} function currentCategory(){return CATEGORIES.find(x=>x.id===menuCategory)||CATEGORIES[0]} - function menuItems(){const q=menuQuery.trim().toLowerCase();return boxes().filter(x=>x.hidden!==true&&Number(x.category||0)===menuCategory&&(!q||[x.name,x.weight,x.catalogSection,...(x.composition||[])].join(' ').toLowerCase().includes(q)))} + function menuItems(){const q=menuQuery.trim().toLowerCase();return boxes().filter(x=>(window.CateriumTrainingCatalog?.visible(x)??(x.hidden!==true))&&Number(x.category||0)===menuCategory&&(!q||[x.name,x.weight,x.catalogSection,...(x.composition||[])].join(' ').toLowerCase().includes(q)))} function restoreEditorDialog(){if(editorHome&&editorDialog&&editorDialog.parentNode!==editorHome){editorHome.appendChild(editorDialog);editorDialog.classList.remove('sun-menu-editor-docked');}editorHome?.classList.remove('on');} function emptyMenuDetail(text='Выберите позицию слева, чтобы открыть карточку.'){ const pane=$('sunMenuDetailV1762');if(!pane)return;restoreEditorDialog();pane.innerHTML=`
${esc(text)}${!menuCanEdit()?'
только просмотр
':''}
`; @@ -139,7 +139,7 @@ if(!menuView)return;const list=$('sunMenuListV1762'),count=$('sunMenuCountV1762');if(!list)return; qa('[data-menu-cat-v1762]',menuView).forEach(b=>b.classList.toggle('on',Number(b.dataset.menuCatV1762)===menuCategory)); const items=menuItems();if(count)count.textContent=`${items.length} поз.`; - list.innerHTML=items.length?items.map(item=>``).join(''):'
В этом разделе ничего не найдено.
'; + list.innerHTML=items.length?items.map(item=>``).join(''):'
В этом разделе ничего не найдено.
'; qa('[data-menu-item-v1762]',list).forEach(b=>b.onclick=()=>{const item=boxes().find(x=>String(x.id)===String(b.dataset.menuItemV1762));if(!item)return;if(menuCanEdit()){window.editBox?.(item.id);setTimeout(dockEditor,0)}else showReadOnlyMenuItem(item)}); } function patchMenuLegacyEditor(){ @@ -254,6 +254,7 @@ document.addEventListener('click',e=>{const more=e.target.closest('#calendar .cal-more');if(more){e.preventDefault();e.stopImmediatePropagation();showCalendarDay(more.closest('.cal-day'));return}interceptRouteActions(e)},true); window.addEventListener('sun:cloud-permissions-changed',()=>{patchSupportPermissions();maintainSupport();syncMenuPermission();if(menuActive){renderMenuList();emptyMenuDetail();}}); window.addEventListener('caterium:catalog-prices-changed',()=>{if(menuActive)renderMenuList();}); + window.addEventListener('caterium:training-catalog-changed',()=>{if(menuActive)renderMenuList();}); window.addEventListener('sun:cloud-state-applied',()=>{maintainSupport();syncMenuPermission();if(menuActive)renderMenuList();setTimeout(enhanceRoutePage,80)}); const mo=new MutationObserver(records=>{let route=false,supportBanner=false;for(const r of records){for(const n of r.addedNodes){if(n.nodeType!==1)continue;if(n.id==='routeContent'||n.querySelector?.('#routeContent')||n.closest?.('#sun-routes-view'))route=true;if(n.id==='sunDevSupportBannerV22'||n.querySelector?.('#sunDevSupportBannerV22'))supportBanner=true;}}if(route)setTimeout(enhanceRoutePage,30);if(supportBanner)setTimeout(renderSupportStatus,20);});mo.observe(document.documentElement,{childList:true,subtree:true}); if(!maintenanceTimer)maintenanceTimer=setInterval(()=>{if(document.hidden)return;maintainSupport();syncMenuPermission();if($('sun-routes-view')?.classList.contains('on'))enhanceRoutePage();},8000); diff --git a/public/core/training-catalog.js b/public/core/training-catalog.js new file mode 100644 index 0000000..527df8e --- /dev/null +++ b/public/core/training-catalog.js @@ -0,0 +1,165 @@ +/* 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(); +})(); diff --git a/public/core/trial-demo.js b/public/core/trial-demo.js index d1f55d5..fecc4e2 100644 --- a/public/core/trial-demo.js +++ b/public/core/trial-demo.js @@ -16,44 +16,36 @@ 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();}} - } + async function refreshStatus(){status=null;statusWorkspace='';render();} + function render(){ const host=$('new');if(!host)return; + if(window.CateriumTrainingCatalog&&!window.CateriumTrainingCatalog.enabled()){$('ctTrialDemo')?.remove();return;} const demoItems=catalog().filter(b=>b.demo),items=demoItems.filter(b=>b.ttk),active=installed()&&demoItems.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:demoItems.map(x=>[x.id,x.name]),upgrade:status?.canUpgrade,can:writable()}); if(card.dataset.signature===signature)return;card.dataset.signature=signature; - card.innerHTML=active?`
Демонстрационная база

${demoItems.length} тестовых позиций · готовые составы, учебные цены и поставщики

Можно пробовать
+ const previousGuide={open:card.querySelector('details')?.open,selected:$('ctDemoBox')?.value,message:$('ctDemoMessage')?.textContent}; + card.innerHTML=active?`
Учебный каталог включён

${demoItems.length} тестовых позиций · готовые составы, учебные цены и поставщики

Учебные примеры

Создайте пробный заказ, посмотрите нехватку в закупках, оформите приход на складе и спишите продукты по заказу. Все данные можно редактировать.

${status?.canUpgrade?'':''}
Технологические карты боксов и блюд

`: `
Попробуйте приложение на готовом меню

Боксы и премиум-сеты, 18 банкетных блюд, напитки, посуда, дополнения и доставка. Учебные цены, составы и поставщики для проверки заказа, склада и закупки.

`; + // Preserve the user's disclosure and selection across actual data changes. + if(previousGuide.open&&card.querySelector('details'))card.querySelector('details').open=true; + const selector=$('ctDemoBox');if(selector&&[...selector.options].some(o=>o.value===previousGuide.selected))selector.value=previousGuide.selected; + if(previousGuide.message&&$('ctDemoMessage'))$('ctDemoMessage').textContent=previousGuide.message; 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;} - } + async function install(){try{await window.CateriumTrainingCatalog.setEnabled(true);}catch(error){message(error.message);}} + function createOrder(){ - if(!writable()||!installed())return; + if(!writable()||!installed()||(window.CateriumTrainingCatalog&&!window.CateriumTrainingCatalog.enabled()))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;} diff --git a/public/index.html b/public/index.html index b4c84b8..a421101 100644 --- a/public/index.html +++ b/public/index.html @@ -1,4 +1,4 @@ -