Optional per-profile training catalog with reversible example visibility (#35)
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.
This commit is contained in:
parent
ab32199bd6
commit
716a79c58d
4
.github/workflows/deploy-timeweb.yml
vendored
4
.github/workflows/deploy-timeweb.yml
vendored
@ -54,6 +54,7 @@ jobs:
|
|||||||
core/banquet-menu.js
|
core/banquet-menu.js
|
||||||
core/data-layer-v1773.js
|
core/data-layer-v1773.js
|
||||||
core/trial-demo.js
|
core/trial-demo.js
|
||||||
|
core/training-catalog.js
|
||||||
legacy/bootstrap.js
|
legacy/bootstrap.js
|
||||||
caterium-mark-light.svg
|
caterium-mark-light.svg
|
||||||
service-worker.js
|
service-worker.js
|
||||||
@ -116,6 +117,9 @@ jobs:
|
|||||||
- name: Check the published mobile menu form and return arrow
|
- name: Check the published mobile menu form and return arrow
|
||||||
timeout-minutes: 4
|
timeout-minutes: 4
|
||||||
run: node tests/production-mobile-menu.mjs
|
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
|
- name: Save production UI verification
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
|
|||||||
36
docs/releases/2026-09-20-TRAINING-CATALOG.md
Normal file
36
docs/releases/2026-09-20-TRAINING-CATALOG.md
Normal file
@ -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.
|
||||||
@ -152,7 +152,7 @@ window.SUN_LEGACY_CATALOG_V175=[];
|
|||||||
const buildPdf=pages=>window.SunPdfEngine.fromJpegs(pages);
|
const buildPdf=pages=>window.SunPdfEngine.fromJpegs(pages);
|
||||||
|
|
||||||
async function createCatalogPdfBlob(cat,progress){
|
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('В этом разделе нет позиций.');
|
if(!items.length)throw new Error('В этом разделе нет позиций.');
|
||||||
const pages=[],dateText=new Date().toLocaleDateString('ru-RU'),brand=window.CateriumBranding.identity();
|
const pages=[],dateText=new Date().toLocaleDateString('ru-RU'),brand=window.CateriumBranding.identity();
|
||||||
for(let i=0;i<items.length;i++){
|
for(let i=0;i<items.length;i++){
|
||||||
@ -203,7 +203,7 @@ window.SUN_LEGACY_CATALOG_V175=[];
|
|||||||
|
|
||||||
window.sunOpenCatalogPdfViewer=openCatalogPdfViewer;
|
window.sunOpenCatalogPdfViewer=openCatalogPdfViewer;
|
||||||
window.sunBuildCatalogPdfBlob=()=>createCatalogPdfBlob(currentCategory());
|
window.sunBuildCatalogPdfBlob=()=>createCatalogPdfBlob(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<items.length;i++)pages.push(await renderPageJpeg(items[i],cat,i,items.length,dateText));return pages};
|
window.sunCatalogDebugPdfPages=async(cat=currentCategory(),limit=3)=>{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<items.length;i++)pages.push(await renderPageJpeg(items[i],cat,i,items.length,dateText));return pages};
|
||||||
installButton();setTimeout(installButton,150);setTimeout(installButton,800);
|
installButton();setTimeout(installButton,150);setTimeout(installButton,800);
|
||||||
document.addEventListener('click',e=>{if(e.target.closest('#new .cats button,#new .sun-catalog-top-toolbar .cats button'))setTimeout(installButton,30)},true);
|
document.addEventListener('click',e=>{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});
|
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(){
|
function renderCategoryManager(){
|
||||||
ensureCategoryModal();const root=$('sunCategoryRows');if(!root)return;
|
ensureCategoryModal();const root=$('sunCategoryRows');if(!root)return;
|
||||||
root.innerHTML=catalogCats.slice().sort((a,b)=>a.order-b.order).map(c=>{
|
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 `<div class="sun-category-row ${c.hidden?'is-hidden':''}" data-cat-row="${c.id}"><input value="${esc(c.name)}" data-cat-name="${c.id}" aria-label="Название вкладки"><label class="sun-category-color" title="Цвет вкладки"><input type="color" value="${esc(c.accent||'#708090')}" data-cat-color="${c.id}" aria-label="Цвет вкладки"></label><small>${count} поз.</small><div class="sun-category-row-actions"><button class="outline" type="button" data-cat-up="${c.id}" title="Выше">↑</button><button class="outline" type="button" data-cat-down="${c.id}" title="Ниже">↓</button><button class="outline" type="button" data-cat-toggle="${c.id}">${c.hidden?'Восстановить':'Скрыть'}</button></div></div>`;
|
return `<div class="sun-category-row ${c.hidden?'is-hidden':''}" data-cat-row="${c.id}"><input value="${esc(c.name)}" data-cat-name="${c.id}" aria-label="Название вкладки"><label class="sun-category-color" title="Цвет вкладки"><input type="color" value="${esc(c.accent||'#708090')}" data-cat-color="${c.id}" aria-label="Цвет вкладки"></label><small>${count} поз.</small><div class="sun-category-row-actions"><button class="outline" type="button" data-cat-up="${c.id}" title="Выше">↑</button><button class="outline" type="button" data-cat-down="${c.id}" title="Ниже">↓</button><button class="outline" type="button" data-cat-toggle="${c.id}">${c.hidden?'Восстановить':'Скрыть'}</button></div></div>`;
|
||||||
}).join('');
|
}).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();});
|
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;
|
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';
|
fields.section.style.display=isBanquet?'flex':'none';fields.weight.style.display=isBanquet?'flex':'none';
|
||||||
const caption=$('boxPriceCaption');if(caption)caption.textContent=isBanquet?'Обычная цена на 1 гостя, ₽':'Обычная цена, ₽';
|
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)||''}
|
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='<div class="sun-ingredient-head"><span>Вид / название</span><span>Кол-во</span><span>Ед.</span><span>Вес 1 шт., г</span><span></span></div>';root.innerHTML=head+edited.ingredients.map((row,i)=>{const w=ingredientUnitWeight(row);return `<div class="ingredient sun-ingredient-v175"><input value="${esc(cleanIngredientName(row[0]))}" onchange="edited.ingredients[${i}][0]=this.value"><input type="number" min="0" step="0.01" value="${Number(row[1]||0)}" onchange="edited.ingredients[${i}][1]=+this.value"><input value="${esc(row[2]||'шт.')}" onchange="edited.ingredients[${i}][2]=this.value"><input type="number" min="0" step="0.1" inputmode="decimal" placeholder="г" value="${w||''}" onchange="edited.ingredients[${i}][3]=Math.max(0,+this.value||0)"><button type="button" onclick="edited.ingredients.splice(${i},1);renderIngredients()">×</button></div>`}).join('')};window.addIngredient=()=>{if(!edited)return;if(!Array.isArray(edited.ingredients))edited.ingredients=[];edited.ingredients.push(['Новая позиция',1,'шт.',0]);window.renderIngredients();};}
|
function installIngredientEditorV175(){window.renderIngredients=()=>{if(!edited)return;normalizeBoxItem(edited);const root=$('ingredients');if(!root)return;const head='<div class="sun-ingredient-head"><span>Вид / название</span><span>Кол-во</span><span>Ед.</span><span>Вес 1 шт., г</span><span></span></div>';root.innerHTML=head+edited.ingredients.map((row,i)=>{const w=ingredientUnitWeight(row);return `<div class="ingredient sun-ingredient-v175"><input value="${esc(cleanIngredientName(row[0]))}" onchange="edited.ingredients[${i}][0]=this.value"><input type="number" min="0" step="0.01" value="${Number(row[1]||0)}" onchange="edited.ingredients[${i}][1]=+this.value"><input value="${esc(row[2]||'шт.')}" onchange="edited.ingredients[${i}][2]=this.value"><input type="number" min="0" step="0.1" inputmode="decimal" placeholder="г" value="${w||''}" onchange="edited.ingredients[${i}][3]=Math.max(0,+this.value||0)"><button type="button" onclick="edited.ingredients.splice(${i},1);renderIngredients()">×</button></div>`}).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 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)}
|
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;
|
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();};
|
window.toggleBanquetItem=id=>{window.CateriumBanquet.toggle(draft,banquetCatalog(),String(id));renderCatalogV5();};
|
||||||
function renderBanquetView(items){return window.CateriumBanquet.render({items,catalog:banquetCatalog(),draft})}
|
function renderBanquetView(items){return window.CateriumBanquet.render({items,catalog:banquetCatalog(),draft})}
|
||||||
function renderCatalogV5({catalogOnly=false}={}){
|
function renderCatalogV5({catalogOnly=false}={}){
|
||||||
$('new')?.classList.toggle('sun-banquet-active',Number(activeCat)===BANQUET_CATEGORY);
|
$('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);
|
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;}
|
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=>`<button class="tile" type="button" onclick="add('${esc(item.id)}')" title="Добавить в заказ">${item.photo?`<img src="${esc(window.SunSafe.imageAssetSrc(item.photo))}" alt="" loading="lazy" decoding="async" onerror="this.onerror=null;this.src='${window.CateriumBranding.logoHTML()}'">`:("<div class=\"ph\"><img src=\""+window.CateriumBranding.logoHTML()+"\" class=\"sun-ph-logo\" alt=\"Логотип "+window.CateriumBranding.nameHTML()+"\"></div>")}<span>${esc(item.name)}</span>${item.catalogSection?`<small class="tile-section">${esc(item.catalogSection)}</small>`:''}${(item.weight||inferBoxPieces(item))?`<small class="tile-meta">${item.weight?`Вес: ${esc(item.weight)}`:''}${item.weight&&inferBoxPieces(item)?' · ':''}${inferBoxPieces(item)?`${inferBoxPieces(item)} шт.`:''}</small>`:''}<span class="tile-price-wrap"><small class="tile-price">${money(window.CateriumPricing.price(item))}</small></span>${window.CateriumPricing.badge(item)}</button>`).join('');
|
const cards=items.map(item=>`<button class="tile" type="button" onclick="add('${esc(item.id)}')" title="Добавить в заказ">${item.photo?`<img src="${esc(window.SunSafe.imageAssetSrc(item.photo))}" alt="" loading="lazy" decoding="async" onerror="this.onerror=null;this.src='${window.CateriumBranding.logoHTML()}'">`:("<div class=\"ph\"><img src=\""+window.CateriumBranding.logoHTML()+"\" class=\"sun-ph-logo\" alt=\"Логотип "+window.CateriumBranding.nameHTML()+"\"></div>")}<span>${esc(item.name)}${window.CateriumTrainingCatalog?.badge(item)||''}</span>${item.catalogSection?`<small class="tile-section">${esc(item.catalogSection)}</small>`:''}${(item.weight||inferBoxPieces(item))?`<small class="tile-meta">${item.weight?`Вес: ${esc(item.weight)}`:''}${item.weight&&inferBoxPieces(item)?' · ':''}${inferBoxPieces(item)?`${inferBoxPieces(item)} шт.`:''}</small>`:''}<span class="tile-price-wrap"><small class="tile-price">${money(window.CateriumPricing.price(item))}</small></span>${window.CateriumPricing.badge(item)}</button>`).join('');
|
||||||
const addText=Number(activeCat)===0?'Добавить бокс':(Number(activeCat)===5?'Добавить премиум':'Добавить позицию');
|
const addText=Number(activeCat)===0?'Добавить бокс':(Number(activeCat)===5?'Добавить премиум':'Добавить позицию');
|
||||||
const empty=items.length?'':`<div class="catalog-empty">${catalogQuery?'По вашему запросу ничего не найдено.':`В разделе «${esc(cat.name)}» пока нет позиций.`}</div>`;
|
const empty=items.length?'':`<div class="catalog-empty">${catalogQuery?'По вашему запросу ничего не найдено.':`В разделе «${esc(cat.name)}» пока нет позиций.`}</div>`;
|
||||||
$('tiles').innerHTML=`<button class="tile add" type="button" onclick="editBox(null)">+<br>${addText}</button>${cards}${empty}`;
|
$('tiles').innerHTML=`<button class="tile add" type="button" onclick="editBox(null)">+<br>${addText}</button>${cards}${empty}`;
|
||||||
@ -457,7 +457,7 @@ window.SUN_LEGACY_CATALOG_V175=[];
|
|||||||
});
|
});
|
||||||
window.CateriumPricing?.watch(()=>boxes);
|
window.CateriumPricing?.watch(()=>boxes);
|
||||||
window.render=renderCatalogV5;
|
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();};
|
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(){
|
function ensureManagerToolbar(){
|
||||||
@ -466,7 +466,7 @@ window.SUN_LEGACY_CATALOG_V175=[];
|
|||||||
}
|
}
|
||||||
function renderManagerV5(){
|
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)}
|
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=>`<button type="button" onclick="editBox('${esc(item.id)}')">${item.photo?`<img src="${esc(window.SunSafe.imageAssetSrc(item.photo))}" alt="" loading="lazy" decoding="async" onerror="this.onerror=null;this.src='${window.CateriumBranding.logoHTML()}'">`:("<div class=\"ph\"><img src=\""+window.CateriumBranding.logoHTML()+"\" class=\"sun-ph-logo\" alt=\"Логотип "+window.CateriumBranding.nameHTML()+"\"></div>")}<b>${esc(item.name)}</b><span class="manager-price-wrap"><small class="manager-card-price">${money(window.CateriumPricing.price(item))}</small></span>${window.CateriumPricing.badge(item)}${(item.weight||inferBoxPieces(item))?`<small class="manager-card-meta">${item.weight?`Вес: ${esc(item.weight)}`:''}${item.weight&&inferBoxPieces(item)?' · ':''}${inferBoxPieces(item)?`${inferBoxPieces(item)} шт.`:''}</small>`:''}${cat.prep?`<small>${(item.ingredients||[]).length} позиций в составе</small>`:''}</button>`).join(''):'<p class="empty">Ничего не найдено.</p>';
|
$('managerList').innerHTML=items.length?items.map(item=>`<button type="button" onclick="editBox('${esc(item.id)}')">${item.photo?`<img src="${esc(window.SunSafe.imageAssetSrc(item.photo))}" alt="" loading="lazy" decoding="async" onerror="this.onerror=null;this.src='${window.CateriumBranding.logoHTML()}'">`:("<div class=\"ph\"><img src=\""+window.CateriumBranding.logoHTML()+"\" class=\"sun-ph-logo\" alt=\"Логотип "+window.CateriumBranding.nameHTML()+"\"></div>")}<b>${esc(item.name)}</b><span class="manager-price-wrap"><small class="manager-card-price">${money(window.CateriumPricing.price(item))}</small></span>${window.CateriumPricing.badge(item)}${(item.weight||inferBoxPieces(item))?`<small class="manager-card-meta">${item.weight?`Вес: ${esc(item.weight)}`:''}${item.weight&&inferBoxPieces(item)?' · ':''}${inferBoxPieces(item)?`${inferBoxPieces(item)} шт.`:''}</small>`:''}${cat.prep?`<small>${(item.ingredients||[]).length} позиций в составе</small>`:''}</button>`).join(''):'<p class="empty">Ничего не найдено.</p>';
|
||||||
}
|
}
|
||||||
window.openManager=()=>{managerQuery='';ensureManagerToolbar();if($('sunManagerSearch'))$('sunManagerSearch').value='';renderManagerV5();window.modal?.('manager');};
|
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');
|
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?'Новая премиум позиция':'Новая позиция'));};
|
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?.();};
|
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)})}
|
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)}
|
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;
|
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 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 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){
|
function score(s,t,seed){
|
||||||
|
|||||||
@ -119,7 +119,7 @@
|
|||||||
let originalModal=null,originalCloseModal=null,originalSaveBox=null,originalRemoveBox=null;
|
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 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 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 restoreEditorDialog(){if(editorHome&&editorDialog&&editorDialog.parentNode!==editorHome){editorHome.appendChild(editorDialog);editorDialog.classList.remove('sun-menu-editor-docked');}editorHome?.classList.remove('on');}
|
||||||
function emptyMenuDetail(text='Выберите позицию слева, чтобы открыть карточку.'){
|
function emptyMenuDetail(text='Выберите позицию слева, чтобы открыть карточку.'){
|
||||||
const pane=$('sunMenuDetailV1762');if(!pane)return;restoreEditorDialog();pane.innerHTML=`<div class="sun-menu-detail-placeholder"><div><b>${esc(text)}</b>${!menuCanEdit()?'<div class="sun-menu-readonly-badge" style="margin-top:9px">только просмотр</div>':''}</div></div>`;
|
const pane=$('sunMenuDetailV1762');if(!pane)return;restoreEditorDialog();pane.innerHTML=`<div class="sun-menu-detail-placeholder"><div><b>${esc(text)}</b>${!menuCanEdit()?'<div class="sun-menu-readonly-badge" style="margin-top:9px">только просмотр</div>':''}</div></div>`;
|
||||||
@ -139,7 +139,7 @@
|
|||||||
if(!menuView)return;const list=$('sunMenuListV1762'),count=$('sunMenuCountV1762');if(!list)return;
|
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));
|
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} поз.`;
|
const items=menuItems();if(count)count.textContent=`${items.length} поз.`;
|
||||||
list.innerHTML=items.length?items.map(item=>`<button type="button" class="sun-menu-row" data-menu-item-v1762="${esc(item.id)}">${item.photo?`<img src="${esc(item.photo)}" alt="" loading="lazy" decoding="async">`:'<div class="ph"></div>'}<span><b>${esc(item.name||'Без названия')}</b><small>${[item.weight,Number(item.pieces||0)?`${Number(item.pieces)} шт.`:'',item.catalogSection].filter(Boolean).map(esc).join(' · ')}</small></span><span class="sun-menu-row-price">${esc(money((window.CateriumPricing?.price(item)??Number(item?.price||0))))}${window.CateriumPricing?.badge(item)||''}</span></button>`).join(''):'<div class="sun-menu-empty">В этом разделе ничего не найдено.</div>';
|
list.innerHTML=items.length?items.map(item=>`<button type="button" class="sun-menu-row" data-menu-item-v1762="${esc(item.id)}">${item.photo?`<img src="${esc(item.photo)}" alt="" loading="lazy" decoding="async">`:'<div class="ph"></div>'}<span><b>${esc(item.name||'Без названия')}</b>${window.CateriumTrainingCatalog?.badge(item)||''}<small>${[item.weight,Number(item.pieces||0)?`${Number(item.pieces)} шт.`:'',item.catalogSection].filter(Boolean).map(esc).join(' · ')}</small></span><span class="sun-menu-row-price">${esc(money((window.CateriumPricing?.price(item)??Number(item?.price||0))))}${window.CateriumPricing?.badge(item)||''}</span></button>`).join(''):'<div class="sun-menu-empty">В этом разделе ничего не найдено.</div>';
|
||||||
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)});
|
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(){
|
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);
|
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('sun:cloud-permissions-changed',()=>{patchSupportPermissions();maintainSupport();syncMenuPermission();if(menuActive){renderMenuList();emptyMenuDetail();}});
|
||||||
window.addEventListener('caterium:catalog-prices-changed',()=>{if(menuActive)renderMenuList();});
|
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)});
|
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});
|
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);
|
if(!maintenanceTimer)maintenanceTimer=setInterval(()=>{if(document.hidden)return;maintainSupport();syncMenuPermission();if($('sun-routes-view')?.classList.contains('on'))enhanceRoutePage();},8000);
|
||||||
|
|||||||
165
public/core/training-catalog.js
Normal file
165
public/core/training-catalog.js
Normal file
@ -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()?'<small class="ct-training-badge">Учебное</small>':'';}
|
||||||
|
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='<h2>Обучение и знакомство</h2><label class="ct-training-toggle"><input id="ctTrainingEnabled" type="checkbox" aria-describedby="ctTrainingDescription ctTrainingSafety"><span><b>Учебный каталог</b><small>Готовые боксы и блюда для знакомства с Caterium</small></span></label><p id="ctTrainingDescription">Фотографии, составы, технологические карты, учебные цены и поставщики. Можно собрать пробный заказ, посмотреть предложение клиенту, заготовки и закупки — без ручного заполнения каталога.</p><p id="ctTrainingSafety">Снимите галочку, чтобы скрыть учебные позиции. Свои блюда, изменения учебных карточек и созданные заказы не удаляются. Переключатель действует для вашего профиля в этой компании. Складские операции выполняются только по вашей команде.</p><p id="ctTrainingAccess" class="hint"></p><p id="ctTrainingStatus" role="status" aria-live="polite"></p>';
|
||||||
|
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();
|
||||||
|
})();
|
||||||
@ -16,44 +16,36 @@
|
|||||||
let status=null,statusWorkspace='',pending=null,epoch=0,busy=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 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;}
|
function message(text){const el=$('ctDemoMessage');if(el)el.textContent=text;}
|
||||||
async function refreshStatus(){
|
async function refreshStatus(){status=null;statusWorkspace='';render();}
|
||||||
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(){
|
function render(){
|
||||||
const host=$('new');if(!host)return;
|
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;
|
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;}
|
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);}
|
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()});
|
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;
|
if(card.dataset.signature===signature)return;card.dataset.signature=signature;
|
||||||
card.innerHTML=active?`<div class="ct-demo-heading"><div><strong>Демонстрационная база</strong><p>${demoItems.length} тестовых позиций · готовые составы, учебные цены и поставщики</p></div><span class="ct-demo-badge">Можно пробовать</span></div>
|
const previousGuide={open:card.querySelector('details')?.open,selected:$('ctDemoBox')?.value,message:$('ctDemoMessage')?.textContent};
|
||||||
|
card.innerHTML=active?`<div class="ct-demo-heading"><div><strong>Учебный каталог включён</strong><p>${demoItems.length} тестовых позиций · готовые составы, учебные цены и поставщики</p></div><span class="ct-demo-badge">Учебные примеры</span></div>
|
||||||
<p>Создайте пробный заказ, посмотрите нехватку в закупках, оформите приход на складе и спишите продукты по заказу. Все данные можно редактировать.</p>
|
<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>
|
<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>
|
||||||
${status?.canUpgrade?'<button class="outline" data-demo-install>Дополнить пустые вкладки демо</button>':''}
|
${status?.canUpgrade?'<button class="outline" data-demo-install>Дополнить пустые вкладки демо</button>':''}
|
||||||
<details><summary>Технологические карты боксов и блюд</summary><div class="ct-demo-actions"><label>Бокс или блюдо<select id="ctDemoBox">${[...new Set(items.map(b=>Number(b.category)===6?'Банкетное меню':Number(b.category)===5?'Премиум':'Боксы'))].map(label=>`<optgroup label="${label}">${items.filter(b=>(Number(b.category)===6?'Банкетное меню':Number(b.category)===5?'Премиум':'Боксы')===label).map(b=>`<option value="${esc(b.id)}">${esc(b.name)}</option>`).join('')}</optgroup>`).join('')}</select></label><button class="outline" data-demo-ttk>Открыть ТТК</button></div></details><p id="ctDemoMessage" role="status" aria-live="polite"></p>`:
|
<details><summary>Технологические карты боксов и блюд</summary><div class="ct-demo-actions"><label>Бокс или блюдо<select id="ctDemoBox">${[...new Set(items.map(b=>Number(b.category)===6?'Банкетное меню':Number(b.category)===5?'Премиум':'Боксы'))].map(label=>`<optgroup label="${label}">${items.filter(b=>(Number(b.category)===6?'Банкетное меню':Number(b.category)===5?'Премиум':'Боксы')===label).map(b=>`<option value="${esc(b.id)}">${esc(b.name)}</option>`).join('')}</optgroup>`).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>Боксы и премиум-сеты, 18 банкетных блюд, напитки, посуда, дополнения и доставка. Учебные цены, составы и поставщики для проверки заказа, склада и закупки.</p></div></div><button class="primary" data-demo-install>Загрузить демо</button><p id="ctDemoMessage" role="status" aria-live="polite"></p>`;
|
`<div class="ct-demo-heading"><div><strong>Попробуйте приложение на готовом меню</strong><p>Боксы и премиум-сеты, 18 банкетных блюд, напитки, посуда, дополнения и доставка. Учебные цены, составы и поставщики для проверки заказа, склада и закупки.</p></div></div><button class="primary" data-demo-install>Загрузить демо</button><p id="ctDemoMessage" role="status" aria-live="polite"></p>`;
|
||||||
|
// 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-install]')?.addEventListener('click',install);
|
||||||
card.querySelector('[data-demo-order]')?.addEventListener('click',createOrder);
|
card.querySelector('[data-demo-order]')?.addEventListener('click',createOrder);
|
||||||
card.querySelector('[data-demo-ttk]')?.addEventListener('click',()=>showTTK($('ctDemoBox').value));
|
card.querySelector('[data-demo-ttk]')?.addEventListener('click',()=>showTTK($('ctDemoBox').value));
|
||||||
card.querySelectorAll('[data-demo-go]').forEach(b=>b.addEventListener('click',()=>navigate(b.dataset.demoGo)));
|
card.querySelectorAll('[data-demo-go]').forEach(b=>b.addEventListener('click',()=>navigate(b.dataset.demoGo)));
|
||||||
}
|
}
|
||||||
async function install(){
|
async function install(){try{await window.CateriumTrainingCatalog.setEnabled(true);}catch(error){message(error.message);}}
|
||||||
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(){
|
function createOrder(){
|
||||||
if(!writable()||!installed())return;
|
if(!writable()||!installed()||(window.CateriumTrainingCatalog&&!window.CateriumTrainingCatalog.enabled()))return;
|
||||||
const repo=data()?.orders;if(!repo){message('Приложение ещё загружается. Повторите через несколько секунд.');return;}
|
const repo=data()?.orders;if(!repo){message('Приложение ещё загружается. Повторите через несколько секунд.');return;}
|
||||||
const all=repo.list(),existing=all.find(o=>o.demoBatch===BATCH);
|
const all=repo.list(),existing=all.find(o=>o.demoBatch===BATCH);
|
||||||
if(existing){message(`Пробный заказ №${existing.id} уже создан. Его можно открыть во вкладке «Заказы».`);return;}
|
if(existing){message(`Пробный заказ №${existing.id} уже создан. Его можно открыть во вкладке «Заказы».`);return;}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
<!doctype html><html lang="ru"><head><link rel="stylesheet" href="core/login-signature-v1776.css?v=20260918-ui-stability"><script src="core/cloud-transport.js?v=20260918-ui-stability"></script><script src="core/banquet-menu.js?v=20260918-ui-stability-20260919-client-menu"></script><script src="core/access-policy.js?v=20260918-ui-stability"></script><style>body.sun-cloud-auth-required>header,body.sun-cloud-auth-required>.view{visibility:hidden!important;pointer-events:none!important}</style><meta charset="utf-8"><script defer src="core/import-archive.js?v=20260918-ui-stability"></script><script src="core/company-branding.js?v=20260918-ui-stability"></script><script src="core/brand-theme.js?v=20260918-ui-stability"></script><script id="sun-startup-stability-guard">
|
<!doctype html><html lang="ru"><head><script src="core/training-catalog.js?v=20260920-training"></script><link rel="stylesheet" href="core/login-signature-v1776.css?v=20260918-ui-stability"><script src="core/cloud-transport.js?v=20260918-ui-stability"></script><script src="core/banquet-menu.js?v=20260918-ui-stability-20260919-client-menu"></script><script src="core/access-policy.js?v=20260918-ui-stability"></script><style>body.sun-cloud-auth-required>header,body.sun-cloud-auth-required>.view{visibility:hidden!important;pointer-events:none!important}</style><meta charset="utf-8"><script defer src="core/import-archive.js?v=20260918-ui-stability"></script><script src="core/company-branding.js?v=20260918-ui-stability"></script><script src="core/brand-theme.js?v=20260918-ui-stability"></script><script id="sun-startup-stability-guard">
|
||||||
(()=>{
|
(()=>{
|
||||||
'use strict';
|
'use strict';
|
||||||
const stamp=()=>new Date().toISOString().replace(/[:.]/g,'-');
|
const stamp=()=>new Date().toISOString().replace(/[:.]/g,'-');
|
||||||
@ -261,7 +261,7 @@ if (typeof crypto !== 'undefined' && typeof crypto.randomUUID !== 'function') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function activeItems() {
|
function activeItems() {
|
||||||
return boxes.filter(item => item.hidden !== true && Number(item.category || 0) === activeCategory);
|
return boxes.filter(item => (window.CateriumTrainingCatalog?.visible(item)??(item.hidden !== true)) && Number(item.category || 0) === activeCategory);
|
||||||
}
|
}
|
||||||
|
|
||||||
function syncCategoryUI() {
|
function syncCategoryUI() {
|
||||||
@ -1096,7 +1096,7 @@ if (typeof crypto !== 'undefined' && typeof crypto.randomUUID !== 'function') {
|
|||||||
|
|
||||||
function supplierOptions(selected = '') {
|
function supplierOptions(selected = '') {
|
||||||
const current = String(selected || '');
|
const current = String(selected || '');
|
||||||
const list = getSuppliers().filter(s => s.active !== false || s.id === current).sort((a, b) => String(a.name || '').localeCompare(String(b.name || ''), 'ru'));
|
const list = getSuppliers().filter(s => s.id===current||(trainingVisible(s,'suppliers')&&s.active!==false)).sort((a, b) => String(a.name || '').localeCompare(String(b.name || ''), 'ru'));
|
||||||
return `<option value="">Без поставщика</option>${list.map(s => `<option value="${stockEsc(s.id)}" ${s.id === current ? 'selected' : ''}>${stockEsc(s.name || 'Без названия')}${s.active === false ? ' · неактивен' : ''}</option>`).join('')}`;
|
return `<option value="">Без поставщика</option>${list.map(s => `<option value="${stockEsc(s.id)}" ${s.id === current ? 'selected' : ''}>${stockEsc(s.name || 'Без названия')}${s.active === false ? ' · неактивен' : ''}</option>`).join('')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1110,17 +1110,18 @@ if (typeof crypto !== 'undefined' && typeof crypto.randomUUID !== 'function') {
|
|||||||
return `${String(name || '').trim().toLowerCase()}|${String(unit || '').trim().toLowerCase()}`;
|
return `${String(name || '').trim().toLowerCase()}|${String(unit || '').trim().toLowerCase()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function trainingVisible(row,kind='stock') { return window.CateriumTrainingCatalog?.visible(row,kind)??row?.hidden!==true; }
|
||||||
function syncProductsFromCatalog() {
|
function syncProductsFromCatalog() {
|
||||||
const existing = new Map(stock.map(p => [skuKey(p.name, p.unit), p]));
|
const existing = new Map(stock.map(p => [skuKey(p.name, p.unit), p]));
|
||||||
let changed = false;
|
let changed = false;
|
||||||
boxes.forEach(item => {
|
boxes.filter(item=>trainingVisible(item,'catalog')).forEach(item => {
|
||||||
(item.ingredients || []).forEach(part => {
|
(item.ingredients || []).forEach(part => {
|
||||||
const name = String(part[0] || '').trim();
|
const name = String(part[0] || '').trim();
|
||||||
const unit = String(part[2] || 'шт.').trim() || 'шт.';
|
const unit = String(part[2] || 'шт.').trim() || 'шт.';
|
||||||
if (!name) return;
|
if (!name) return;
|
||||||
const key = skuKey(name, unit);
|
const key = skuKey(name, unit);
|
||||||
if (!existing.has(key)) {
|
if (!existing.has(key)) {
|
||||||
const product = { id: sunUUID(), name, unit, qty: 0, cost: 0, min: 0, supplierId: '', lastPurchaseDate: '', lastPurchasePrice: 0 };
|
const product = { id: sunUUID(), name, unit, qty: 0, cost: 0, min: 0, supplierId: '', lastPurchaseDate: '', lastPurchasePrice: 0, trainingCatalog:window.CateriumTrainingCatalog?.isSample(item)===true };
|
||||||
stock.push(product);
|
stock.push(product);
|
||||||
existing.set(key, product);
|
existing.set(key, product);
|
||||||
changed = true;
|
changed = true;
|
||||||
@ -1132,7 +1133,7 @@ if (typeof crypto !== 'undefined' && typeof crypto.randomUUID !== 'function') {
|
|||||||
|
|
||||||
// Normalize legacy values.
|
// Normalize legacy values.
|
||||||
stock = stock.map(p => ({
|
stock = stock.map(p => ({
|
||||||
id: p.id || sunUUID(), name: String(p.name || 'Без названия'), unit: String(p.unit || 'шт.'),
|
...p, id: p.id || sunUUID(), name: String(p.name || 'Без названия'), unit: String(p.unit || 'шт.'),
|
||||||
qty: Number(p.qty || 0), cost: Number(p.cost || 0), min: Number(p.min || 0),
|
qty: Number(p.qty || 0), cost: Number(p.cost || 0), min: Number(p.min || 0),
|
||||||
supplierId: String(p.supplierId || ''), lastPurchaseDate: String(p.lastPurchaseDate || ''), lastPurchasePrice: Number(p.lastPurchasePrice || 0)
|
supplierId: String(p.supplierId || ''), lastPurchaseDate: String(p.lastPurchaseDate || ''), lastPurchasePrice: Number(p.lastPurchasePrice || 0)
|
||||||
}));
|
}));
|
||||||
@ -1210,15 +1211,16 @@ if (typeof crypto !== 'undefined' && typeof crypto.randomUUID !== 'function') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderStockSummary() {
|
function renderStockSummary() {
|
||||||
const low = stock.filter(p => Number(p.qty) <= Number(p.min) && Number(p.min) > 0).length;
|
const shownStock=stock.filter(p=>trainingVisible(p));
|
||||||
const zero = stock.filter(p => Number(p.qty) <= 0).length;
|
const low = shownStock.filter(p => Number(p.qty) <= Number(p.min) && Number(p.min) > 0).length;
|
||||||
const value = stock.reduce((sum, p) => sum + Number(p.qty || 0) * Number(p.cost || 0), 0);
|
const zero = shownStock.filter(p => Number(p.qty) <= 0).length;
|
||||||
|
const value = shownStock.reduce((sum, p) => sum + Number(p.qty || 0) * Number(p.cost || 0), 0);
|
||||||
const need = aggregateNeed().reduce((sum, n) => {
|
const need = aggregateNeed().reduce((sum, n) => {
|
||||||
const p = productByKey(n.name, n.unit);
|
const p = productByKey(n.name, n.unit);
|
||||||
return sum + Math.max(0, n.qty - Number(p?.qty || 0)) * Number(p?.cost || 0);
|
return sum + Math.max(0, n.qty - Number(p?.qty || 0)) * Number(p?.cost || 0);
|
||||||
}, 0);
|
}, 0);
|
||||||
document.getElementById('stockSummary').innerHTML = `
|
document.getElementById('stockSummary').innerHTML = `
|
||||||
<div class="summary-box"><small>Позиций на складе</small><b>${stock.length}</b></div>
|
<div class="summary-box"><small>Позиций на складе</small><b>${shownStock.length}</b></div>
|
||||||
<div class="summary-box"><small>Нулевой / минусовой остаток</small><b>${zero}</b></div>
|
<div class="summary-box"><small>Нулевой / минусовой остаток</small><b>${zero}</b></div>
|
||||||
<div class="summary-box"><small>Стоимость остатков</small><b>${fmt(value)}</b></div>
|
<div class="summary-box"><small>Стоимость остатков</small><b>${fmt(value)}</b></div>
|
||||||
<div class="summary-box"><small>Ориентир закупки под заказы</small><b>${fmt(need)}</b></div>`;
|
<div class="summary-box"><small>Ориентир закупки под заказы</small><b>${fmt(need)}</b></div>`;
|
||||||
@ -1227,7 +1229,7 @@ if (typeof crypto !== 'undefined' && typeof crypto.randomUUID !== 'function') {
|
|||||||
function renderBalances(filter = '') {
|
function renderBalances(filter = '') {
|
||||||
const q = String(filter || '').trim().toLowerCase();
|
const q = String(filter || '').trim().toLowerCase();
|
||||||
const supplierMap = new Map(getSuppliers().map(s => [String(s.id), s]));
|
const supplierMap = new Map(getSuppliers().map(s => [String(s.id), s]));
|
||||||
const list = stock.filter(p => {
|
const list = stock.filter(p=>trainingVisible(p)).filter(p => {
|
||||||
const supplier = supplierMap.get(String(p.supplierId || ''));
|
const supplier = supplierMap.get(String(p.supplierId || ''));
|
||||||
const haystack = `${p.name} ${p.unit} ${supplier?.name || ''} ${supplier?.phone || ''}`.toLowerCase();
|
const haystack = `${p.name} ${p.unit} ${supplier?.name || ''} ${supplier?.phone || ''}`.toLowerCase();
|
||||||
return !q || haystack.includes(q);
|
return !q || haystack.includes(q);
|
||||||
@ -1436,6 +1438,10 @@ if (typeof crypto !== 'undefined' && typeof crypto.randomUUID !== 'function') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
window.sunRefreshStock = renderStock;
|
window.sunRefreshStock = renderStock;
|
||||||
|
window.addEventListener('caterium:training-catalog-changed',()=>{
|
||||||
|
try{const next=JSON.parse(localStorage.getItem(STOCK_KEY)||'[]');if(Array.isArray(next))stock=next;}catch(_){return;}
|
||||||
|
if(stockView.classList.contains('on'))renderStock();
|
||||||
|
});
|
||||||
// Cloud hydration replaces localStorage after this module has booted.
|
// Cloud hydration replaces localStorage after this module has booted.
|
||||||
// Refresh the closure before edits/receipts can write an older inventory back.
|
// Refresh the closure before edits/receipts can write an older inventory back.
|
||||||
window.addEventListener('sun:cloud-state-applied', () => {
|
window.addEventListener('sun:cloud-state-applied', () => {
|
||||||
@ -2728,7 +2734,7 @@ if (typeof crypto !== 'undefined' && typeof crypto.randomUUID !== 'function') {
|
|||||||
const fmt=v=>typeof money==='function'?money(v):`${Number(v||0).toLocaleString('ru-RU',{maximumFractionDigits:2})} ₽`;
|
const fmt=v=>typeof money==='function'?money(v):`${Number(v||0).toLocaleString('ru-RU',{maximumFractionDigits:2})} ₽`;
|
||||||
const dateLabel=v=>v?new Date(`${v}T12:00:00`).toLocaleDateString('ru-RU'):'—';
|
const dateLabel=v=>v?new Date(`${v}T12:00:00`).toLocaleDateString('ru-RU'):'—';
|
||||||
const readArray=(key)=>{try{const x=JSON.parse(localStorage.getItem(key)||'[]');return Array.isArray(x)?x:[];}catch{return[];}};
|
const readArray=(key)=>{try{const x=JSON.parse(localStorage.getItem(key)||'[]');return Array.isArray(x)?x:[];}catch{return[];}};
|
||||||
let suppliers=readArray(KEY).map(s=>({id:String(s.id||sunUUID()),name:String(s.name||'Без названия'),category:String(s.category||''),contact:String(s.contact||''),phone:String(s.phone||''),email:String(s.email||''),address:String(s.address||''),deliveryDays:String(s.deliveryDays||''),minOrder:Number(s.minOrder||0),notes:String(s.notes||''),active:s.active!==false,createdAt:s.createdAt||new Date().toISOString()}));
|
let suppliers=readArray(KEY).map(s=>({...s,id:String(s.id||sunUUID()),name:String(s.name||'Без названия'),category:String(s.category||''),contact:String(s.contact||''),phone:String(s.phone||''),email:String(s.email||''),address:String(s.address||''),deliveryDays:String(s.deliveryDays||''),minOrder:Number(s.minOrder||0),notes:String(s.notes||''),active:s.active!==false,createdAt:s.createdAt||new Date().toISOString()}));
|
||||||
let editingId=null;
|
let editingId=null;
|
||||||
|
|
||||||
function saveSuppliers(){
|
function saveSuppliers(){
|
||||||
@ -2770,11 +2776,12 @@ if (typeof crypto !== 'undefined' && typeof crypto.randomUUID !== 'function') {
|
|||||||
|
|
||||||
const el=id=>document.getElementById(id);
|
const el=id=>document.getElementById(id);
|
||||||
function render(){
|
function render(){
|
||||||
const stock=getStock(), moves=getMoves(), linked=stock.filter(p=>p.supplierId).length, unassigned=stock.filter(p=>!p.supplierId).length, active=suppliers.filter(s=>s.active!==false).length;
|
const shownSuppliers=suppliers.filter(s=>window.CateriumTrainingCatalog?.visible(s,'suppliers')??true);
|
||||||
|
const stock=getStock().filter(p=>window.CateriumTrainingCatalog?.visible(p,'stock')??true), moves=getMoves(), linked=stock.filter(p=>p.supplierId).length, unassigned=stock.filter(p=>!p.supplierId).length, active=shownSuppliers.filter(s=>s.active!==false).length;
|
||||||
const purchaseTotal=moves.filter(m=>m.type==='in'&&m.supplierId).reduce((s,m)=>s+Math.abs(Number(m.qty||0))*Number(m.cost||0),0);
|
const purchaseTotal=moves.filter(m=>m.type==='in'&&m.supplierId).reduce((s,m)=>s+Math.abs(Number(m.qty||0))*Number(m.cost||0),0);
|
||||||
el('supSummary').innerHTML=`<div><small>Всего поставщиков</small><b>${suppliers.length}</b></div><div><small>Активных</small><b>${active}</b></div><div><small>Продуктов привязано</small><b>${linked}</b></div><div><small>Без поставщика</small><b>${unassigned}</b></div><div><small>Закупок по истории</small><b>${fmt(purchaseTotal)}</b></div>`;
|
el('supSummary').innerHTML=`<div><small>Всего поставщиков</small><b>${shownSuppliers.length}</b></div><div><small>Активных</small><b>${active}</b></div><div><small>Продуктов привязано</small><b>${linked}</b></div><div><small>Без поставщика</small><b>${unassigned}</b></div><div><small>Закупок по истории</small><b>${fmt(purchaseTotal)}</b></div>`;
|
||||||
const q=(el('supSearch').value||'').trim().toLowerCase(), filter=el('supFilter').value;
|
const q=(el('supSearch').value||'').trim().toLowerCase(), filter=el('supFilter').value;
|
||||||
const list=suppliers.filter(s=>(filter==='all'||(filter==='active'&&s.active!==false)||(filter==='inactive'&&s.active===false))&&(!q||`${s.name} ${s.category} ${s.contact} ${s.phone} ${s.email} ${s.address}`.toLowerCase().includes(q))).sort((a,b)=>(a.active===b.active?String(a.name).localeCompare(String(b.name),'ru'):(a.active===false?1:-1)));
|
const list=shownSuppliers.filter(s=>(filter==='all'||(filter==='active'&&s.active!==false)||(filter==='inactive'&&s.active===false))&&(!q||`${s.name} ${s.category} ${s.contact} ${s.phone} ${s.email} ${s.address}`.toLowerCase().includes(q))).sort((a,b)=>(a.active===b.active?String(a.name).localeCompare(String(b.name),'ru'):(a.active===false?1:-1)));
|
||||||
const cards=list.map(s=>{
|
const cards=list.map(s=>{
|
||||||
const products=stock.filter(p=>String(p.supplierId||'')===String(s.id)), stats=purchaseStats(s.id), phoneHref=String(s.phone||'').replace(/[^\d+]/g,'');
|
const products=stock.filter(p=>String(p.supplierId||'')===String(s.id)), stats=purchaseStats(s.id), phoneHref=String(s.phone||'').replace(/[^\d+]/g,'');
|
||||||
return `<article class="sup-card ${s.active===false?'inactive':''}"><div class="sup-card-head"><div><h3>${esc(s.name)}</h3><small class="hint">${esc(s.category||'Категория не указана')}</small></div><span class="sup-badge ${s.active===false?'off':''}">${s.active===false?'Неактивен':'Активен'}</span></div><div class="sup-meta">${s.contact?`<span>Контакт: <b>${esc(s.contact)}</b></span>`:''}${s.phone?`<span>Телефон: <a href="tel:${esc(phoneHref)}">${esc(s.phone)}</a></span>`:''}${s.email?`<span>Email: <a href="mailto:${esc(s.email)}">${esc(s.email)}</a></span>`:''}${s.address?`<span>Адрес: ${esc(s.address)}</span>`:''}${s.deliveryDays?`<span>Доставка: ${esc(s.deliveryDays)}</span>`:''}${Number(s.minOrder||0)>0?`<span>Минимальный заказ: <b>${fmt(s.minOrder)}</b></span>`:''}</div><div class="sup-stats"><div><small>Продуктов</small><b>${products.length}</b></div><div><small>Закупок</small><b>${stats.count}</b></div><div><small>Последняя</small><b>${dateLabel(stats.last)}</b></div></div>${s.notes?`<div class="sup-notes">${esc(s.notes)}</div>`:''}<div class="sup-actions"><button class="outline" type="button" onclick="sunSupplierProducts('${esc(s.id)}')">Продукты</button><button class="primary" type="button" onclick="sunEditSupplier('${esc(s.id)}')">Изменить</button></div></article>`;
|
return `<article class="sup-card ${s.active===false?'inactive':''}"><div class="sup-card-head"><div><h3>${esc(s.name)}</h3><small class="hint">${esc(s.category||'Категория не указана')}</small></div><span class="sup-badge ${s.active===false?'off':''}">${s.active===false?'Неактивен':'Активен'}</span></div><div class="sup-meta">${s.contact?`<span>Контакт: <b>${esc(s.contact)}</b></span>`:''}${s.phone?`<span>Телефон: <a href="tel:${esc(phoneHref)}">${esc(s.phone)}</a></span>`:''}${s.email?`<span>Email: <a href="mailto:${esc(s.email)}">${esc(s.email)}</a></span>`:''}${s.address?`<span>Адрес: ${esc(s.address)}</span>`:''}${s.deliveryDays?`<span>Доставка: ${esc(s.deliveryDays)}</span>`:''}${Number(s.minOrder||0)>0?`<span>Минимальный заказ: <b>${fmt(s.minOrder)}</b></span>`:''}</div><div class="sup-stats"><div><small>Продуктов</small><b>${products.length}</b></div><div><small>Закупок</small><b>${stats.count}</b></div><div><small>Последняя</small><b>${dateLabel(stats.last)}</b></div></div>${s.notes?`<div class="sup-notes">${esc(s.notes)}</div>`:''}<div class="sup-actions"><button class="outline" type="button" onclick="sunSupplierProducts('${esc(s.id)}')">Продукты</button><button class="primary" type="button" onclick="sunEditSupplier('${esc(s.id)}')">Изменить</button></div></article>`;
|
||||||
@ -2816,6 +2823,7 @@ if (typeof crypto !== 'undefined' && typeof crypto.randomUUID !== 'function') {
|
|||||||
btn.onclick=()=>{document.querySelectorAll('.view').forEach(v=>v.classList.remove('on'));view.classList.add('on');[...nav.querySelectorAll('button')].forEach(b=>b.classList.remove('on'));btn.classList.add('on');render();};
|
btn.onclick=()=>{document.querySelectorAll('.view').forEach(v=>v.classList.remove('on'));view.classList.add('on');[...nav.querySelectorAll('button')].forEach(b=>b.classList.remove('on'));btn.classList.add('on');render();};
|
||||||
window.addEventListener('sun:stock-changed',()=>{if(view.classList.contains('on'))render();});
|
window.addEventListener('sun:stock-changed',()=>{if(view.classList.contains('on'))render();});
|
||||||
window.sunRenderSuppliers=render;
|
window.sunRenderSuppliers=render;
|
||||||
|
window.addEventListener('caterium:training-catalog-changed',()=>{suppliers=readArray(KEY);if(view.classList.contains('on'))render();});
|
||||||
render();
|
render();
|
||||||
})();
|
})();
|
||||||
|
|
||||||
@ -4955,13 +4963,13 @@ window.SunQR = {
|
|||||||
if (orderMatches.length) groups.push({name:'Заказы', items:orderMatches});
|
if (orderMatches.length) groups.push({name:'Заказы', items:orderMatches});
|
||||||
const clientMatches = clientIndex().filter(c => [c.name,c.phone,...c.addresses].join(' ').toLowerCase().includes(q)).slice(0,10).map(c => ({title:c.name, meta:`${c.phone || 'без телефона'} · заказов: ${c.orders.length}`, action:'client', id:encodeURIComponent(c.key)}));
|
const clientMatches = clientIndex().filter(c => [c.name,c.phone,...c.addresses].join(' ').toLowerCase().includes(q)).slice(0,10).map(c => ({title:c.name, meta:`${c.phone || 'без телефона'} · заказов: ${c.orders.length}`, action:'client', id:encodeURIComponent(c.key)}));
|
||||||
if (clientMatches.length) groups.push({name:'Клиенты', items:clientMatches});
|
if (clientMatches.length) groups.push({name:'Клиенты', items:clientMatches});
|
||||||
const boxMatches = appBoxes().filter(b => b.hidden !== true && [b.name,b.catalogSection,(b.ingredients||[]).map(x=>x[0]).join(' ')].join(' ').toLowerCase().includes(q)).slice(0,12).map(b=>({title:b.name,meta:`${moneyFmt(window.CateriumPricing.price(b))} · ${b.catalogSection || 'каталог'}`,action:'box',id:b.id}));
|
const boxMatches = appBoxes().filter(b => (window.CateriumTrainingCatalog?.visible(b)??(b.hidden !== true)) && [b.name,b.catalogSection,(b.ingredients||[]).map(x=>x[0]).join(' ')].join(' ').toLowerCase().includes(q)).slice(0,12).map(b=>({title:b.name,meta:`${moneyFmt(window.CateriumPricing.price(b))} · ${b.catalogSection || 'каталог'}`,action:'box',id:b.id}));
|
||||||
if (boxMatches.length) groups.push({name:'Каталог',items:boxMatches});
|
if (boxMatches.length) groups.push({name:'Каталог',items:boxMatches});
|
||||||
const stockMatches = getStock().filter(p => [p.name,p.unit].join(' ').toLowerCase().includes(q)).slice(0,12).map(p=>({title:p.name,meta:`Остаток ${numFmt(p.qty)} ${p.unit} · ${moneyFmt(p.cost)}`,action:'stock',id:p.id}));
|
const stockMatches = getStock().filter(p=>window.CateriumTrainingCatalog?.visible(p,'stock')??true).filter(p => [p.name,p.unit].join(' ').toLowerCase().includes(q)).slice(0,12).map(p=>({title:p.name,meta:`Остаток ${numFmt(p.qty)} ${p.unit} · ${moneyFmt(p.cost)}`,action:'stock',id:p.id}));
|
||||||
if (stockMatches.length) groups.push({name:'Склад',items:stockMatches});
|
if (stockMatches.length) groups.push({name:'Склад',items:stockMatches});
|
||||||
const employeeMatches = getEmployees().filter(e=>[e.name,e.role,e.phone,e.note].join(' ').toLowerCase().includes(q)).slice(0,10).map(e=>({title:e.name,meta:`${e.role || 'сотрудник'} · ${e.phone || 'без телефона'}`,action:'employee',id:e.id}));
|
const employeeMatches = getEmployees().filter(e=>[e.name,e.role,e.phone,e.note].join(' ').toLowerCase().includes(q)).slice(0,10).map(e=>({title:e.name,meta:`${e.role || 'сотрудник'} · ${e.phone || 'без телефона'}`,action:'employee',id:e.id}));
|
||||||
if(employeeMatches.length)groups.push({name:'Команда',items:employeeMatches});
|
if(employeeMatches.length)groups.push({name:'Команда',items:employeeMatches});
|
||||||
const supplierMatches = getSuppliers().filter(s=>[s.name,s.contact,s.phone,s.email,s.address].join(' ').toLowerCase().includes(q)).slice(0,10).map(s=>({title:s.name,meta:`${s.contact || ''} ${s.phone || ''}`.trim() || 'поставщик',action:'supplier',id:s.id}));
|
const supplierMatches = getSuppliers().filter(s=>window.CateriumTrainingCatalog?.visible(s,'suppliers')??true).filter(s=>[s.name,s.contact,s.phone,s.email,s.address].join(' ').toLowerCase().includes(q)).slice(0,10).map(s=>({title:s.name,meta:`${s.contact || ''} ${s.phone || ''}`.trim() || 'поставщик',action:'supplier',id:s.id}));
|
||||||
if(supplierMatches.length)groups.push({name:'Поставщики',items:supplierMatches});
|
if(supplierMatches.length)groups.push({name:'Поставщики',items:supplierMatches});
|
||||||
return groups;
|
return groups;
|
||||||
}
|
}
|
||||||
@ -7684,7 +7692,7 @@ button.sun-busy-safe:after{content:"";width:12px;height:12px;margin-left:7px;bor
|
|||||||
return Object.entries(map).find(([name])=>title.startsWith(name))?.[1]??0;
|
return Object.entries(map).find(([name])=>title.startsWith(name))?.[1]??0;
|
||||||
}
|
}
|
||||||
function categoryName(cat){return ({0:'Боксы',1:'Посуда',2:'Дополнения',3:'Напитки',4:'Доставка',5:'Премиум'})[cat]||'Каталог'}
|
function categoryName(cat){return ({0:'Боксы',1:'Посуда',2:'Дополнения',3:'Напитки',4:'Доставка',5:'Премиум'})[cat]||'Каталог'}
|
||||||
function getCatalogItems(cat){try{return (Array.isArray(boxes)?boxes:[]).filter(x=>x.hidden!==true&&Number(x.category||0)===Number(cat))}catch(_){return[]}}
|
function getCatalogItems(cat){try{return (Array.isArray(boxes)?boxes:[]).filter(x=>(window.CateriumTrainingCatalog?.visible(x)??(x.hidden!==true))&&Number(x.category||0)===Number(cat))}catch(_){return[]}}
|
||||||
function ensureCatalogPdfButton(){
|
function ensureCatalogPdfButton(){
|
||||||
const head=document.querySelector('#new .catalog .catalog-head');const configure=qa('button',head||document).find(b=>b.textContent.trim()==='Настроить');if(!head||!configure)return;
|
const head=document.querySelector('#new .catalog .catalog-head');const configure=qa('button',head||document).find(b=>b.textContent.trim()==='Настроить');if(!head||!configure)return;
|
||||||
let btn=$('sunCatalogPdfButton');if(!btn){btn=document.createElement('button');btn.id='sunCatalogPdfButton';btn.type='button';btn.className='outline sun-catalog-pdf-button';btn.textContent='Скачать PDF';btn.title='Текущий раздел: одна позиция на одной странице';configure.insertAdjacentElement('beforebegin',btn);}
|
let btn=$('sunCatalogPdfButton');if(!btn){btn=document.createElement('button');btn.id='sunCatalogPdfButton';btn.type='button';btn.className='outline sun-catalog-pdf-button';btn.textContent='Скачать PDF';btn.title='Текущий раздел: одна позиция на одной странице';configure.insertAdjacentElement('beforebegin',btn);}
|
||||||
@ -8455,7 +8463,7 @@ body #sunBlankSettingsCardV3{display:none!important}
|
|||||||
const hide=currentCategory()===4;btn.hidden=hide;btn.style.display=hide?'none':'';
|
const hide=currentCategory()===4;btn.hidden=hide;btn.style.display=hide?'none':'';
|
||||||
}
|
}
|
||||||
async function printLiveCatalog(){
|
async function printLiveCatalog(){
|
||||||
const cat=currentCategory(),items=allBoxes().filter(item=>item.hidden!==true&&Number(item.category||0)===Number(cat));
|
const cat=currentCategory(),items=allBoxes().filter(item=>(window.CateriumTrainingCatalog?.visible(item)??(item.hidden!==true))&&Number(item.category||0)===Number(cat));
|
||||||
if(!items.length){window.SunEnterprise?.toast?.('\u0412 \u044d\u0442\u043e\u043c \u0440\u0430\u0437\u0434\u0435\u043b\u0435 \u043d\u0435\u0442 \u043f\u043e\u0437\u0438\u0446\u0438\u0439.','warn');return}
|
if(!items.length){window.SunEnterprise?.toast?.('\u0412 \u044d\u0442\u043e\u043c \u0440\u0430\u0437\u0434\u0435\u043b\u0435 \u043d\u0435\u0442 \u043f\u043e\u0437\u0438\u0446\u0438\u0439.','warn');return}
|
||||||
let root=$('sunLiveCatalogPrintRoot');if(!root){root=document.createElement('div');root.id='sunLiveCatalogPrintRoot';document.body.appendChild(root)}
|
let root=$('sunLiveCatalogPrintRoot');if(!root){root=document.createElement('div');root.id='sunLiveCatalogPrintRoot';document.body.appendChild(root)}
|
||||||
const total=items.length,dateText=new Date().toLocaleDateString('ru-RU');
|
const total=items.length,dateText=new Date().toLocaleDateString('ru-RU');
|
||||||
|
|||||||
2
public/legacy/bootstrap.js
vendored
2
public/legacy/bootstrap.js
vendored
File diff suppressed because one or more lines are too long
@ -1,6 +1,7 @@
|
|||||||
const CACHE='sun-catering-pwa-v110-20260918-ui-stability-20260919-client-menu-support-bot';
|
const CACHE='sun-catering-pwa-v110-20260918-ui-stability-20260919-client-menu-support-bot-training-catalog';
|
||||||
const VERSION='20260918-ui-stability';
|
const VERSION='20260918-ui-stability';
|
||||||
const CORE=[
|
const CORE=[
|
||||||
|
'./core/training-catalog.js?v=20260920-training',
|
||||||
'./core/catalog-pricing.js?v=20260919-client-menu','./core/client-menu.css?v=20260919-client-menu',
|
'./core/catalog-pricing.js?v=20260919-client-menu','./core/client-menu.css?v=20260919-client-menu',
|
||||||
'./vendor/supabase-2.112.4.min.js',
|
'./vendor/supabase-2.112.4.min.js',
|
||||||
`./core/help-center.js?v=${VERSION}`,`./core/help-center.css?v=${VERSION}`,`./help/knowledge-v1.json?v=${VERSION}`,
|
`./core/help-center.js?v=${VERSION}`,`./core/help-center.css?v=${VERSION}`,`./help/knowledge-v1.json?v=${VERSION}`,
|
||||||
@ -12,6 +13,7 @@ const CORE=[
|
|||||||
'./offer-templates/thumb-light.jpg','./offer-templates/thumb-editorial-grid.jpg','./offer-templates/thumb-midnight-glass.jpg','./offer-templates/thumb-emerald-gold.jpg'
|
'./offer-templates/thumb-light.jpg','./offer-templates/thumb-editorial-grid.jpg','./offer-templates/thumb-midnight-glass.jpg','./offer-templates/thumb-emerald-gold.jpg'
|
||||||
];
|
];
|
||||||
const CRITICAL_FRESH=new Set([
|
const CRITICAL_FRESH=new Set([
|
||||||
|
'/core/training-catalog.js',
|
||||||
'/core/catalog-pricing.js','/core/client-menu.css',
|
'/core/catalog-pricing.js','/core/client-menu.css',
|
||||||
'/core/help-center.js','/core/help-center.css','/help/knowledge-v1.json',
|
'/core/help-center.js','/core/help-center.css','/help/knowledge-v1.json',
|
||||||
'/core/mobile-order.js','/core/cloud-transport.js','/core/trial-demo.js','/core/proposal-layout.js',
|
'/core/mobile-order.js','/core/cloud-transport.js','/core/trial-demo.js','/core/proposal-layout.js',
|
||||||
|
|||||||
@ -2,13 +2,13 @@ import { defineConfig, devices } from '@playwright/test';
|
|||||||
import {fileURLToPath} from 'node:url';
|
import {fileURLToPath} from 'node:url';
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
testDir:'.',
|
testDir:'.',
|
||||||
testMatch:['mobile-menu.spec.mjs','client-menu.spec.mjs','ui-stability.spec.mjs','help-center.spec.mjs','app.spec.mjs','theme-startup.spec.mjs','company-branding.spec.mjs','order-import.spec.mjs','account-access.spec.mjs','banquet-menu.spec.mjs','calendar-print.spec.mjs','login-recovery.spec.mjs','workspace-loading.spec.mjs','trial-demo.spec.mjs','proposal-quality.spec.mjs'],
|
testMatch:['training-catalog.spec.mjs','mobile-menu.spec.mjs','client-menu.spec.mjs','ui-stability.spec.mjs','help-center.spec.mjs','app.spec.mjs','theme-startup.spec.mjs','company-branding.spec.mjs','order-import.spec.mjs','account-access.spec.mjs','banquet-menu.spec.mjs','calendar-print.spec.mjs','login-recovery.spec.mjs','workspace-loading.spec.mjs','trial-demo.spec.mjs','proposal-quality.spec.mjs'],
|
||||||
timeout:30000,
|
timeout:30000,
|
||||||
use:{baseURL:'http://127.0.0.1:4173'},
|
use:{baseURL:'http://127.0.0.1:4173'},
|
||||||
webServer:{command:'npx http-server public -p 4173 -c-1',cwd:fileURLToPath(new URL('../',import.meta.url)),port:4173,reuseExistingServer:true},
|
webServer:{command:'npx http-server public -p 4173 -c-1',cwd:fileURLToPath(new URL('../',import.meta.url)),port:4173,reuseExistingServer:true},
|
||||||
projects:[
|
projects:[
|
||||||
{name:'iphone-pdf',testMatch:['proposal-quality.spec.mjs'],grep:/all six selections|transparent wide|an actual offer downloads/,use:{...devices['iPhone 13'],serviceWorkers:'block'}},
|
{name:'iphone-pdf',testMatch:['proposal-quality.spec.mjs'],grep:/all six selections|transparent wide|an actual offer downloads/,use:{...devices['iPhone 13'],serviceWorkers:'block'}},
|
||||||
{name:'iphone-webkit',testMatch:['mobile-menu.spec.mjs','client-menu.spec.mjs','ui-stability.spec.mjs','help-center.spec.mjs','login-recovery.spec.mjs','workspace-loading.spec.mjs','account-access.spec.mjs','calendar-print.spec.mjs'],use:{...devices['iPhone 13'],serviceWorkers:'block'}},
|
{name:'iphone-webkit',testMatch:['training-catalog.spec.mjs','mobile-menu.spec.mjs','client-menu.spec.mjs','ui-stability.spec.mjs','help-center.spec.mjs','login-recovery.spec.mjs','workspace-loading.spec.mjs','account-access.spec.mjs','calendar-print.spec.mjs'],use:{...devices['iPhone 13'],serviceWorkers:'block'}},
|
||||||
{name:'desktop',use:{...devices['Desktop Chrome']}},
|
{name:'desktop',use:{...devices['Desktop Chrome']}},
|
||||||
{name:'mobile-390',use:{viewport:{width:390,height:844},isMobile:true,hasTouch:true}}
|
{name:'mobile-390',use:{viewport:{width:390,height:844},isMobile:true,hasTouch:true}}
|
||||||
]
|
]
|
||||||
|
|||||||
47
tests/production-training-catalog.mjs
Normal file
47
tests/production-training-catalog.mjs
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import fs from 'node:fs/promises';
|
||||||
|
import {chromium,expect} from '@playwright/test';
|
||||||
|
import {trainingInit} from './training-catalog-fixture.mjs';
|
||||||
|
|
||||||
|
// Exercise the actual published assets with a synthetic profile. All traffic
|
||||||
|
// except same-origin static GETs is blocked, so no real workspace is touched.
|
||||||
|
const base=new URL(process.env.TIMEWEB_BASE_URL||'https://app.caterium.ru');
|
||||||
|
if(base.protocol!=='https:')throw new Error('Published UI verification requires HTTPS');
|
||||||
|
const output='production-ui-results';await fs.mkdir(output,{recursive:true});
|
||||||
|
const browser=await chromium.launch(),results=[];
|
||||||
|
try{
|
||||||
|
for(const width of [390,1440]){
|
||||||
|
const context=await browser.newContext({viewport:{width,height:900},serviceWorkers:'block'});
|
||||||
|
try{
|
||||||
|
await context.route('**/*',route=>{const r=route.request(),u=new URL(r.url());return r.method()==='GET'&&u.origin===base.origin&&!u.pathname.startsWith('/api/')?route.continue():route.abort();});
|
||||||
|
await context.addInitScript(trainingInit);
|
||||||
|
const page=await context.newPage();page.on('dialog',d=>d.accept());
|
||||||
|
await page.goto(base.href,{waitUntil:'domcontentloaded',timeout:60000});
|
||||||
|
await expect(page.locator('body > header')).toBeVisible({timeout:20000});
|
||||||
|
await page.waitForFunction(()=>window.CateriumDataV1773&&window.SunOpsUXV1762&&window.CateriumTrialDemo&&window.CateriumTrainingCatalog);
|
||||||
|
const nav=page.locator('header nav'),toggle=page.locator('#ctTrainingEnabled');
|
||||||
|
const settings=async()=>{await nav.getByRole('button',{name:'Настройки',exact:true}).click();await expect(toggle).toBeVisible();};
|
||||||
|
await settings();await expect(toggle).not.toBeChecked();
|
||||||
|
const before=await page.evaluate(()=>({orders:localStorage.sunOrders,box:boxes.find(b=>b.id==='own-box'),stock:JSON.parse(localStorage.sunStock).find(p=>p.id==='own-stock')}));
|
||||||
|
await toggle.check();await expect(toggle).toBeEnabled();
|
||||||
|
await expect.poll(()=>page.evaluate(()=>CateriumTrainingCatalog.enabled())).toBe(true);
|
||||||
|
assert.equal(await page.evaluate(()=>boxes.length),45);
|
||||||
|
await page.locator('#ctTrainingSettings').scrollIntoViewIfNeeded();
|
||||||
|
await page.screenshot({path:`${output}/training-settings-${width}.png`,animations:'disabled'});
|
||||||
|
await nav.getByRole('button',{name:'Меню',exact:true}).click();await expect(page.locator('[data-menu-item-v1762]')).toHaveCount(11);
|
||||||
|
await expect(page.locator('[data-menu-item-v1762="demo-v1-bruschetta-tomato"] .ct-training-badge')).toHaveText('Учебное');
|
||||||
|
await page.screenshot({path:`${output}/training-catalog-${width}.png`,animations:'disabled'});
|
||||||
|
await settings();await toggle.uncheck();await expect(toggle).toBeEnabled();
|
||||||
|
await expect.poll(()=>page.evaluate(()=>CateriumTrainingCatalog.enabled())).toBe(false);
|
||||||
|
await nav.getByRole('button',{name:'Меню',exact:true}).click();await expect(page.locator('[data-menu-item-v1762]')).toHaveCount(1);
|
||||||
|
const after=await page.evaluate(()=>({orders:localStorage.sunOrders,box:boxes.find(b=>b.id==='own-box'),stock:JSON.parse(localStorage.sunStock).find(p=>p.id==='own-stock')}));
|
||||||
|
assert.deepEqual(after,before);
|
||||||
|
assert.equal(await page.evaluate(()=>boxes.length),45);
|
||||||
|
await settings();await toggle.check();await expect(toggle).toBeEnabled();
|
||||||
|
assert.equal(await page.evaluate(()=>boxes.length),45);
|
||||||
|
results.push({width,defaultOff:true,readyExamples:44,hideRestoresOwnCatalog:true,repeatDoesNotDuplicate:true,ordersAndOwnDataPreserved:true});
|
||||||
|
console.log(`PASS published training catalog ${width}px: opt-in, complete examples, hide/show, preserved own data and orders`);
|
||||||
|
}finally{await context.close();}
|
||||||
|
}
|
||||||
|
await fs.writeFile(`${output}/training-catalog-result.json`,JSON.stringify({base:base.href,checkedAt:new Date().toISOString(),backend:'synthetic, network-blocked',results},null,2));
|
||||||
|
}finally{await browser.close();}
|
||||||
17
tests/training-catalog-fixture.mjs
Normal file
17
tests/training-catalog-fixture.mjs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
export function trainingInit(){
|
||||||
|
const ws='training-workspace',user={id:'training-user',email:'training@example.invalid',email_confirmed_at:'2026-01-01T00:00:00Z'};
|
||||||
|
localStorage.setItem('sunCloudV2Config',JSON.stringify({workspaceId:ws,localWorkspaceId:ws,tenantStorageReady:true,autoSync:false}));
|
||||||
|
if(!localStorage.getItem('training-fixture-ready')){
|
||||||
|
localStorage.setItem('sunBoxes',JSON.stringify([{id:'own-box',name:'Мой настоящий бокс',category:0,price:1700,weight:'900 г',pieces:12,ingredients:[['Томаты',1,'кг']]}]));
|
||||||
|
localStorage.setItem('sunOrders',JSON.stringify([{id:1,event:'Настоящий заказ',date:'2099-12-20',time:'12:00',status:'Новый',lines:[{id:'own-box',qty:2,price:1500}],total:3000}]));
|
||||||
|
localStorage.setItem('sunStock',JSON.stringify([{id:'own-stock',name:'Томаты',unit:'кг',cost:380,qty:12,min:1,supplierId:'own-supplier'}]));
|
||||||
|
localStorage.setItem('sunSuppliers',JSON.stringify([{id:'own-supplier',name:'Мой поставщик',active:true,phone:'70000000000'}]));
|
||||||
|
localStorage.setItem('training-fixture-ready','1');
|
||||||
|
}
|
||||||
|
const features=Object.fromEntries('orders calendar clients catalog_view catalog_edit production shopping stock routes mailings money stats_basic stats_advanced team suppliers print settings branding client_offers offer_templates backups audit users_manage'.split(' ').map(k=>[k,true]));
|
||||||
|
window.supabase={createClient:()=>({
|
||||||
|
auth:{onAuthStateChange:()=>({data:{subscription:{unsubscribe(){}}}}),getSession:async()=>({data:{session:{user}},error:null}),getUser:async()=>({data:{user},error:null})},
|
||||||
|
rpc:async name=>({data:name==='sun_my_workspaces'?[{id:ws,name:'Тестовая компания',role:'admin',is_active:true,permissions:{}}]:name==='sun_subscription_snapshot'?{plan_id:'full',plan_name:'Полный',status:'active',access_mode:'full',features}:name==='sun_is_platform_admin'?false:name==='caterium_trial_demo_status'?{canInstall:false,canUpgrade:false}:null,error:null}),
|
||||||
|
channel:()=>({on(){return this},subscribe(){return this}}),removeChannel(){}
|
||||||
|
})};
|
||||||
|
}
|
||||||
96
tests/training-catalog.spec.mjs
Normal file
96
tests/training-catalog.spec.mjs
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import {test,expect} from '@playwright/test';
|
||||||
|
import {trainingInit} from './training-catalog-fixture.mjs';
|
||||||
|
import {demo} from '../ops/demo/trial-demo-data.mjs';
|
||||||
|
|
||||||
|
test.use({serviceWorkers:'block'});
|
||||||
|
const nav=page=>page.locator('header nav');
|
||||||
|
const setting=page=>page.locator('#ctTrainingEnabled');
|
||||||
|
async function settings(page){await nav(page).getByRole('button',{name:'Настройки',exact:true}).click();await expect(setting(page)).toBeVisible();}
|
||||||
|
async function boot(page){
|
||||||
|
page.on('dialog',d=>d.accept());
|
||||||
|
await page.route('**/*',route=>{const r=route.request(),u=new URL(r.url());return r.method()==='GET'&&u.origin==='http://127.0.0.1:4173'&&!u.pathname.startsWith('/api/')?route.continue():route.abort();});
|
||||||
|
await page.addInitScript(trainingInit);
|
||||||
|
await page.goto('/index.html',{waitUntil:'domcontentloaded'});
|
||||||
|
await page.waitForFunction(()=>window.CateriumDataV1773&&window.SunOpsUXV1762&&window.CateriumTrialDemo&&window.__cateriumOrderEnhancementsV1775);
|
||||||
|
await expect(page.locator('body > header')).toBeVisible();await settings(page);
|
||||||
|
}
|
||||||
|
async function toggle(page,on){await setting(page).setChecked(on);await expect(setting(page)).toBeEnabled();await expect.poll(()=>page.evaluate(()=>CateriumTrainingCatalog.enabled())).toBe(on);}
|
||||||
|
const state=page=>page.evaluate(()=>Object.fromEntries(['sunBoxes','sunStock','sunSuppliers','sunOrders','sunStockMoves','sunTrialDemoV1'].map(k=>[k,localStorage.getItem(k)])));
|
||||||
|
const own=page=>page.evaluate(()=>({box:JSON.parse(localStorage.sunBoxes).find(x=>x.id==='own-box'),stock:JSON.parse(localStorage.sunStock).find(x=>x.id==='own-stock'),supplier:JSON.parse(localStorage.sunSuppliers).find(x=>x.id==='own-supplier'),orders:localStorage.sunOrders,moves:localStorage.getItem('sunStockMoves')}));
|
||||||
|
|
||||||
|
test('training is opt-in in an existing paid profile, adds complete examples, hides them and preserves originals',async({page},info)=>{
|
||||||
|
test.setTimeout(60000); // Full multi-page round trip plus reload in iPhone WebKit.
|
||||||
|
await boot(page);await expect(setting(page)).not.toBeChecked();
|
||||||
|
const before=await own(page);expect(await page.evaluate(()=>boxes.length)).toBe(1);
|
||||||
|
await toggle(page,true);expect(await page.evaluate(()=>boxes.length)).toBe(45);
|
||||||
|
expect(await own(page)).toEqual(before);
|
||||||
|
const source=await page.evaluate(()=>boxes.find(x=>x.id==='demo-v1-bruschetta-tomato'));
|
||||||
|
expect(source.photo).toBeTruthy();expect(source.composition.length).toBeGreaterThan(0);expect(source.ttk.rows.length).toBeGreaterThan(0);expect(source.ingredients.some(i=>i[0]==='Учебное: Томаты')).toBe(true);
|
||||||
|
await page.locator('#ctTrainingSettings').scrollIntoViewIfNeeded();
|
||||||
|
await page.screenshot({path:info.outputPath('training-settings.png'),animations:'disabled'});
|
||||||
|
await nav(page).getByRole('button',{name:'Меню',exact:true}).click();await expect(page.locator('[data-menu-item-v1762]')).toHaveCount(11);
|
||||||
|
await expect(page.locator('[data-menu-item-v1762="demo-v1-bruschetta-tomato"] .ct-training-badge')).toHaveText('Учебное');
|
||||||
|
await nav(page).getByRole('button',{name:'Новый заказ',exact:true}).click();
|
||||||
|
await page.getByRole('button',{name:'Банкетное меню',exact:true}).click();await expect(page.locator('[data-banquet-row]')).toHaveCount(18);
|
||||||
|
await settings(page);await toggle(page,false);
|
||||||
|
await nav(page).getByRole('button',{name:'Меню',exact:true}).click();await page.locator('[data-menu-cat-v1762="0"]').click();await expect(page.locator('[data-menu-item-v1762]')).toHaveCount(1);
|
||||||
|
await nav(page).getByRole('button',{name:'Новый заказ',exact:true}).click();await page.getByRole('button',{name:'Банкетное меню',exact:true}).click();await expect(page.locator('[data-banquet-row]')).toHaveCount(0);await page.getByRole('button',{name:'Боксы',exact:true}).click();await expect(page.locator('#tiles .tile:not(.add)')).toHaveCount(1);await expect(page.locator('#ctTrialDemo')).toHaveCount(0);
|
||||||
|
await nav(page).getByRole('button',{name:'Склад',exact:true}).click();await expect(page.locator('#stockContent')).not.toContainText('Учебное:');
|
||||||
|
await nav(page).getByRole('button',{name:'Поставщики',exact:true}).click();await expect(page.locator('#supList')).not.toContainText('Демо:');
|
||||||
|
expect(await own(page)).toEqual(before);expect(await page.evaluate(()=>boxes.length)).toBe(45);
|
||||||
|
await page.reload();await page.waitForFunction(()=>window.SunOpsUXV1762&&window.CateriumDataV1773);await settings(page);await expect(setting(page)).not.toBeChecked();await toggle(page,true);
|
||||||
|
expect(await page.evaluate(()=>boxes.length)).toBe(45);expect(await own(page)).toEqual(before);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('TTKs, trial order and preparation keep their complete records after hiding and restore without resetting edits',async({page})=>{
|
||||||
|
await boot(page);await toggle(page,true);
|
||||||
|
await nav(page).getByRole('button',{name:'Новый заказ',exact:true}).click();await expect(page.locator('#ctTrialDemo')).toBeVisible();
|
||||||
|
await page.evaluate(()=>{window.ctGuideEvents=[];const card=document.getElementById('ctTrialDemo');new MutationObserver(rs=>{ctGuideEvents.push(...rs.map(r=>({type:r.type,attribute:r.attributeName,open:card.querySelector('details')?.open,signature:card.dataset.signature})));}).observe(card,{childList:true,subtree:true,attributes:true,attributeFilter:['open','data-signature']});});
|
||||||
|
await page.locator('#ctTrialDemo summary').click();await page.locator('[data-demo-ttk]').click();await expect(page.locator('#ctDemoTtk')).toContainText('Брутто');await expect(page.locator('#ctDemoTtk')).toContainText('840 г');await page.getByRole('button',{name:'Закрыть ТТК',exact:true}).click();
|
||||||
|
await page.locator('[data-demo-order]').click();await expect(page.locator('#ctDemoMessage')).toContainText('9 700 ₽');
|
||||||
|
const order=await page.evaluate(()=>orders.find(o=>o.demo===true));expect(order.autoCompletionDisabled).toBe(true);
|
||||||
|
await page.evaluate(()=>{const items=CateriumDataV1773.catalog.list();const box=items.find(b=>b.id==='demo-v1-bruschetta-tomato');box.price=2222;box.name='Мой учебный рецепт';CateriumDataV1773.catalog.replace(items,{persistLocal:false});const stock=JSON.parse(localStorage.sunStock);stock.find(s=>s.id==='demo-v1-stock-baguette').qty=7;localStorage.setItem('sunStock',JSON.stringify(stock));});
|
||||||
|
const before=await state(page);await settings(page);await toggle(page,false);
|
||||||
|
expect(await page.evaluate(id=>orders.find(o=>o.id===id),order.id)).toEqual(order);
|
||||||
|
expect(await page.evaluate(()=>CateriumDataV1773.catalog.get('demo-v1-bruschetta-tomato').ttk.rows.length)).toBeGreaterThan(0);
|
||||||
|
await toggle(page,true);
|
||||||
|
const after=await state(page);for(const k of ['sunBoxes','sunOrders','sunStock','sunSuppliers','sunStockMoves'])expect(after[k]).toBe(before[k]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('old automatically seeded boxes are hidden until opt-in and their customised prices are not overwritten',async({page})=>{
|
||||||
|
await boot(page);
|
||||||
|
await page.evaluate(d=>{d.boxes[0].price=4321;CateriumDataV1773.catalog.replace([...CateriumDataV1773.catalog.list(),...d.boxes],{persistLocal:false});localStorage.setItem('sunTrialDemoV1',JSON.stringify({version:1,batch:d.batch,scenario:d.scenario}));CateriumTrainingCatalog.refresh();},demo);
|
||||||
|
await nav(page).getByRole('button',{name:'Меню',exact:true}).click();await expect(page.locator('[data-menu-item-v1762]')).toHaveCount(1);
|
||||||
|
await settings(page);await toggle(page,true);expect(await page.evaluate(()=>boxes.find(b=>b.id==='demo-v1-bruschetta-tomato').price)).toBe(4321);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the checkbox is profile- and company-specific; read-only and support access do not gain write rights',async({page})=>{
|
||||||
|
await boot(page);await toggle(page,true);const before=await state(page);
|
||||||
|
await page.evaluate(()=>{window.savedSession=SunCloudV2.getSession;SunCloudV2.getSession=()=>({user:{id:'another-user'}});CateriumTrainingCatalog.refresh();});await expect(setting(page)).not.toBeChecked();
|
||||||
|
await page.evaluate(()=>{SunCloudV2.getSession=window.savedSession;window.savedWorkspace=SunCloudV2.getWorkspace;SunCloudV2.getWorkspace=()=>({id:'another-workspace'});CateriumTrainingCatalog.refresh();});await expect(setting(page)).not.toBeChecked();
|
||||||
|
// Keep read access, deny writes. Revoking read access too correctly triggers
|
||||||
|
// the existing RBAC cache redaction and is not a training-catalog mutation.
|
||||||
|
await page.evaluate(()=>{SunCloudV2.getWorkspace=window.savedWorkspace;window.savedPermission=SunCloudV2.hasPermission;SunCloudV2.hasPermission=p=>p==='app.read'||p.endsWith('.view');CateriumTrainingCatalog.refresh();});await expect(setting(page)).toBeDisabled();
|
||||||
|
expect(await page.evaluate(()=>CateriumTrainingCatalog.setEnabled(false).then(()=>false,()=>true))).toBe(true);
|
||||||
|
await page.evaluate(()=>{SunCloudV2.hasPermission=window.savedPermission;SunCloudV2.isSupportMode=()=>true;CateriumTrainingCatalog.refresh();});await expect(setting(page)).toBeDisabled();
|
||||||
|
expect(await state(page)).toEqual(before);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('failed example download leaves the catalog, inventory and preference unchanged and can be retried',async({page})=>{
|
||||||
|
await boot(page);const before=await state(page);
|
||||||
|
await page.route('**/demo/catalog-v1.json?**',route=>route.fulfill({status:503,body:'temporarily unavailable'}));
|
||||||
|
// A fast rejected download correctly reverts the checkbox before check()
|
||||||
|
// could assert a checked state. Click and assert the intended error state.
|
||||||
|
await setting(page).click();await expect(page.locator('#ctTrainingStatus')).toContainText('Не удалось загрузить');await expect(setting(page)).not.toBeChecked();await expect(setting(page)).toBeEnabled();expect(await state(page)).toEqual(before);
|
||||||
|
await page.unroute('**/demo/catalog-v1.json?**');await toggle(page,true);expect(await page.evaluate(()=>boxes.length)).toBe(45);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a delayed seed cannot be applied to another company after switching',async({page})=>{
|
||||||
|
await boot(page);const before=await state(page);let release,requested=false;const wait=new Promise(resolve=>{release=resolve;});
|
||||||
|
await page.route('**/demo/catalog-v1.json?**',async route=>{requested=true;await wait;await route.continue();});
|
||||||
|
await setting(page).check();await expect.poll(()=>requested).toBe(true);
|
||||||
|
await page.evaluate(()=>{window.dispatchEvent(new Event('sun:cloud-tenant-changing'));SunCloudV2.getWorkspace=()=>({id:'new-company'});window.dispatchEvent(new Event('sun:cloud-state-applied'));});release();
|
||||||
|
await expect(setting(page)).not.toBeChecked();await page.waitForTimeout(200);expect(await state(page)).toEqual(before);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterEach(async({page},info)=>{if(info.status!==info.expectedStatus){const events=await page.evaluate(()=>window.ctGuideEvents||[]).catch(()=>[]);await info.attach('training-guide-events',{body:JSON.stringify(events, null, 2),contentType:'application/json'});console.log('GUIDE EVENTS',JSON.stringify(events));}});
|
||||||
@ -23,7 +23,7 @@ async function ready(page){
|
|||||||
await page.evaluate(d=>{
|
await page.evaluate(d=>{
|
||||||
window.SunCloudV2={getSession:()=>({user:{id:'test'}}),getWorkspace:()=>({id:'test',role:'admin',permissions:{}}),getClient:()=>({rpc:async(name)=>({data:name==='sun_subscription_snapshot'?{access_mode:'full',status:'trialing',plan_name:'Полный',features:Object.fromEntries(['orders','shopping','stock','catalog_view','catalog_edit','suppliers','settings','clients','calendar','print'].map(k=>[k,true]))}:name==='caterium_trial_demo_status'?{eligible:true,installed:true,empty:false,canInstall:false}:null})}),status:()=>({connected:true,signedIn:true,membershipsLoaded:true}),hasPermission:()=>true};
|
window.SunCloudV2={getSession:()=>({user:{id:'test'}}),getWorkspace:()=>({id:'test',role:'admin',permissions:{}}),getClient:()=>({rpc:async(name)=>({data:name==='sun_subscription_snapshot'?{access_mode:'full',status:'trialing',plan_name:'Полный',features:Object.fromEntries(['orders','shopping','stock','catalog_view','catalog_edit','suppliers','settings','clients','calendar','print'].map(k=>[k,true]))}:name==='caterium_trial_demo_status'?{eligible:true,installed:true,empty:false,canInstall:false}:null})}),status:()=>({connected:true,signedIn:true,membershipsLoaded:true}),hasPermission:()=>true};
|
||||||
// Hydrate AFTER stock UI boot: the real cloud path must update its closure.
|
// Hydrate AFTER stock UI boot: the real cloud path must update its closure.
|
||||||
for(const [key,value] of Object.entries({sunBoxes:d.boxes,sunStock:d.stock,sunSuppliers:d.suppliers,sunStockMoves:[],sunOrders:[],sunTrialDemoV1:{version:1,batch:d.batch,scenario:d.scenario}}))localStorage.setItem(key,JSON.stringify(value));
|
for(const [key,value] of Object.entries({sunBoxes:d.boxes,sunStock:d.stock,sunSuppliers:d.suppliers,sunStockMoves:[],sunOrders:[],sunTrialDemoV1:{version:1,batch:d.batch,scenario:d.scenario,trainingCatalog:{workspaceId:'test',profiles:{test:true}}}}))localStorage.setItem(key,JSON.stringify(value));
|
||||||
boxes=structuredClone(d.boxes);orders=[];window.dispatchEvent(new Event('sun:cloud-state-applied'));window.dispatchEvent(new Event('sun:cloud-permissions-changed'));window.render();window.CateriumTrialDemo.render();
|
boxes=structuredClone(d.boxes);orders=[];window.dispatchEvent(new Event('sun:cloud-state-applied'));window.dispatchEvent(new Event('sun:cloud-permissions-changed'));window.render();window.CateriumTrialDemo.render();
|
||||||
},completeDemo);
|
},completeDemo);
|
||||||
await expect(page.locator('#ctTrialDemo')).toBeVisible();
|
await expect(page.locator('#ctTrialDemo')).toBeVisible();
|
||||||
|
|||||||
@ -194,7 +194,9 @@ test('full app stays stable across settings, menu, calendar and delayed backgrou
|
|||||||
await page.goto('/index.html',{waitUntil:'domcontentloaded'});
|
await page.goto('/index.html',{waitUntil:'domcontentloaded'});
|
||||||
await expect(page.locator('body > header')).toBeVisible({timeout:20000});
|
await expect(page.locator('body > header')).toBeVisible({timeout:20000});
|
||||||
await page.waitForFunction(()=>window.SunOpsUXV1762&&window.SunSaaSV16?.getSnapshot());
|
await page.waitForFunction(()=>window.SunOpsUXV1762&&window.SunSaaSV16?.getSnapshot());
|
||||||
await expect.poll(()=>servedRpcs.has('caterium_trial_demo_status')).toBe(true);
|
await page.waitForFunction(()=>window.CateriumTrialDemo&&window.CateriumTrainingCatalog);
|
||||||
|
expect(await page.evaluate(()=>CateriumTrainingCatalog.enabled())).toBe(false);
|
||||||
|
expect(servedRpcs.has('caterium_trial_demo_status')).toBe(false);
|
||||||
const nav=page.locator('header nav');
|
const nav=page.locator('header nav');
|
||||||
for(const name of ['Настройки','Меню','Календарь','Заказы','Настройки']){
|
for(const name of ['Настройки','Меню','Календарь','Заказы','Настройки']){
|
||||||
await nav.getByRole('button',{name,exact:true}).click();
|
await nav.getByRole('button',{name,exact:true}).click();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user