feat: replace the large training banner with a one-line notice

The big "Учебный каталог включён" card with its buttons sat on top of the
order screen. While training mode is on, the order screen now shows only
"Включён учебный режим · отключить в настройках"; the link opens Settings
at the training switch. The guide (trial order, stock, purchasing, TTK)
moves into the "Обучение и знакомство" settings card, so nothing is lost.
The invitation card for companies with an empty catalog is unchanged.

Specs updated for the new placement, plus a check that the notice link
lands on (and focuses) the switch.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
pavlov346346-source 2026-09-21 17:36:00 +03:00
parent 2e6c0e3939
commit 32d6eb86c8
5 changed files with 45 additions and 10 deletions

View File

@ -128,6 +128,8 @@
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(()=>{});});
// The trial guide (order, stock, TTK) is mounted inside this card while training is on.
queueMicrotask(()=>window.CateriumTrialDemo?.render?.());
}
const check=$('ctTrainingEnabled');check.checked=busy?desired:enabled();check.disabled=busy||!canManage()||!repo()?.catalog;
card.setAttribute('aria-busy',String(busy));

View File

@ -18,16 +18,40 @@
function message(text){const el=$('ctDemoMessage');if(el)el.textContent=text;}
async function refreshStatus(){status=null;statusWorkspace='';render();}
function openTrainingSettings(){
const nav=[...document.querySelectorAll('header nav button')].find(b=>String(b.dataset.navLabel||b.textContent||'').trim()==='Настройки');
if(nav)nav.click();else window.show?.('enterprise-settings');
setTimeout(()=>{
const card=$('ctTrainingSettings');if(!card)return;
const tab=card.dataset.settingsTab;if(tab)document.querySelector(`[data-settings-tab-target="${tab}"]`)?.click();
setTimeout(()=>{card.scrollIntoView({block:'start',behavior:'smooth'});card.querySelector('input')?.focus({preventScroll:true});},120);
},150);
}
function ensureNotice(host){
let notice=$('ctTrainingNotice');
if(!notice){
notice=document.createElement('div');notice.id='ctTrainingNotice';notice.className='ct-training-notice';notice.setAttribute('role','status');
notice.innerHTML='<span>Включён учебный режим ·</span><button type="button" data-training-settings>отключить в настройках</button>';
notice.querySelector('button').addEventListener('click',openTrainingSettings);
}
if(host.firstElementChild!==notice)host.prepend(notice);
}
function render(){
const host=$('new');if(!host)return;
if(window.CateriumTrainingCatalog&&!window.CateriumTrainingCatalog.enabled()){$('ctTrialDemo')?.remove();return;}
const clear=()=>{$('ctTrialDemo')?.remove();$('ctTrainingNotice')?.remove();};
if(window.CateriumTrainingCatalog&&!window.CateriumTrainingCatalog.enabled()){clear();return;}
const demoItems=catalog().filter(b=>b.demo),items=demoItems.filter(b=>b.ttk),active=installed()&&demoItems.length>0;
if(!signedIn()||cloud()?.isSupportMode?.()||(!active&&!status?.canInstall)){$('ctTrialDemo')?.remove();return;}
let card=$('ctTrialDemo');if(!card){card=document.createElement('section');card.id='ctTrialDemo';card.className='card';host.prepend(card);}
if(!signedIn()||cloud()?.isSupportMode?.()||(!active&&!status?.canInstall)){clear();return;}
// While training is on, the order screen only carries a one-line notice; the guide lives in Settings.
if(active)ensureNotice(host);else $('ctTrainingNotice')?.remove();
const target=active?$('ctTrainingSettings'):host;
let card=$('ctTrialDemo');if(card&&card.parentElement!==target){card.remove();card=null;}
if(!target)return;
if(!card){card=document.createElement('section');card.id='ctTrialDemo';card.className=active?'ct-demo-guide':'card';if(active)target.append(card);else target.prepend(card);}
const signature=JSON.stringify({active,items:demoItems.map(x=>[x.id,x.name]),upgrade:status?.canUpgrade,can:writable()});
if(card.dataset.signature===signature)return;card.dataset.signature=signature;
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>
card.innerHTML=active?`<div class="ct-demo-heading"><div><strong>Пробный заказ, склад и закупки</strong><p>${demoItems.length} тестовых позиций · готовые составы, учебные цены и поставщики</p></div></div>
<p>Создайте пробный заказ, посмотрите нехватку в закупках, оформите приход на складе и спишите продукты по заказу. Все данные можно редактировать.</p>
<div class="ct-demo-actions"><button class="primary" data-demo-order ${writable()?'':'disabled'}>Создать пробный заказ</button><button class="outline" data-demo-go="stock">Склад</button><button class="outline" data-demo-go="shopping">Закупки</button></div>
${status?.canUpgrade?'<button class="outline" data-demo-install>Дополнить пустые вкладки демо</button>':''}
@ -76,9 +100,9 @@
dialog.querySelector('[data-ttk-edit]').onclick=()=>{dialog.close();window.editBox?.(box.id)};
dialog.addEventListener('close',()=>dialog.remove(),{once:true});dialog.showModal();
}
function reset(){epoch++;pending=null;status=null;statusWorkspace='';$('ctTrialDemo')?.remove();$('ctDemoTtk')?.remove();}
function reset(){epoch++;pending=null;status=null;statusWorkspace='';$('ctTrialDemo')?.remove();$('ctTrainingNotice')?.remove();$('ctDemoTtk')?.remove();}
function boot(){
const style=document.createElement('style');style.textContent=`#ctTrialDemo{margin:0 0 18px;padding:18px 22px}#ctTrialDemo p{font-size:13px;line-height:1.5;margin:6px 0 12px;max-width:900px}.ct-demo-heading{display:flex;justify-content:space-between;gap:14px;align-items:flex-start}.ct-demo-heading strong{font-size:18px}.ct-demo-heading h2{font-size:22px;margin:8px 0}.ct-demo-badge{border:1px solid #c3d4bd;border-radius:18px;padding:5px 10px;font-size:12px;white-space:nowrap}.ct-demo-actions{display:flex;gap:10px;align-items:end;flex-wrap:wrap;margin:12px 0}.ct-demo-actions label{min-width:0;max-width:100%;flex:1}.ct-demo-actions select{width:100%;font:inherit;padding:9px}.ct-demo-actions button{max-width:100%}#ctTrialDemo details{font-size:13px;margin-top:14px}#ctTrialDemo summary{cursor:pointer}#ctDemoMessage:empty{display:none}#ctDemoTtk{width:min(860px,calc(100vw - 24px));max-height:90dvh;box-sizing:border-box;overflow:auto;border:1px solid #d4c8b0;border-radius:18px;padding:24px;background:var(--card,#fff);color:var(--text,#27241e)}#ctDemoTtk::backdrop{background:#0008}.ct-demo-table{overflow-x:auto}.ct-demo-table table{border-collapse:collapse;min-width:580px;width:100%;font-size:13px}.ct-demo-table td,.ct-demo-table th{text-align:right;padding:9px 7px;border-bottom:1px solid #ddd}.ct-demo-table th:first-child,.ct-demo-table td:first-child{text-align:left}#ctDemoTtk li{margin:8px 0;line-height:1.5}.ct-demo-notice{background:#fff3d6;color:#604918;padding:12px;border-radius:8px}@media(max-width:600px){#ctTrialDemo{padding:14px}.ct-demo-heading{flex-wrap:wrap}.ct-demo-actions>button{flex:1 1 auto}.ct-demo-actions label{flex-basis:100%}#ctDemoTtk{padding:16px}}@media print{#ctTrialDemo{display:none!important}}`;
const style=document.createElement('style');style.textContent=`#ctTrialDemo{margin:0 0 18px;padding:18px 22px}#ctTrialDemo p{font-size:13px;line-height:1.5;margin:6px 0 12px;max-width:900px}.ct-demo-heading{display:flex;justify-content:space-between;gap:14px;align-items:flex-start}.ct-demo-heading strong{font-size:18px}.ct-demo-heading h2{font-size:22px;margin:8px 0}.ct-demo-badge{border:1px solid #c3d4bd;border-radius:18px;padding:5px 10px;font-size:12px;white-space:nowrap}.ct-demo-actions{display:flex;gap:10px;align-items:end;flex-wrap:wrap;margin:12px 0}.ct-demo-actions label{min-width:0;max-width:100%;flex:1}.ct-demo-actions select{width:100%;font:inherit;padding:9px}.ct-demo-actions button{max-width:100%}#ctTrialDemo details{font-size:13px;margin-top:14px}#ctTrialDemo summary{cursor:pointer}#ctDemoMessage:empty{display:none}#ctDemoTtk{width:min(860px,calc(100vw - 24px));max-height:90dvh;box-sizing:border-box;overflow:auto;border:1px solid #d4c8b0;border-radius:18px;padding:24px;background:var(--card,#fff);color:var(--text,#27241e)}#ctDemoTtk::backdrop{background:#0008}.ct-demo-table{overflow-x:auto}.ct-demo-table table{border-collapse:collapse;min-width:580px;width:100%;font-size:13px}.ct-demo-table td,.ct-demo-table th{text-align:right;padding:9px 7px;border-bottom:1px solid #ddd}.ct-demo-table th:first-child,.ct-demo-table td:first-child{text-align:left}#ctDemoTtk li{margin:8px 0;line-height:1.5}.ct-demo-notice{background:#fff3d6;color:#604918;padding:12px;border-radius:8px}@media(max-width:600px){#ctTrialDemo{padding:14px}.ct-demo-heading{flex-wrap:wrap}.ct-demo-actions>button{flex:1 1 auto}.ct-demo-actions label{flex-basis:100%}#ctDemoTtk{padding:16px}}#ctTrainingNotice{display:flex;align-items:center;flex-wrap:wrap;gap:4px 6px;margin:0 0 12px;padding:7px 12px;border:1px solid #e6dcc0;border-radius:10px;background:#faf6e9;color:#5f4f25;font-size:12px;line-height:1.4}#ctTrainingNotice button{all:unset;box-sizing:border-box;cursor:pointer;color:#7a5c12;text-decoration:underline;font:inherit}#ctTrainingNotice button:focus-visible{outline:2px solid #b88c2e;outline-offset:2px;border-radius:3px}#ctTrainingSettings #ctTrialDemo{margin:16px 0 0;padding:16px 0 0;border:0;border-top:1px solid var(--sun-ui-border,#deded6);border-radius:0;box-shadow:none;background:none}#ctTrainingSettings #ctTrialDemo .ct-demo-heading strong{font-size:15px}@media print{#ctTrialDemo,#ctTrainingNotice{display:none!important}}`;
document.head.append(style);
window.addEventListener('sun:cloud-tenant-changing',reset);
window.addEventListener('sun:cloud-permissions-changed',()=>{render();refreshStatus()});

File diff suppressed because one or more lines are too long

View File

@ -44,7 +44,11 @@ test('training is opt-in in an existing paid profile, adds complete examples, hi
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 expect(page.locator('#ctTrialDemo')).toBeVisible();
// The order screen only shows a one-line notice; its link leads back to the switch and the guide.
await nav(page).getByRole('button',{name:'Новый заказ',exact:true}).click();
await expect(page.locator('#ctTrainingNotice')).toContainText('Включён учебный режим');await expect(page.locator('#ctTrialDemo')).toBeHidden();
await page.locator('#ctTrainingNotice button').click();await expect(page.locator('#ctTrialDemo')).toBeVisible();await expect(setting(page)).toBeFocused();
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 ₽');

View File

@ -26,10 +26,15 @@ async function ready(page){
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();
},completeDemo);
await expect(page.locator('#ctTrainingNotice')).toBeVisible();
}
// The trial guide (order, stock, TTK) lives in Settings; the order screen only shows a notice.
async function guide(page){
await page.locator('header nav').getByRole('button',{name:'Настройки',exact:true}).click();
await expect(page.locator('#ctTrialDemo')).toBeVisible();
}
test('demo TTK, order, shortages, receipt and one-time write-off work in the full app',async({page})=>{
await ready(page);
await ready(page);await guide(page);
await page.locator('#ctTrialDemo summary').click();await page.locator('[data-demo-ttk]').click();
await expect(page.locator('#ctDemoTtk')).toBeVisible();await expect(page.locator('#ctDemoTtk')).toContainText('840 г');await expect(page.locator('#ctDemoTtk')).toContainText('Брутто');
expect(await page.locator('#ctDemoTtk').evaluate(el=>el.getBoundingClientRect().width<=innerWidth)).toBe(true);