Compact clients and explicit timed menu promotions (#31)
Add compact accessible client summaries and menu discounts in percent or rubles with explicit durations. Preserve ordinary catalog prices and order line snapshots; derive current prices and expire promotions automatically without background writes. Keep fractional prices, legacy sale compatibility, and regression coverage. Full pull-request QA passed. Publication remains gated by full main QA and byte-for-byte production asset and UI verification.
This commit is contained in:
parent
b6e4a1aaad
commit
424bf8ce87
9
.github/workflows/deploy-timeweb.yml
vendored
9
.github/workflows/deploy-timeweb.yml
vendored
@ -47,6 +47,13 @@ jobs:
|
|||||||
core/login-signature-v1776.css
|
core/login-signature-v1776.css
|
||||||
core/help-center.js
|
core/help-center.js
|
||||||
core/help-center.css
|
core/help-center.css
|
||||||
|
core/catalog-pricing.js
|
||||||
|
core/client-menu.css
|
||||||
|
core/ops-ux-v1762.js
|
||||||
|
core/banquet-menu.js
|
||||||
|
core/data-layer-v1773.js
|
||||||
|
core/trial-demo.js
|
||||||
|
legacy/bootstrap.js
|
||||||
caterium-mark-light.svg
|
caterium-mark-light.svg
|
||||||
service-worker.js
|
service-worker.js
|
||||||
)
|
)
|
||||||
@ -102,7 +109,7 @@ jobs:
|
|||||||
node-version: 22
|
node-version: 22
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- run: npx playwright install --with-deps chromium
|
- run: npx playwright install --with-deps chromium
|
||||||
- name: Check the published loading screen and Help icon
|
- name: Check the published loading screen, Help, clients and promotions
|
||||||
timeout-minutes: 4
|
timeout-minutes: 4
|
||||||
run: node tests/production-ui-smoke.mjs
|
run: node tests/production-ui-smoke.mjs
|
||||||
- name: Save production UI verification
|
- name: Save production UI verification
|
||||||
|
|||||||
30
docs/releases/2026-09-19-CLIENTS-PROMOTIONS.md
Normal file
30
docs/releases/2026-09-19-CLIENTS-PROMOTIONS.md
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# Compact clients and scheduled menu promotions
|
||||||
|
|
||||||
|
Client summaries are compact, whole-card keyboard-accessible buttons. Name,
|
||||||
|
phone, order count, turnover and loyalty discount remain visible; full addresses,
|
||||||
|
orders and loyalty settings remain in the existing detail dialog. Search is retained.
|
||||||
|
|
||||||
|
Menu editing now uses the ordinary price plus an explicit percentage or ruble
|
||||||
|
discount, not a manually entered previous price. Duration presets (1, 7, 14, 30
|
||||||
|
days), an exact local date/time, and unlimited duration are supported. Existing
|
||||||
|
finite promotions keep their original deadline when an unrelated field is edited.
|
||||||
|
The editor validates values and previews the effective price and return deadline.
|
||||||
|
|
||||||
|
`price` stores the ordinary price. `promotion` stores type, value, startsAt and
|
||||||
|
endsAt (UTC instants). A shared pure price resolver calculates the effective price;
|
||||||
|
at the deadline it returns the base price and no promotion badge. No background
|
||||||
|
server write, open tab or cron is required. An open tab schedules the next boundary
|
||||||
|
and rechecks after focus, visibility and cloud refresh. Saved order line price
|
||||||
|
snapshots are not recalculated. The expiry event never writes orders or catalog.
|
||||||
|
The client device clock is used, just like the existing app scheduling.
|
||||||
|
|
||||||
|
Legacy price/oldPrice records retain their existing effective price and convert
|
||||||
|
only when explicitly saved. JSON and spreadsheet catalog round-trips keep promotion
|
||||||
|
metadata. Full JSON cloud payloads already preserve these fields; no migration,
|
||||||
|
new permissions, database mutation or cross-project change is required.
|
||||||
|
|
||||||
|
Regression coverage: compact client geometry/search/details; percentage and ruble
|
||||||
|
validation; legacy conversion; fractional prices; exact deadline and reload;
|
||||||
|
unchanged saved orders; foreground expiry without losing an unsaved editor. The
|
||||||
|
production smoke uses fetched public assets with a synthetic, network-blocked
|
||||||
|
workspace; no customer account or database is used for this verification.
|
||||||
@ -4,7 +4,7 @@
|
|||||||
"version": "17.7.3",
|
"version": "17.7.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"check:syntax": "node --check public/app-runtime.js && node --check public/service-worker.js && node --check public/legacy/bootstrap.js && node --check public/core/sun-safe.js && node --check public/core/account-center-v1780.js && node --check public/core/performance.js && node --check public/core/auth-security-v1774.js && node --check public/core/trial-promo-developer-v181.js && node --check public/core/order-enhancements-v1775.js && node --check public/core/data-layer-v1773.js && node --check public/core/server-automation-v1770.js && node --check public/core/hotfix-v1763.js && node --check public/core/ops-ux-v1762.js && node --check public/core/ux-fixes-v1764.js && node --check public/core/pdf-engine.js && node --check public/core/classic-offer-pdf-v1767.js && node --check public/core/signature-offer-pdf-v18.js && node --check public/core/developer-console-v1768.js && node --check public/core/offer-workspace-v1769.js && node --check public/core/brand-theme.js && node --check public/core/company-branding.js && node --check public/core/import-archive.js && node --check public/core/access-policy.js && node --check public/core/banquet-menu.js && node --check public/core/cloud-transport.js && node --check public/core/trial-demo.js && node --check public/core/proposal-layout.js && node --check public/core/mobile-order.js && node --check public/core/help-center.js",
|
"check:syntax": "node --check public/core/catalog-pricing.js && node --check public/app-runtime.js && node --check public/service-worker.js && node --check public/legacy/bootstrap.js && node --check public/core/sun-safe.js && node --check public/core/account-center-v1780.js && node --check public/core/performance.js && node --check public/core/auth-security-v1774.js && node --check public/core/trial-promo-developer-v181.js && node --check public/core/order-enhancements-v1775.js && node --check public/core/data-layer-v1773.js && node --check public/core/server-automation-v1770.js && node --check public/core/hotfix-v1763.js && node --check public/core/ops-ux-v1762.js && node --check public/core/ux-fixes-v1764.js && node --check public/core/pdf-engine.js && node --check public/core/classic-offer-pdf-v1767.js && node --check public/core/signature-offer-pdf-v18.js && node --check public/core/developer-console-v1768.js && node --check public/core/offer-workspace-v1769.js && node --check public/core/brand-theme.js && node --check public/core/company-branding.js && node --check public/core/import-archive.js && node --check public/core/access-policy.js && node --check public/core/banquet-menu.js && node --check public/core/cloud-transport.js && node --check public/core/trial-demo.js && node --check public/core/proposal-layout.js && node --check public/core/mobile-order.js && node --check public/core/help-center.js",
|
||||||
"test:static": "node tests/static-security.mjs && node tests/auth-security-v1774.mjs && node tests/employee-create-v1774.mjs && node tests/html-integrity-v1774.mjs && node tests/edge-security-v1774.mjs && node tests/branding-v1774.mjs && node tests/order-enhancements-v1775.mjs",
|
"test:static": "node tests/static-security.mjs && node tests/auth-security-v1774.mjs && node tests/employee-create-v1774.mjs && node tests/html-integrity-v1774.mjs && node tests/edge-security-v1774.mjs && node tests/branding-v1774.mjs && node tests/order-enhancements-v1775.mjs",
|
||||||
"check:release": "node tests/release-check.mjs",
|
"check:release": "node tests/release-check.mjs",
|
||||||
"check:deploy": "npm run check:syntax && npm run test:static && npm run check:release && node tests/backend-cutover.mjs && npm run test:db",
|
"check:deploy": "npm run check:syntax && npm run test:static && npm run check:release && node tests/backend-cutover.mjs && npm run test:db",
|
||||||
|
|||||||
@ -113,7 +113,7 @@ window.SUN_LEGACY_CATALOG_V175=[];
|
|||||||
const titleY=717;
|
const titleY=717;
|
||||||
ctx.fillStyle='#1f1f1d';ctx.font='800 36px Arial, sans-serif';
|
ctx.fillStyle='#1f1f1d';ctx.font='800 36px Arial, sans-serif';
|
||||||
const nameLines=wrapText(ctx,item.name||'',650,2);drawTextLines(ctx,nameLines,85,titleY,41);
|
const nameLines=wrapText(ctx,item.name||'',650,2);drawTextLines(ctx,nameLines,85,titleY,41);
|
||||||
ctx.textAlign='right';ctx.fillStyle='#c99a32';ctx.font='800 37px Arial, sans-serif';ctx.fillText(money(item.price||0),965,titleY);ctx.textAlign='left';
|
ctx.textAlign='right';ctx.fillStyle='#c99a32';ctx.font='800 37px Arial, sans-serif';ctx.fillText(money(window.CateriumPricing.price(item)),965,titleY);ctx.textAlign='left';
|
||||||
|
|
||||||
const factsY=820;
|
const factsY=820;
|
||||||
ctx.fillStyle='#77736d';ctx.font='800 14px Arial, sans-serif';ctx.fillText('ВЕС',85,factsY);ctx.fillText('КОЛ-ВО',525,factsY);
|
ctx.fillStyle='#77736d';ctx.font='800 14px Arial, sans-serif';ctx.fillText('ВЕС',85,factsY);ctx.fillText('КОЛ-ВО',525,factsY);
|
||||||
@ -217,7 +217,7 @@ window.SUN_LEGACY_CATALOG_V175=[];
|
|||||||
const $=id=>document.getElementById(id);
|
const $=id=>document.getElementById(id);
|
||||||
const qa=(sel,root=document)=>[...root.querySelectorAll(sel)];
|
const qa=(sel,root=document)=>[...root.querySelectorAll(sel)];
|
||||||
const esc=window.SunSafe.escapeHTML;
|
const esc=window.SunSafe.escapeHTML;
|
||||||
const money=v=>`${Math.round(Number(v||0)).toLocaleString('ru-RU')} ₽`;
|
const money=v=>`${Number(v||0).toLocaleString('ru-RU',{maximumFractionDigits:2})} ₽`;
|
||||||
const toast=(m,t='success')=>{try{return window.SunEnterprise?.toast?.(m,t)}catch(_){}; if(t==='warn'||t==='error')console.warn(m);};
|
const toast=(m,t='success')=>{try{return window.SunEnterprise?.toast?.(m,t)}catch(_){}; if(t==='warn'||t==='error')console.warn(m);};
|
||||||
|
|
||||||
const style=document.createElement('style');
|
const style=document.createElement('style');
|
||||||
@ -384,7 +384,7 @@ window.SUN_LEGACY_CATALOG_V175=[];
|
|||||||
let search=$('sunCatalogSearch');if(!search){search=document.createElement('input');search.id='sunCatalogSearch';search.className='sun-catalog-search';search.type='search';search.placeholder='Поиск по № бокса';search.inputMode='numeric';search.oninput=()=>{catalogQuery=search.value;renderCatalogV5();};configure.insertAdjacentElement('beforebegin',search)}
|
let search=$('sunCatalogSearch');if(!search){search=document.createElement('input');search.id='sunCatalogSearch';search.className='sun-catalog-search';search.type='search';search.placeholder='Поиск по № бокса';search.inputMode='numeric';search.oninput=()=>{catalogQuery=search.value;renderCatalogV5();};configure.insertAdjacentElement('beforebegin',search)}
|
||||||
let tabs=$('sunCatalogTabsButton');if(!tabs){tabs=document.createElement('button');tabs.id='sunCatalogTabsButton';tabs.className='outline sun-catalog-tabs-button';tabs.type='button';tabs.textContent='Вкладки';tabs.onclick=window.sunOpenCategoryManager;configure.insertAdjacentElement('beforebegin',tabs)}
|
let tabs=$('sunCatalogTabsButton');if(!tabs){tabs=document.createElement('button');tabs.id='sunCatalogTabsButton';tabs.className='outline sun-catalog-tabs-button';tabs.type='button';tabs.textContent='Вкладки';tabs.onclick=window.sunOpenCategoryManager;configure.insertAdjacentElement('beforebegin',tabs)}
|
||||||
}
|
}
|
||||||
function lineUnitPrice(line){const direct=Number(line?.price);if(Number.isFinite(direct)&&direct>=0)return direct;const item=boxes.find(x=>String(x.id)===String(line?.id));return Math.max(0,Number(item?.price||0));}
|
function lineUnitPrice(line){const item=boxes.find(x=>String(x.id)===String(line?.id));return window.CateriumPricing.linePrice(line,item);}
|
||||||
function baseTotal(order){return (order?.lines||[]).reduce((s,l)=>s+lineUnitPrice(l)*Math.max(0,Number(l.qty||0)),0)}
|
function baseTotal(order){return (order?.lines||[]).reduce((s,l)=>s+lineUnitPrice(l)*Math.max(0,Number(l.qty||0)),0)}
|
||||||
window.sunBaseOrderTotal=baseTotal;
|
window.sunBaseOrderTotal=baseTotal;
|
||||||
function parseWeightGrams(value){const raw=String(value??'').trim().toLowerCase().replace(',','.');if(!raw)return 0;const m=raw.match(/(\d+(?:\.\d+)?)/);if(!m)return 0;let n=Math.max(0,Number(m[1]||0));if(/кг/.test(raw))n*=1000;return Math.round(n)}
|
function parseWeightGrams(value){const raw=String(value??'').trim().toLowerCase().replace(',','.');if(!raw)return 0;const m=raw.match(/(\d+(?:\.\d+)?)/);if(!m)return 0;let n=Math.max(0,Number(m[1]||0));if(/кг/.test(raw))n*=1000;return Math.round(n)}
|
||||||
@ -420,7 +420,7 @@ window.SUN_LEGACY_CATALOG_V175=[];
|
|||||||
function syncBanquetEditor(category,item){
|
function syncBanquetEditor(category,item){
|
||||||
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=>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)||''}
|
||||||
}
|
}
|
||||||
@ -429,10 +429,10 @@ window.SUN_LEGACY_CATALOG_V175=[];
|
|||||||
function renderOrderLines(){
|
function renderOrderLines(){
|
||||||
if(!$('lines'))return;
|
if(!$('lines'))return;
|
||||||
if(!(draft.lines||[]).length){$('lines').innerHTML='<p class="empty">Выберите позиции из каталога.</p>';return}
|
if(!(draft.lines||[]).length){$('lines').innerHTML='<p class="empty">Выберите позиции из каталога.</p>';return}
|
||||||
const rows=draft.lines.map((line,index)=>{const item=boxes.find(x=>String(x.id)===String(line.id));if(!item)return'';const qty=Math.max(1,Number(line.qty||1)),price=lineUnitPrice(line),isBox=[0,5,6].includes(Number(item.category||0));return `<div class="sun-order-line-row" data-line-index="${index}"><div class="sun-order-line-name" title="${esc(item.name)}">${esc(String(item.name||'').replace(/^(?:Фуршетный\s+)?бокс\s*№\s*(\d+)\s*(?:[—–-]\s*)?/i,'№ $1 '))}${isBox&&(item.weight||inferBoxPieces(item))?`<small>${item.weight?`<b>Вес:</b> ${esc(item.weight)}`:''}${item.weight&&inferBoxPieces(item)?' · ':''}${inferBoxPieces(item)?`<b>Количество:</b> ${inferBoxPieces(item)} шт.`:''}</small>`:''}</div><div class="sun-order-line-qty"><button type="button" onclick="qty('${esc(line.id)}',${qty-1})">−</button><input type="number" min="1" value="${qty}" onchange="qty('${esc(line.id)}',+this.value)"><button type="button" onclick="qty('${esc(line.id)}',${qty+1})">+</button></div><div class="sun-order-line-price"><input type="text" inputmode="numeric" pattern="[0-9]*" autocomplete="off" value="${Math.round(price)}" onchange="sunOrderLinePriceChanged(${index},this)"></div><div class="sun-order-line-sum"><b>${money(price*qty)}</b></div></div>`}).join('');
|
const rows=draft.lines.map((line,index)=>{const item=boxes.find(x=>String(x.id)===String(line.id));if(!item)return'';const qty=Math.max(1,Number(line.qty||1)),price=lineUnitPrice(line),isBox=[0,5,6].includes(Number(item.category||0));return `<div class="sun-order-line-row" data-line-index="${index}"><div class="sun-order-line-name" title="${esc(item.name)}">${esc(String(item.name||'').replace(/^(?:Фуршетный\s+)?бокс\s*№\s*(\d+)\s*(?:[—–-]\s*)?/i,'№ $1 '))}${isBox&&(item.weight||inferBoxPieces(item))?`<small>${item.weight?`<b>Вес:</b> ${esc(item.weight)}`:''}${item.weight&&inferBoxPieces(item)?' · ':''}${inferBoxPieces(item)?`<b>Количество:</b> ${inferBoxPieces(item)} шт.`:''}</small>`:''}</div><div class="sun-order-line-qty"><button type="button" onclick="qty('${esc(line.id)}',${qty-1})">−</button><input type="number" min="1" value="${qty}" onchange="qty('${esc(line.id)}',+this.value)"><button type="button" onclick="qty('${esc(line.id)}',${qty+1})">+</button></div><div class="sun-order-line-price"><input type="text" inputmode="decimal" autocomplete="off" value="${price}" onchange="sunOrderLinePriceChanged(${index},this)"></div><div class="sun-order-line-sum"><b>${money(price*qty)}</b></div></div>`}).join('');
|
||||||
$('lines').innerHTML=`<div class="sun-order-lines-table"><div class="sun-order-line-head"><span>Наименование</span><span>Кол.</span><span>Цена</span><span>Стоимость</span></div><div class="sun-order-line-body">${rows}</div><div class="sun-order-line-total"><span>Стоимость позиций</span><b>${money(baseTotal(draft))}</b></div></div>`;
|
$('lines').innerHTML=`<div class="sun-order-lines-table"><div class="sun-order-line-head"><span>Наименование</span><span>Кол.</span><span>Цена</span><span>Стоимость</span></div><div class="sun-order-line-body">${rows}</div><div class="sun-order-line-total"><span>Стоимость позиций</span><b>${money(baseTotal(draft))}</b></div></div>`;
|
||||||
}
|
}
|
||||||
window.sunOrderLinePriceChanged=(index,input)=>{const line=draft.lines?.[index];if(!line)return;line.price=Math.max(0,Math.round(Number(input.value||0)));const row=input.closest('.sun-order-line-row'),sum=row?.querySelector('.sun-order-line-sum b');if(sum)sum.textContent=money(line.price*Math.max(0,Number(line.qty||0)));const total=$('lines')?.querySelector('.sun-order-line-total b');if(total)total.textContent=money(baseTotal(draft));updateOrderSummary();};
|
window.sunOrderLinePriceChanged=(index,input)=>{const line=draft.lines?.[index];if(!line)return;const value=Number(String(input.value||0).replace(',','.'));if(!Number.isFinite(value)){input.value=String(lineUnitPrice(line));return;}line.price=Math.max(0,Math.round(value*100)/100);const row=input.closest('.sun-order-line-row'),sum=row?.querySelector('.sun-order-line-sum b');if(sum)sum.textContent=money(line.price*Math.max(0,Number(line.qty||0)));const total=$('lines')?.querySelector('.sun-order-line-total b');if(total)total.textContent=money(baseTotal(draft));updateOrderSummary();};
|
||||||
function boxNumber(item){const explicit=Number(item?.boxNumber);if(Number.isInteger(explicit)&&explicit>=0)return explicit;const name=String(item?.name||'');const match=name.match(/(?:бокс\s*)?№\s*0*(\d+)/i)||name.match(/^\s*№\s*0*(\d+)/i);return match?Number(match[1]):null}
|
function boxNumber(item){const explicit=Number(item?.boxNumber);if(Number.isInteger(explicit)&&explicit>=0)return explicit;const name=String(item?.name||'');const match=name.match(/(?:бокс\s*)?№\s*0*(\d+)/i)||name.match(/^\s*№\s*0*(\d+)/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 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)}
|
||||||
@ -440,19 +440,24 @@ window.SUN_LEGACY_CATALOG_V175=[];
|
|||||||
function banquetCatalog(){return boxes.filter(item=>item.hidden!==true&&Number(item.category)===BANQUET_CATEGORY)}
|
function banquetCatalog(){return boxes.filter(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(){
|
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=>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});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(item.price||0)}</small>${Number(item.oldPrice||0)>Number(item.price||0)?`<small class="old-price">${money(item.oldPrice)}</small>`:''}</span>${Number(item.oldPrice||0)>Number(item.price||0)?`<span class="sale-badge">АКЦИЯ</span>`:''}</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)}</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}`;
|
||||||
renderOrderLines();updateOrderSummary();
|
if(!catalogOnly){renderOrderLines();updateOrderSummary();}
|
||||||
}
|
}
|
||||||
|
window.addEventListener('caterium:catalog-prices-changed',()=>{
|
||||||
|
if($('new')?.classList.contains('on'))renderCatalogV5({catalogOnly:true});
|
||||||
|
if($('manager')?.classList.contains('on'))renderManagerV5();
|
||||||
|
});
|
||||||
|
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,Number(item.price||0))})}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.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(){
|
||||||
@ -462,16 +467,16 @@ 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=>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(item.price||0)}</small>${Number(item.oldPrice||0)>Number(item.price||0)?`<small class="old-price">${money(item.oldPrice)}</small>`:''}</span>${(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');};
|
||||||
window.editBox=id=>{
|
window.editBox=id=>{
|
||||||
edited=id?structuredClone(boxes.find(x=>String(x.id)===String(id))):{id:'',name:Number(activeCat)===0?'Новый бокс':(Number(activeCat)===5?'Новая премиум позиция':'Новая позиция'),category:Number(activeCat),price:0,oldPrice:0,ingredients:[]};if(!edited)return;
|
edited=id?structuredClone(boxes.find(x=>String(x.id)===String(id))):{id:'',name:Number(activeCat)===0?'Новый бокс':(Number(activeCat)===5?'Новая премиум позиция':'Новая позиция'),category:Number(activeCat),price:0,oldPrice:0,ingredients:[]};if(!edited)return;
|
||||||
if(!Array.isArray(edited.ingredients))edited.ingredients=[];normalizeBoxItem(edited);syncEditorCategoryOptions();$('itemCategory').value=String(edited.category??activeCat);$('boxName').value=edited.name||'';$('boxPrice').value=Number(edited.price||0);if($('boxOldPrice'))$('boxOldPrice').value=Number(edited.oldPrice||0);if($('boxCompositionText'))$('boxCompositionText').value=(Array.isArray(edited.composition)?edited.composition:[]).join('\n');syncBoxWeightEditor(edited.category??activeCat,edited);syncBanquetEditor(edited.category??activeCat,edited);$('editPhoto').style.display=edited.photo?'block':'none';$('editPhoto').src=edited.photo||'';$('deleteBox').style.display=edited.id?'block':'none';
|
if(!Array.isArray(edited.ingredients))edited.ingredients=[];normalizeBoxItem(edited);syncEditorCategoryOptions();$('itemCategory').value=String(edited.category??activeCat);$('boxName').value=edited.name||'';window.CateriumPricing.loadEditor(edited);if($('boxCompositionText'))$('boxCompositionText').value=(Array.isArray(edited.composition)?edited.composition:[]).join('\n');syncBoxWeightEditor(edited.category??activeCat,edited);syncBanquetEditor(edited.category??activeCat,edited);$('editPhoto').style.display=edited.photo?'block':'none';$('editPhoto').src=edited.photo||'';$('deleteBox').style.display=edited.id?'block':'none';
|
||||||
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();edited.price=Math.max(0,Number($('boxPrice').value||0));edited.oldPrice=Math.max(0,Number($('boxOldPrice')?.value||0));if(!(edited.oldPrice>edited.price))delete edited.oldPrice;edited.sale=Boolean(edited.oldPrice&&edited.oldPrice>edited.price);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)return alert('Введите название.');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.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.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)})}
|
||||||
@ -881,7 +886,7 @@ window.SUN_LEGACY_CATALOG_V175=[];
|
|||||||
const $=id=>document.getElementById(id);
|
const $=id=>document.getElementById(id);
|
||||||
const qa=(sel,root=document)=>[...root.querySelectorAll(sel)];
|
const qa=(sel,root=document)=>[...root.querySelectorAll(sel)];
|
||||||
const esc=window.SunSafe.escapeHTML;
|
const esc=window.SunSafe.escapeHTML;
|
||||||
const money=v=>`${Math.round(Number(v||0)).toLocaleString('ru-RU')} ₽`;
|
const money=v=>`${Number(v||0).toLocaleString('ru-RU',{maximumFractionDigits:2})} ₽`;
|
||||||
const num=v=>Math.max(0,Number(v||0));
|
const num=v=>Math.max(0,Number(v||0));
|
||||||
const toast=(m,t='success')=>{try{return window.SunEnterprise?.toast?.(m,t)}catch(_){} if(t==='error'||t==='warn')console.warn(m);};
|
const toast=(m,t='success')=>{try{return window.SunEnterprise?.toast?.(m,t)}catch(_){} if(t==='error'||t==='warn')console.warn(m);};
|
||||||
// Document logos are supplied by the selected company.
|
// Document logos are supplied by the selected company.
|
||||||
@ -925,7 +930,7 @@ window.SUN_LEGACY_CATALOG_V175=[];
|
|||||||
const getDraft=()=>{try{return typeof draft!=='undefined'?draft:null}catch(_){return null}};
|
const getDraft=()=>{try{return typeof draft!=='undefined'?draft:null}catch(_){return null}};
|
||||||
const catName=id=>{try{return window.sunCatalogCategoryName?.(id)||({0:'Боксы',1:'Посуда и упаковка',2:'Дополнительно',3:'Напитки',4:'Доставка',5:'Премиум'}[Number(id)]||'Каталог')}catch(_){return 'Каталог'}};
|
const catName=id=>{try{return window.sunCatalogCategoryName?.(id)||({0:'Боксы',1:'Посуда и упаковка',2:'Дополнительно',3:'Напитки',4:'Доставка',5:'Премиум'}[Number(id)]||'Каталог')}catch(_){return 'Каталог'}};
|
||||||
|
|
||||||
function linePrice(line,item){const p=Number(line?.price);return Number.isFinite(p)&&p>=0?p:num(item?.price)}
|
function linePrice(line,item){const p=Number(line?.price);return Number.isFinite(p)&&p>=0?p:num(window.CateriumPricing.price(item))}
|
||||||
function discountAmount(value,type,base){const n=num(value);return Math.min(base,type==='amount'?n:base*Math.min(100,n)/100)}
|
function discountAmount(value,type,base){const n=num(value);return Math.min(base,type==='amount'?n:base*Math.min(100,n)/100)}
|
||||||
function priceCalc(order){
|
function priceCalc(order){
|
||||||
const all=getBoxes();
|
const all=getBoxes();
|
||||||
@ -1795,7 +1800,7 @@ window.SUN_LEGACY_CATALOG_V175=[];
|
|||||||
const $=id=>document.getElementById(id);
|
const $=id=>document.getElementById(id);
|
||||||
const qa=(sel,root=document)=>[...root.querySelectorAll(sel)];
|
const qa=(sel,root=document)=>[...root.querySelectorAll(sel)];
|
||||||
const esc=window.SunSafe.escapeHTML;
|
const esc=window.SunSafe.escapeHTML;
|
||||||
const money=v=>`${Math.round(Number(v||0)).toLocaleString('ru-RU')} \u20bd`;
|
const money=v=>`${Number(v||0).toLocaleString('ru-RU',{maximumFractionDigits:2})} \u20bd`;
|
||||||
const txt={
|
const txt={
|
||||||
gen:'\u0421\u0433\u0435\u043d\u0435\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043f\u0440\u0435\u0434\u043b\u043e\u0436\u0435\u043d\u0438\u0435',
|
gen:'\u0421\u0433\u0435\u043d\u0435\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043f\u0440\u0435\u0434\u043b\u043e\u0436\u0435\u043d\u0438\u0435',
|
||||||
title:'\u0421\u0433\u0435\u043d\u0435\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043f\u0440\u0435\u0434\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u043a\u043b\u0438\u0435\u043d\u0442\u0443',
|
title:'\u0421\u0433\u0435\u043d\u0435\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043f\u0440\u0435\u0434\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u043a\u043b\u0438\u0435\u043d\u0442\u0443',
|
||||||
@ -1836,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&&Number(x?.price||0)>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:Number(x.price||0),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&&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){
|
||||||
@ -4101,7 +4106,7 @@ window.SUN_LEGACY_CATALOG_V175=[];
|
|||||||
const DEFAULT_SOURCES=['Заявка с сайта','CaterMe','Avito','Рекомендация','Повторный клиент','Соцсети','Другое'];
|
const DEFAULT_SOURCES=['Заявка с сайта','CaterMe','Avito','Рекомендация','Повторный клиент','Соцсети','Другое'];
|
||||||
const getDraft=()=>{try{return draft}catch(_){return null}};
|
const getDraft=()=>{try{return draft}catch(_){return null}};
|
||||||
const getOrders=()=>{try{return Array.isArray(orders)?orders:[]}catch(_){try{const x=JSON.parse(localStorage.getItem('sunOrders')||'[]');return Array.isArray(x)?x:[]}catch(__){return[]}}};
|
const getOrders=()=>{try{return Array.isArray(orders)?orders:[]}catch(_){try{const x=JSON.parse(localStorage.getItem('sunOrders')||'[]');return Array.isArray(x)?x:[]}catch(__){return[]}}};
|
||||||
const money=v=>typeof window.money==='function'?window.money(v):`${Math.round(Number(v||0)).toLocaleString('ru-RU')} ₽`;
|
const money=v=>typeof window.money==='function'?window.money(v):`${Number(v||0).toLocaleString('ru-RU',{maximumFractionDigits:2})} ₽`;
|
||||||
const toast=(m,t='success')=>{try{if(window.SunEnterprise?.toast)return window.SunEnterprise.toast(m,t)}catch(_){} if(t==='warn'||t==='error')console.warn(m)};
|
const toast=(m,t='success')=>{try{if(window.SunEnterprise?.toast)return window.SunEnterprise.toast(m,t)}catch(_){} if(t==='warn'||t==='error')console.warn(m)};
|
||||||
const style=document.createElement('style');style.id='sun-order-crm-v1-style';style.textContent=`
|
const style=document.createElement('style');style.id='sun-order-crm-v1-style';style.textContent=`
|
||||||
/* The precise-address fields existed in an older module but were hidden by a later polish layer. */
|
/* The precise-address fields existed in an older module but were hidden by a later polish layer. */
|
||||||
|
|||||||
@ -3,13 +3,13 @@
|
|||||||
const groups=[['cold','Холодные закуски'],['salads','Салаты'],['starters','Горячие закуски'],['main','Горячее'],['sides','Гарниры'],['desserts','Десерты'],['fruit','Фрукты и ягоды'],['bread','Хлеб и масло'],['other','Другие блюда']];
|
const groups=[['cold','Холодные закуски'],['salads','Салаты'],['starters','Горячие закуски'],['main','Горячее'],['sides','Гарниры'],['desserts','Десерты'],['fruit','Фрукты и ягоды'],['bread','Хлеб и масло'],['other','Другие блюда']];
|
||||||
let packageId='all',groupId='all';
|
let packageId='all',groupId='all';
|
||||||
const esc=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
const esc=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||||||
const money=v=>Math.round(Number(v)||0).toLocaleString('ru-RU')+' ₽';
|
const money=v=>Number(v||0).toLocaleString('ru-RU',{maximumFractionDigits:2})+' ₽';
|
||||||
const grams=item=>{const value=String(item?.weight||'').replace(',','.');return Math.round((parseFloat(value)||0)*(/кг/i.test(value)?1000:1))};
|
const grams=item=>{const value=String(item?.weight||'').replace(',','.');return Math.round((parseFloat(value)||0)*(/кг/i.test(value)?1000:1))};
|
||||||
const group=item=>{const s=String(item?.catalogSection||'').toLowerCase();return /салат/.test(s)?'salads':/(горяч|тёпл|тепл).*закуск/.test(s)?'starters':/горяч|основн/.test(s)?'main':/гарнир/.test(s)?'sides':/десерт|сладк/.test(s)?'desserts':/фрукт|ягод/.test(s)?'fruit':/хлеб|масло|булоч/.test(s)?'bread':/рыб|мясн|сыр|овощ|холод|закуск/.test(s)?'cold':'other'};
|
const group=item=>{const s=String(item?.catalogSection||'').toLowerCase();return /салат/.test(s)?'salads':/(горяч|тёпл|тепл).*закуск/.test(s)?'starters':/горяч|основн/.test(s)?'main':/гарнир/.test(s)?'sides':/десерт|сладк/.test(s)?'desserts':/фрукт|ягод/.test(s)?'fruit':/хлеб|масло|булоч/.test(s)?'bread':/рыб|мясн|сыр|овощ|холод|закуск/.test(s)?'cold':'other'};
|
||||||
function packages(catalog){const result=new Map();for(const i of catalog){const b=i.banquet;if(b?.packageId&&!result.has(b.packageId))result.set(b.packageId,{id:b.packageId,name:b.packageName||b.packageId,price:b.packagePrice,grams:b.packageGrams})}return [...result.values()]}
|
function packages(catalog){const result=new Map();for(const i of catalog){const b=i.banquet;if(b?.packageId&&!result.has(b.packageId))result.set(b.packageId,{id:b.packageId,name:b.packageName||b.packageId,price:b.packagePrice,grams:b.packageGrams})}return [...result.values()]}
|
||||||
function selection(draft,catalog){if(!Array.isArray(draft.banquetSelection)){const ids=new Set(catalog.map(i=>String(i.id)));draft.banquetSelection=(draft.lines||[]).filter(l=>ids.has(String(l.id))).map(l=>String(l.id))}return new Set(draft.banquetSelection.map(String))}
|
function selection(draft,catalog){if(!Array.isArray(draft.banquetSelection)){const ids=new Set(catalog.map(i=>String(i.id)));draft.banquetSelection=(draft.lines||[]).filter(l=>ids.has(String(l.id))).map(l=>String(l.id))}return new Set(draft.banquetSelection.map(String))}
|
||||||
const guestCount=draft=>Math.max(1,Math.round(Number(draft.guestsCount)||1));
|
const guestCount=draft=>Math.max(1,Math.round(Number(draft.guestsCount)||1));
|
||||||
const unitPrice=(item,draft)=>{const line=(draft.lines||[]).find(l=>String(l.id)===String(item.id));return line&&Number.isFinite(Number(line.price))?Math.max(0,Number(line.price)):Number(item.price)||0};
|
const unitPrice=(item,draft)=>{const line=(draft.lines||[]).find(l=>String(l.id)===String(item.id));return line&&Number.isFinite(Number(line.price))?Math.max(0,Number(line.price)):(window.CateriumPricing?.price(item)??Number(item?.price||0))};
|
||||||
function totals(items,draft){return {price:items.reduce((n,i)=>n+unitPrice(i,draft),0),grams:items.reduce((n,i)=>n+grams(i),0)}}
|
function totals(items,draft){return {price:items.reduce((n,i)=>n+unitPrice(i,draft),0),grams:items.reduce((n,i)=>n+grams(i),0)}}
|
||||||
function chooseMenu(draft,catalog,id){const rows=catalog.filter(i=>i.banquet?.packageId===id),used=new Set();draft.banquetSelection=rows.filter(i=>{const choice=i.banquet?.choiceGroup;if(!choice)return true;if(used.has(choice))return false;const defaults=rows.some(r=>r.banquet?.choiceGroup===choice&&r.banquet.defaultChoice);if(defaults&&!i.banquet.defaultChoice)return false;used.add(choice);return true}).map(i=>String(i.id));packageId=id;groupId='all'}
|
function chooseMenu(draft,catalog,id){const rows=catalog.filter(i=>i.banquet?.packageId===id),used=new Set();draft.banquetSelection=rows.filter(i=>{const choice=i.banquet?.choiceGroup;if(!choice)return true;if(used.has(choice))return false;const defaults=rows.some(r=>r.banquet?.choiceGroup===choice&&r.banquet.defaultChoice);if(defaults&&!i.banquet.defaultChoice)return false;used.add(choice);return true}).map(i=>String(i.id));packageId=id;groupId='all'}
|
||||||
function toggle(draft,catalog,id){const ids=selection(draft,catalog),item=catalog.find(i=>String(i.id)===id);if(!item)return;if(ids.has(id))ids.delete(id);else{const choice=item.banquet?.choiceGroup;if(choice)for(const old of catalog)if(old.banquet?.packageId===item.banquet.packageId&&old.banquet?.choiceGroup===choice)ids.delete(String(old.id));ids.add(id)}draft.banquetSelection=[...ids]}
|
function toggle(draft,catalog,id){const ids=selection(draft,catalog),item=catalog.find(i=>String(i.id)===id);if(!item)return;if(ids.has(id))ids.delete(id);else{const choice=item.banquet?.choiceGroup;if(choice)for(const old of catalog)if(old.banquet?.packageId===item.banquet.packageId&&old.banquet?.choiceGroup===choice)ids.delete(String(old.id));ids.add(id)}draft.banquetSelection=[...ids]}
|
||||||
@ -29,7 +29,7 @@
|
|||||||
${estimates?'<p class="ct-banquet-estimate">≈ Веса и цены отдельных блюд рассчитаны приблизительно по общей стоимости меню. Их можно изменить в карточке блюда.</p>':''}
|
${estimates?'<p class="ct-banquet-estimate">≈ Веса и цены отдельных блюд рассчитаны приблизительно по общей стоимости меню. Их можно изменить в карточке блюда.</p>':''}
|
||||||
${menu?`<div class="ct-banquet-preset"><span><b>${esc(menu.name)}</b> · горячее — одно блюдо на выбор</span><button class="outline" type="button" data-banquet-preset="${esc(menu.id)}">Выбрать меню целиком</button></div>`:''}
|
${menu?`<div class="ct-banquet-preset"><span><b>${esc(menu.name)}</b> · горячее — одно блюдо на выбор</span><button class="outline" type="button" data-banquet-preset="${esc(menu.id)}">Выбрать меню целиком</button></div>`:''}
|
||||||
<div class="ct-banquet-groups" role="group" aria-label="Разделы банкетного меню"><button type="button" data-banquet-group="all" aria-pressed="${groupId==='all'}">Все разделы <span>${candidates.length}</span></button>${available.map(([id,label])=>`<button type="button" data-banquet-group="${id}" aria-pressed="${groupId===id}">${label} <span>${candidates.filter(i=>group(i)===id).length}</span></button>`).join('')}</div>
|
<div class="ct-banquet-groups" role="group" aria-label="Разделы банкетного меню"><button type="button" data-banquet-group="all" aria-pressed="${groupId==='all'}">Все разделы <span>${candidates.length}</span></button>${available.map(([id,label])=>`<button type="button" data-banquet-group="${id}" aria-pressed="${groupId===id}">${label} <span>${candidates.filter(i=>group(i)===id).length}</span></button>`).join('')}</div>
|
||||||
<div class="ct-banquet-layout"><div class="ct-banquet-dishes">${visible.length?[...sections].map(([section,rows])=>`<section class="ct-banquet-section"><h3>${esc(section)}${rows.some(i=>i.banquet?.choiceGroup)?'<small>Одно блюдо на выбор в каждом пакете</small>':''}</h3>${rows.map(i=>`<div class="ct-banquet-dish ${ids.has(String(i.id))?'selected':''}" data-banquet-row="${esc(i.id)}"><label><input type="checkbox" data-banquet-item="${esc(i.id)}" ${ids.has(String(i.id))?'checked':''}><span class="ct-banquet-name">${esc(i.name)}<small>${esc(i.banquet?.packageName||i.catalogSection||'')}${i.banquet?.estimated?' · ≈':''}</small></span><span class="ct-banquet-weight">${esc(i.weight||'—')}</span><b class="ct-banquet-price">${money(unitPrice(i,draft))}</b></label>${i.demo&&i.ttk?`<button type="button" data-banquet-ttk="${esc(i.id)}" aria-label="ТТК: ${esc(i.name)}" title="Открыть технологическую карту">ТТК</button>`:''}<button type="button" data-banquet-edit="${esc(i.id)}" aria-label="Изменить ${esc(i.name)}" title="Изменить блюдо">✎</button></div>`).join('')}</section>`).join(''):`<p class="catalog-empty">${catalog.length?'По вашему запросу блюда не найдены.':'В банкетном меню пока нет блюд. Добавьте свои позиции.'}</p>`}</div>
|
<div class="ct-banquet-layout"><div class="ct-banquet-dishes">${visible.length?[...sections].map(([section,rows])=>`<section class="ct-banquet-section"><h3>${esc(section)}${rows.some(i=>i.banquet?.choiceGroup)?'<small>Одно блюдо на выбор в каждом пакете</small>':''}</h3>${rows.map(i=>`<div class="ct-banquet-dish ${ids.has(String(i.id))?'selected':''}" data-banquet-row="${esc(i.id)}"><label><input type="checkbox" data-banquet-item="${esc(i.id)}" ${ids.has(String(i.id))?'checked':''}><span class="ct-banquet-name">${esc(i.name)}${window.CateriumPricing?.badge(i)||''}<small>${esc(i.banquet?.packageName||i.catalogSection||'')}${i.banquet?.estimated?' · ≈':''}</small></span><span class="ct-banquet-weight">${esc(i.weight||'—')}</span><b class="ct-banquet-price">${money(unitPrice(i,draft))}</b></label>${i.demo&&i.ttk?`<button type="button" data-banquet-ttk="${esc(i.id)}" aria-label="ТТК: ${esc(i.name)}" title="Открыть технологическую карту">ТТК</button>`:''}<button type="button" data-banquet-edit="${esc(i.id)}" aria-label="Изменить ${esc(i.name)}" title="Изменить блюдо">✎</button></div>`).join('')}</section>`).join(''):`<p class="catalog-empty">${catalog.length?'По вашему запросу блюда не найдены.':'В банкетном меню пока нет блюд. Добавьте свои позиции.'}</p>`}</div>
|
||||||
<aside class="ct-banquet-summary"><span class="ct-eyebrow">Ваше меню</span><h3>${selected.length?`${selected.length} блюд`:'Пока пусто'}</h3><label>Количество гостей<input type="number" min="1" max="10000" step="1" data-banquet-guests value="${count}"></label><div class="ct-banquet-metric"><span>На одного гостя</span><b data-banquet-per-guest>${money(sum.price)}</b><small data-banquet-weight>${sum.grams.toLocaleString('ru-RU')} г</small></div><div class="ct-banquet-total"><span>На всех гостей</span><b data-banquet-total>${money(sum.price*count)}</b><small data-banquet-total-weight>${(sum.grams*count/1000).toLocaleString('ru-RU',{maximumFractionDigits:2})} кг</small></div><button class="primary" type="button" data-banquet-apply ${selected.length?'':'disabled'}>${hasExisting?'Обновить банкет в заказе':'Добавить меню в заказ'}</button>${hasExisting?'<small class="ct-banquet-help">Обновятся только банкетные блюда. Остальные позиции заказа сохранятся.</small>':''}${selected.length?`<details><summary>Выбрано: ${selected.length}</summary><ul>${selected.map(i=>`<li>${esc(i.name)}</li>`).join('')}</ul></details><button class="ct-banquet-clear" type="button" data-banquet-clear>Снять выбор блюд</button>`:'<p class="ct-banquet-help">Блюда из разных разделов остаются в общей подборке.</p>'}</aside></div></div>`;
|
<aside class="ct-banquet-summary"><span class="ct-eyebrow">Ваше меню</span><h3>${selected.length?`${selected.length} блюд`:'Пока пусто'}</h3><label>Количество гостей<input type="number" min="1" max="10000" step="1" data-banquet-guests value="${count}"></label><div class="ct-banquet-metric"><span>На одного гостя</span><b data-banquet-per-guest>${money(sum.price)}</b><small data-banquet-weight>${sum.grams.toLocaleString('ru-RU')} г</small></div><div class="ct-banquet-total"><span>На всех гостей</span><b data-banquet-total>${money(sum.price*count)}</b><small data-banquet-total-weight>${(sum.grams*count/1000).toLocaleString('ru-RU',{maximumFractionDigits:2})} кг</small></div><button class="primary" type="button" data-banquet-apply ${selected.length?'':'disabled'}>${hasExisting?'Обновить банкет в заказе':'Добавить меню в заказ'}</button>${hasExisting?'<small class="ct-banquet-help">Обновятся только банкетные блюда. Остальные позиции заказа сохранятся.</small>':''}${selected.length?`<details><summary>Выбрано: ${selected.length}</summary><ul>${selected.map(i=>`<li>${esc(i.name)}</li>`).join('')}</ul></details><button class="ct-banquet-clear" type="button" data-banquet-clear>Снять выбор блюд</button>`:'<p class="ct-banquet-help">Блюда из разных разделов остаются в общей подборке.</p>'}</aside></div></div>`;
|
||||||
}
|
}
|
||||||
function bind(root,context){
|
function bind(root,context){
|
||||||
|
|||||||
112
public/core/catalog-pricing.js
Normal file
112
public/core/catalog-pricing.js
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
/* Catalog prices are derived, never overwritten by a timer. Stored order lines
|
||||||
|
keep their own price snapshots. Promotion dates are absolute UTC instants. */
|
||||||
|
(()=>{
|
||||||
|
'use strict';
|
||||||
|
const DAY=86400000,MAX_TIMER=2147480000;
|
||||||
|
const number=v=>typeof v==='number'?v:Number(String(v??'').trim().replace(',','.'));
|
||||||
|
const cents=v=>Math.round((v+Number.EPSILON)*100)/100;
|
||||||
|
const validPrice=v=>Number.isFinite(number(v))&&number(v)>=0;
|
||||||
|
const money=v=>`${Number(v).toLocaleString('ru-RU',{maximumFractionDigits:2})} ₽`;
|
||||||
|
const stamp=v=>typeof v==='string'&&v.trim()?Date.parse(v):NaN;
|
||||||
|
const $=id=>document.getElementById(id);
|
||||||
|
function basePrice(item){
|
||||||
|
const current=validPrice(item?.price)?cents(number(item.price)):0;
|
||||||
|
// Compatibility only: old records stored the discounted price in price.
|
||||||
|
return !item?.promotion&&validPrice(item?.oldPrice)&&number(item.oldPrice)>current?cents(number(item.oldPrice)):current;
|
||||||
|
}
|
||||||
|
function promotion(item){
|
||||||
|
if(item?.promotion&&typeof item.promotion==='object')return item.promotion;
|
||||||
|
const base=basePrice(item),current=validPrice(item?.price)?cents(number(item.price)):0;
|
||||||
|
return base>current?{type:'amount',value:cents(base-current),startsAt:null,endsAt:null}:null;
|
||||||
|
}
|
||||||
|
function quote(item,now=Date.now()){
|
||||||
|
const base=basePrice(item),p=promotion(item),value=number(p?.value);
|
||||||
|
const start=p?.startsAt==null?-Infinity:stamp(p.startsAt),end=p?.endsAt==null?Infinity:stamp(p.endsAt);
|
||||||
|
const valid=Boolean(p&&['percent','amount'].includes(p.type)&&Number.isFinite(value)&&value>0&&base>0&&value<=(p.type==='percent'?100:base)&&!Number.isNaN(start)&&!Number.isNaN(end)&&end>start);
|
||||||
|
const active=valid&&now>=start&&now<end;
|
||||||
|
return {base,price:active?cents(Math.max(0,base-(p.type==='percent'?base*value/100:value))):base,active,scheduled:valid&&now<start,expired:valid&&now>=end};
|
||||||
|
}
|
||||||
|
const price=(item,now)=>quote(item,now).price;
|
||||||
|
function linePrice(line,item,now){return line?.price!==undefined&&line.price!==null&&String(line.price).trim()!==''&&validPrice(line.price)?cents(number(line.price)):price(item,now)}
|
||||||
|
const badge=(item)=>quote(item).active?'<span class="ct-promotion-badge">Акция</span>':'';
|
||||||
|
const localTime=iso=>{const d=new Date(iso);if(!Number.isFinite(d.getTime()))return '';return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}T${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`};
|
||||||
|
let editorOriginal=null,getter=null,timer=0,lastSignature='';
|
||||||
|
function loadEditor(item){
|
||||||
|
const p=promotion(item),q=quote(item);
|
||||||
|
editorOriginal=p?{...p}:null;
|
||||||
|
if(!$('ctPromotionEnabled'))return;
|
||||||
|
$('boxPrice').value=basePrice(item);
|
||||||
|
$('ctPromotionEnabled').checked=Boolean(p&&!q.expired);
|
||||||
|
$('ctDiscountType').value=p?.type==='amount'?'amount':'percent';
|
||||||
|
$('ctDiscountValue').value=p?.value??'';
|
||||||
|
const keep=$('ctPromotionDuration').querySelector('[value="keep"]');
|
||||||
|
keep.hidden=!(p?.endsAt&&!q.expired);
|
||||||
|
$('ctPromotionDuration').value=p&&!q.expired?(p.endsAt?'keep':'none'):'7';
|
||||||
|
$('ctPromotionEnd').value=p?.endsAt?localTime(p.endsAt):'';
|
||||||
|
$('ctPromotionError').textContent='';
|
||||||
|
updateEditor();
|
||||||
|
}
|
||||||
|
function readEditor(now=Date.now()){
|
||||||
|
const base=number($('boxPrice')?.value);
|
||||||
|
if(!Number.isFinite(base)||base<0)return {error:'Укажите обычную цену: число не меньше нуля.',field:'boxPrice'};
|
||||||
|
if(!$('ctPromotionEnabled')?.checked)return {base:cents(base),promotion:null};
|
||||||
|
const type=$('ctDiscountType').value,raw=$('ctDiscountValue').value,value=number(raw);
|
||||||
|
if(!raw.trim()||!Number.isFinite(value)||cents(value)<=0)return {error:'Укажите скидку больше нуля.',field:'ctDiscountValue'};
|
||||||
|
if(base<=0||value>(type==='percent'?100:base))return {error:type==='percent'?'Скидка должна быть не больше 100%, а обычная цена — больше нуля.':'Скидка не может превышать обычную цену.',field:'ctDiscountValue'};
|
||||||
|
const duration=$('ctPromotionDuration').value;
|
||||||
|
let startsAt=editorOriginal?.startsAt||new Date(now).toISOString(),endsAt=null;
|
||||||
|
if(duration==='keep')endsAt=editorOriginal?.endsAt;
|
||||||
|
else if(duration==='custom'){
|
||||||
|
const rawEnd=$('ctPromotionEnd').value,t=new Date(rawEnd).getTime();
|
||||||
|
if(!rawEnd||!Number.isFinite(t))return {error:'Выберите дату и время окончания акции.',field:'ctPromotionEnd'};
|
||||||
|
endsAt=new Date(t).toISOString();startsAt=new Date(now).toISOString();
|
||||||
|
}else if(duration!=='none'){
|
||||||
|
if(!['1','7','14','30'].includes(duration))return {error:'Выберите срок акции.',field:'ctPromotionDuration'};
|
||||||
|
startsAt=new Date(now).toISOString();endsAt=new Date(now+Number(duration)*DAY).toISOString();
|
||||||
|
}
|
||||||
|
if(endsAt&&stamp(endsAt)<=now)return {error:'Акция уже закончилась. Выберите новый срок или выключите её.',field:'ctPromotionDuration'};
|
||||||
|
return {base:cents(base),promotion:{type,value:cents(value),startsAt,endsAt}};
|
||||||
|
}
|
||||||
|
function updateEditor(){
|
||||||
|
const enabled=$('ctPromotionEnabled')?.checked,fields=$('ctPromotionFields');if(!fields)return;
|
||||||
|
fields.hidden=!enabled;
|
||||||
|
$('ctPromotionEndLabel').hidden=$('ctPromotionDuration').value!=='custom';
|
||||||
|
const type=$('ctDiscountType').value;
|
||||||
|
$('ctDiscountCaption').textContent=type==='percent'?'Скидка, %':'Скидка, ₽';
|
||||||
|
$('ctDiscountValue').max=type==='percent'?'100':String(number($('boxPrice').value)||0);
|
||||||
|
const data=readEditor(),preview=$('ctPromotionPreview');
|
||||||
|
if(data.error){preview.textContent=enabled?data.error:'';return;}
|
||||||
|
const q=quote({price:data.base,promotion:data.promotion});
|
||||||
|
const ending=data.promotion?.endsAt?` До ${new Date(data.promotion.endsAt).toLocaleString('ru-RU',{day:'numeric',month:'long',hour:'2-digit',minute:'2-digit'})}. Затем снова ${money(data.base)}.`:' Без ограничения срока.';
|
||||||
|
preview.textContent=enabled?`Цена по акции: ${money(q.price)}.${ending}`:`Обычная цена: ${money(data.base)}.`;
|
||||||
|
}
|
||||||
|
function saveEditor(item){
|
||||||
|
const data=readEditor();
|
||||||
|
if(data.error){$('ctPromotionError').textContent=data.error;$(data.field)?.focus();return false;}
|
||||||
|
item.price=data.base;
|
||||||
|
if(data.promotion)item.promotion=data.promotion;else delete item.promotion;
|
||||||
|
delete item.oldPrice;delete item.sale;
|
||||||
|
$('ctPromotionError').textContent='';return true;
|
||||||
|
}
|
||||||
|
function refresh(){
|
||||||
|
clearTimeout(timer);timer=0;if(!getter)return;
|
||||||
|
const now=Date.now(),items=getter()||[];
|
||||||
|
const signature=JSON.stringify(items.map(i=>[String(i.id),price(i,now),quote(i,now).active]));
|
||||||
|
if(signature!==lastSignature){lastSignature=signature;window.dispatchEvent(new CustomEvent('caterium:catalog-prices-changed'));}
|
||||||
|
let next=Infinity;
|
||||||
|
for(const item of items){const p=promotion(item);for(const t of [stamp(p?.startsAt),stamp(p?.endsAt)])if(t>now)next=Math.min(next,t);}
|
||||||
|
if(Number.isFinite(next))timer=setTimeout(refresh,Math.min(MAX_TIMER,Math.max(1,next-Date.now()+10)));
|
||||||
|
}
|
||||||
|
function watch(catalogGetter){getter=catalogGetter;refresh();}
|
||||||
|
function boot(){
|
||||||
|
$('ctPromotionEditor')?.addEventListener('input',()=>{if($('ctPromotionError'))$('ctPromotionError').textContent='';updateEditor();});
|
||||||
|
$('ctPromotionEditor')?.addEventListener('change',updateEditor);
|
||||||
|
$('boxPrice')?.addEventListener('input',updateEditor);
|
||||||
|
window.addEventListener('sun:cloud-state-applied',refresh);
|
||||||
|
window.addEventListener('focus',refresh);
|
||||||
|
document.addEventListener('visibilitychange',()=>{if(!document.hidden)refresh();});
|
||||||
|
window.addEventListener('sun:cloud-tenant-changing',()=>{clearTimeout(timer);timer=0;lastSignature='';editorOriginal=null;});
|
||||||
|
}
|
||||||
|
window.CateriumPricing=Object.freeze({basePrice,promotion,quote,price,linePrice,badge,loadEditor,saveEditor,refresh,watch});
|
||||||
|
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',boot,{once:true});else boot();
|
||||||
|
})();
|
||||||
30
public/core/client-menu.css
Normal file
30
public/core/client-menu.css
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
/* Compact client summaries. Full addresses, orders and loyalty remain in the
|
||||||
|
existing detail dialog. The entire summary is a keyboard-accessible button. */
|
||||||
|
#clients #client-list{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:8px;align-items:start}
|
||||||
|
#clients .client-card{min-width:0;margin:0;padding:0;border-radius:10px;overflow:hidden;background:var(--sun-ui-card,#fff);border:1px solid var(--sun-ui-border,#d9dde0);box-shadow:none}
|
||||||
|
#clients .client-open{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:4px 8px;align-content:center;width:100%;min-height:82px;margin:0;padding:10px 12px;border:0;border-radius:9px;background:transparent;color:var(--sun-ui-text,#24211d);text-align:left;font:inherit;cursor:pointer}
|
||||||
|
#clients .client-open:hover{background:color-mix(in srgb,var(--sun-ui-accent,#c99a32) 8%,var(--sun-ui-card,#fff))}
|
||||||
|
#clients .client-open:focus-visible{outline:2px solid var(--sun-ui-accent,#c99a32);outline-offset:-3px}
|
||||||
|
#clients .client-name{font-size:14px;line-height:18px;font-weight:700;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||||
|
#clients .client-phone{grid-column:1/-1;font-size:12px;line-height:15px;color:var(--sun-ui-muted,#68717a);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||||
|
#clients .client-brief{grid-column:1/-1;font-size:12px;line-height:16px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||||
|
#clients .client-loyalty-compact{align-self:center;padding:2px 5px;border-radius:6px;background:#f1ecff;color:#5b49ad;font-size:11px;line-height:14px;font-weight:700}
|
||||||
|
#clients .client-open-arrow{align-self:center;font-size:18px;line-height:18px;color:var(--sun-ui-muted,#68717a)}
|
||||||
|
#ctPromotionEditor{min-width:0;padding:12px;margin:12px 0;border:1px solid var(--sun-ui-border,#deded6);border-radius:12px;background:var(--sun-ui-card,#fff)}
|
||||||
|
#ctPromotionEditor legend{font-weight:700;font-size:14px;padding:0 5px}
|
||||||
|
#ctPromotionEditor .ct-promotion-toggle{display:flex;flex-direction:row;align-items:center;gap:8px;margin:0;font-size:14px;cursor:pointer}
|
||||||
|
#ctPromotionEditor #ctPromotionEnabled{width:18px!important;height:18px;min-height:0;margin:0;flex:0 0 18px}
|
||||||
|
#ctPromotionFields{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:12px}
|
||||||
|
#ctPromotionFields label{display:flex;flex-direction:column;gap:5px;margin:0;min-width:0}
|
||||||
|
#ctPromotionFields input,#ctPromotionFields select{width:100%;min-width:0;min-height:40px;box-sizing:border-box;font:inherit}
|
||||||
|
#ctPromotionFields .ct-promotion-wide{grid-column:1/-1}
|
||||||
|
#ctPromotionEditor [hidden]{display:none!important}
|
||||||
|
#ctPromotionEditor .ct-promotion-note{font-size:11px;line-height:1.4;color:var(--sun-ui-muted,#68717a);margin:7px 0 0}
|
||||||
|
#ctPromotionPreview{font-size:13px;line-height:1.45;margin:10px 0 0;color:var(--sun-ui-text,#24211d)}
|
||||||
|
#ctPromotionError{font-size:13px;line-height:1.4;color:#a12f24;margin:8px 0 0}
|
||||||
|
#ctPromotionError:empty{display:none}
|
||||||
|
.ct-promotion-badge{display:inline-flex;align-items:center;align-self:start;width:auto!important;padding:3px 7px;border-radius:999px;background:#b9473f;color:#fff!important;font-size:10px!important;line-height:1.2!important;font-weight:800!important;white-space:nowrap;letter-spacing:.02em}
|
||||||
|
#tiles .tile>.ct-promotion-badge{position:absolute;top:7px;right:7px;z-index:2}
|
||||||
|
.sun-menu-row-price .ct-promotion-badge{display:table;margin:4px 0 0 auto}
|
||||||
|
.ct-banquet-name .ct-promotion-badge{margin-left:6px;vertical-align:middle}
|
||||||
|
@media(max-width:480px){#clients #client-list{grid-template-columns:minmax(0,1fr)}#clients .client-open{min-height:80px}#ctPromotionFields{grid-template-columns:minmax(0,1fr)}}
|
||||||
@ -92,7 +92,7 @@
|
|||||||
const orderStamp=order=>`${String(order?.date||'').padStart(10,'0')}T${String(order?.time||'').padStart(5,'0')}`;
|
const orderStamp=order=>`${String(order?.date||'').padStart(10,'0')}T${String(order?.time||'').padStart(5,'0')}`;
|
||||||
function orderTotal(order){
|
function orderTotal(order){
|
||||||
const explicit=Number(order?.total);if(Number.isFinite(explicit))return explicit;
|
const explicit=Number(order?.total);if(Number.isFinite(explicit))return explicit;
|
||||||
const catalog=getCatalog();return (Array.isArray(order?.lines)?order.lines:[]).reduce((sum,line)=>{const item=catalog.find(x=>String(x?.id)===String(line?.id));return sum+Number(item?.price||0)*Number(line?.qty||0)},0);
|
const catalog=getCatalog();return (Array.isArray(order?.lines)?order.lines:[]).reduce((sum,line)=>{const item=catalog.find(x=>String(x?.id)===String(line?.id));return sum+(window.CateriumPricing?.linePrice(line,item)??Number(line?.price??item?.price??0))*Number(line?.qty||0)},0);
|
||||||
}
|
}
|
||||||
function readObject(key){const value=storage.read(key,{});return value&&typeof value==='object'&&!Array.isArray(value)?value:{}}
|
function readObject(key){const value=storage.read(key,{});return value&&typeof value==='object'&&!Array.isArray(value)?value:{}}
|
||||||
function cacheCore(profile){return {key:profile.key,name:String(profile.name||''),phone:String(profile.phone||''),latestAddress:String(profile.latestAddress||''),loyalty:profile.loyalty?clone(profile.loyalty):null,communication:profile.communication?clone(profile.communication):null,serverVersion:Number(profile.serverVersion||0)||null,serverUpdatedAt:String(profile.serverUpdatedAt||'')}}
|
function cacheCore(profile){return {key:profile.key,name:String(profile.name||''),phone:String(profile.phone||''),latestAddress:String(profile.latestAddress||''),loyalty:profile.loyalty?clone(profile.loyalty):null,communication:profile.communication?clone(profile.communication):null,serverVersion:Number(profile.serverVersion||0)||null,serverUpdatedAt:String(profile.serverUpdatedAt||'')}}
|
||||||
|
|||||||
@ -20,7 +20,7 @@
|
|||||||
const qa=(s,r=document)=>[...r.querySelectorAll(s)];
|
const qa=(s,r=document)=>[...r.querySelectorAll(s)];
|
||||||
const esc=v=>window.SunSafe?.escapeHTML?window.SunSafe.escapeHTML(String(v??'')):String(v??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
const esc=v=>window.SunSafe?.escapeHTML?window.SunSafe.escapeHTML(String(v??'')):String(v??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||||||
const toast=(text,type='info',ms=4500)=>{try{return window.SunEnterprise?.toast?.(text,type,ms)}catch(_){}console[type==='error'?'error':'log'](text)};
|
const toast=(text,type='info',ms=4500)=>{try{return window.SunEnterprise?.toast?.(text,type,ms)}catch(_){}console[type==='error'?'error':'log'](text)};
|
||||||
const money=n=>`${Math.round(Number(n||0)).toLocaleString('ru-RU')} ₽`;
|
const money=n=>`${Number(n||0).toLocaleString('ru-RU',{maximumFractionDigits:2})} ₽`;
|
||||||
const readJson=(key,fallback)=>{try{const v=JSON.parse(localStorage.getItem(key)||'');return v??fallback}catch(_){return fallback}};
|
const readJson=(key,fallback)=>{try{const v=JSON.parse(localStorage.getItem(key)||'');return v??fallback}catch(_){return fallback}};
|
||||||
const writeJson=(key,value)=>localStorage.setItem(key,JSON.stringify(value));
|
const writeJson=(key,value)=>localStorage.setItem(key,JSON.stringify(value));
|
||||||
const orders=()=>{const v=readJson('sunOrders',[]);return Array.isArray(v)?v:[]};
|
const orders=()=>{const v=readJson('sunOrders',[]);return Array.isArray(v)?v:[]};
|
||||||
@ -103,7 +103,7 @@
|
|||||||
function orderTotal(order){
|
function orderTotal(order){
|
||||||
if(Number.isFinite(Number(order?.total))&&Number(order.total)>0)return Number(order.total);
|
if(Number.isFinite(Number(order?.total))&&Number(order.total)>0)return Number(order.total);
|
||||||
const catalog=new Map(boxes().map(x=>[String(x.id),x]));
|
const catalog=new Map(boxes().map(x=>[String(x.id),x]));
|
||||||
return (order?.lines||[]).reduce((sum,line)=>{const live=catalog.get(String(line.id));const p=Number.isFinite(Number(line.price))?Number(line.price):Number(live?.price||0);return sum+p*Math.max(0,Number(line.qty||0));},0);
|
return (order?.lines||[]).reduce((sum,line)=>{const live=catalog.get(String(line.id));const p=Number.isFinite(Number(line.price))?Number(line.price):(window.CateriumPricing?.price(live)??Number(live?.price||0));return sum+p*Math.max(0,Number(line.qty||0));},0);
|
||||||
}
|
}
|
||||||
function showCalendarDay(cell){
|
function showCalendarDay(cell){
|
||||||
const date=calendarCellDate(cell);if(!date)return;
|
const date=calendarCellDate(cell);if(!date)return;
|
||||||
@ -127,7 +127,7 @@
|
|||||||
function showReadOnlyMenuItem(item){
|
function showReadOnlyMenuItem(item){
|
||||||
restoreEditorDialog();const pane=$('sunMenuDetailV1762');if(!pane)return;
|
restoreEditorDialog();const pane=$('sunMenuDetailV1762');if(!pane)return;
|
||||||
const comp=Array.isArray(item.composition)?item.composition.filter(Boolean):[],ingredients=Array.isArray(item.ingredients)?item.ingredients:[];
|
const comp=Array.isArray(item.composition)?item.composition.filter(Boolean):[],ingredients=Array.isArray(item.ingredients)?item.ingredients:[];
|
||||||
pane.innerHTML=`<div class="sun-menu-readonly"><h2>${esc(item.name||'Позиция')}<span class="sun-menu-readonly-badge">только просмотр</span></h2>${item.photo?`<img class="sun-menu-readonly-photo" src="${esc(item.photo)}" alt="">`:''}<div class="sun-menu-readonly-grid"><div class="sun-menu-readonly-box"><small>Категория</small><b>${esc(currentCategory().label)}</b></div><div class="sun-menu-readonly-box"><small>Цена</small><b>${esc(money(item.price||0))}</b></div><div class="sun-menu-readonly-box"><small>Вес</small><b>${esc(item.weight||'—')}</b></div><div class="sun-menu-readonly-box"><small>Количество</small><b>${Number(item.pieces||0)||'—'}${item.pieces?' шт.':''}</b></div></div><div class="sun-route-order-section"><b>Состав для клиента</b>${comp.length?`<ul class="sun-menu-readonly-list">${comp.map(x=>`<li>${esc(x)}</li>`).join('')}</ul>`:'<p class="hint">Не заполнен.</p>'}</div><div class="sun-route-order-section"><b>Состав / ТТК</b>${ingredients.length?`<ul class="sun-menu-readonly-list">${ingredients.map(x=>`<li>${esc(x?.[0]||'')} — ${esc(x?.[1]??'')} ${esc(x?.[2]||'')}</li>`).join('')}</ul>`:'<p class="hint">Не заполнен.</p>'}</div></div>`;
|
pane.innerHTML=`<div class="sun-menu-readonly"><h2>${esc(item.name||'Позиция')}<span class="sun-menu-readonly-badge">только просмотр</span></h2>${item.photo?`<img class="sun-menu-readonly-photo" src="${esc(item.photo)}" alt="">`:''}<div class="sun-menu-readonly-grid"><div class="sun-menu-readonly-box"><small>Категория</small><b>${esc(currentCategory().label)}</b></div><div class="sun-menu-readonly-box"><small>Цена</small><b>${esc(money((window.CateriumPricing?.price(item)??Number(item?.price||0))))}</b></div><div class="sun-menu-readonly-box"><small>Вес</small><b>${esc(item.weight||'—')}</b></div><div class="sun-menu-readonly-box"><small>Количество</small><b>${Number(item.pieces||0)||'—'}${item.pieces?' шт.':''}</b></div></div><div class="sun-route-order-section"><b>Состав для клиента</b>${comp.length?`<ul class="sun-menu-readonly-list">${comp.map(x=>`<li>${esc(x)}</li>`).join('')}</ul>`:'<p class="hint">Не заполнен.</p>'}</div><div class="sun-route-order-section"><b>Состав / ТТК</b>${ingredients.length?`<ul class="sun-menu-readonly-list">${ingredients.map(x=>`<li>${esc(x?.[0]||'')} — ${esc(x?.[1]??'')} ${esc(x?.[2]||'')}</li>`).join('')}</ul>`:'<p class="hint">Не заполнен.</p>'}</div></div>`;
|
||||||
}
|
}
|
||||||
function dockEditor(){
|
function dockEditor(){
|
||||||
if(!menuActive||!menuView?.classList.contains('on'))return;
|
if(!menuActive||!menuView?.classList.contains('on'))return;
|
||||||
@ -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(item.price||0))}${Number(item.oldPrice||0)>Number(item.price||0)?`<small><s>${esc(money(item.oldPrice))}</s></small>`:''}</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><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(){
|
||||||
@ -147,7 +147,7 @@
|
|||||||
originalModal=window.modal;originalCloseModal=window.closeModal;originalSaveBox=window.saveBox;originalRemoveBox=window.removeBox;
|
originalModal=window.modal;originalCloseModal=window.closeModal;originalSaveBox=window.saveBox;originalRemoveBox=window.removeBox;
|
||||||
if(typeof originalModal==='function')window.modal=function(id,...args){if(menuActive&&(id==='editor'||id==='manager')){if(id==='editor')setTimeout(dockEditor,0);return;}return originalModal.call(this,id,...args)};
|
if(typeof originalModal==='function')window.modal=function(id,...args){if(menuActive&&(id==='editor'||id==='manager')){if(id==='editor')setTimeout(dockEditor,0);return;}return originalModal.call(this,id,...args)};
|
||||||
if(typeof originalCloseModal==='function')window.closeModal=function(id,...args){if(menuActive&&(id==='editor'||id==='manager')){if(id==='editor')emptyMenuDetail('Изменения сохранены. Выберите следующую позицию.');return;}return originalCloseModal.call(this,id,...args)};
|
if(typeof originalCloseModal==='function')window.closeModal=function(id,...args){if(menuActive&&(id==='editor'||id==='manager')){if(id==='editor')emptyMenuDetail('Изменения сохранены. Выберите следующую позицию.');return;}return originalCloseModal.call(this,id,...args)};
|
||||||
if(typeof originalSaveBox==='function')window.saveBox=function(...args){const r=originalSaveBox.apply(this,args);if(menuActive)setTimeout(()=>{renderMenuList();emptyMenuDetail('Изменения сохранены. Выберите позицию для продолжения.');},0);return r};
|
if(typeof originalSaveBox==='function')window.saveBox=function(...args){const r=originalSaveBox.apply(this,args);if(menuActive&&r!==false)setTimeout(()=>{renderMenuList();emptyMenuDetail('Изменения сохранены. Выберите позицию для продолжения.');},0);return r};
|
||||||
if(typeof originalRemoveBox==='function')window.removeBox=function(...args){const r=originalRemoveBox.apply(this,args);if(menuActive)setTimeout(()=>{renderMenuList();emptyMenuDetail('Позиция удалена.');},0);return r};
|
if(typeof originalRemoveBox==='function')window.removeBox=function(...args){const r=originalRemoveBox.apply(this,args);if(menuActive)setTimeout(()=>{renderMenuList();emptyMenuDetail('Позиция удалена.');},0);return r};
|
||||||
}
|
}
|
||||||
function leaveMenu(){if(!menuActive)return;menuActive=false;restoreEditorDialog();}
|
function leaveMenu(){if(!menuActive)return;menuActive=false;restoreEditorDialog();}
|
||||||
@ -218,7 +218,7 @@
|
|||||||
const list=routeOrders(),distance=routeDistance(list,start),summary=qa('.route-summary>div',view).find(x=>/Примерно км/i.test(x.querySelector('small')?.textContent||''));if(summary?.querySelector('b'))summary.querySelector('b').textContent=distance.toLocaleString('ru-RU',{maximumFractionDigits:1});
|
const list=routeOrders(),distance=routeDistance(list,start),summary=qa('.route-summary>div',view).find(x=>/Примерно км/i.test(x.querySelector('small')?.textContent||''));if(summary?.querySelector('b'))summary.querySelector('b').textContent=distance.toLocaleString('ru-RU',{maximumFractionDigits:1});
|
||||||
}
|
}
|
||||||
function routeOrderById(id){return orders().find(x=>String(x.id)===String(id))||null}
|
function routeOrderById(id){return orders().find(x=>String(x.id)===String(id))||null}
|
||||||
function routeOrderLines(order){const catalog=new Map(boxes().map(x=>[String(x.id),x]));return (order.lines||[]).map(line=>{const item=catalog.get(String(line.id)),qty=Math.max(0,Number(line.qty||0)),price=Number.isFinite(Number(line.price))?Number(line.price):Number(item?.price||0);return {name:item?.name||line.name||`Позиция ${line.id}`,qty,price,sum:qty*price}}).filter(x=>x.qty>0)}
|
function routeOrderLines(order){const catalog=new Map(boxes().map(x=>[String(x.id),x]));return (order.lines||[]).map(line=>{const item=catalog.get(String(line.id)),qty=Math.max(0,Number(line.qty||0)),price=Number.isFinite(Number(line.price))?Number(line.price):(window.CateriumPricing?.price(item)??Number(item?.price||0));return {name:item?.name||line.name||`Позиция ${line.id}`,qty,price,sum:qty*price}}).filter(x=>x.qty>0)}
|
||||||
function showRouteOrder(id){
|
function showRouteOrder(id){
|
||||||
const o=routeOrderById(id);if(!o)return toast('Заказ не найден.','warn');const lines=routeOrderLines(o),total=orderTotal(o),paid=Math.max(0,Number(o.prepayment||0)),balance=Math.max(0,total-paid),editable=!support()&&can('orders.edit');
|
const o=routeOrderById(id);if(!o)return toast('Заказ не найден.','warn');const lines=routeOrderLines(o),total=orderTotal(o),paid=Math.max(0,Number(o.prepayment||0)),balance=Math.max(0,total-paid),editable=!support()&&can('orders.edit');
|
||||||
const body=`<div class="sun-route-order-grid"><div class="sun-route-order-kpi"><small>Дата и время</small><b>${esc(fmtDate(o.date))} · ${esc(o.time||'—')}</b></div><div class="sun-route-order-kpi"><small>Статус</small><b>${esc(o.status||'Новый')}</b></div><div class="sun-route-order-kpi"><small>Сумма</small><b>${esc(money(total))}</b></div><div class="sun-route-order-kpi"><small>Остаток</small><b>${esc(money(balance))}</b></div></div><div class="sun-route-order-section"><p><b>${esc(o.event||'Заказ')}</b>${o.guestsCount?` · ${esc(o.guestsCount)} гостей`:''}</p><p><b>Клиент:</b> ${esc(o.contact||'—')} · ${esc(o.phone||'—')}</p><p><b>Адрес:</b> ${esc(o.address||'—')}</p>${o.delivery?`<p><b>Доставка:</b> ${esc(money(o.delivery))}</p>`:''}${o.courierNote?`<div class="sun-route-order-note"><b>Курьеру:</b><br>${esc(o.courierNote)}</div>`:''}${o.note?`<div class="sun-route-order-note"><b>Комментарий:</b><br>${esc(o.note)}</div>`:''}</div><div class="sun-route-order-section"><b>Меню заказа</b><div style="overflow:auto;margin-top:8px"><table class="sun-route-order-lines"><thead><tr><th>Позиция</th><th>Кол.</th><th>Цена</th><th>Сумма</th></tr></thead><tbody>${lines.length?lines.map(x=>`<tr><td>${esc(x.name)}</td><td>${x.qty}</td><td>${esc(money(x.price))}</td><td><b>${esc(money(x.sum))}</b></td></tr>`).join(''):'<tr><td colspan="4">Позиции не указаны.</td></tr>'}</tbody></table></div></div>`;
|
const body=`<div class="sun-route-order-grid"><div class="sun-route-order-kpi"><small>Дата и время</small><b>${esc(fmtDate(o.date))} · ${esc(o.time||'—')}</b></div><div class="sun-route-order-kpi"><small>Статус</small><b>${esc(o.status||'Новый')}</b></div><div class="sun-route-order-kpi"><small>Сумма</small><b>${esc(money(total))}</b></div><div class="sun-route-order-kpi"><small>Остаток</small><b>${esc(money(balance))}</b></div></div><div class="sun-route-order-section"><p><b>${esc(o.event||'Заказ')}</b>${o.guestsCount?` · ${esc(o.guestsCount)} гостей`:''}</p><p><b>Клиент:</b> ${esc(o.contact||'—')} · ${esc(o.phone||'—')}</p><p><b>Адрес:</b> ${esc(o.address||'—')}</p>${o.delivery?`<p><b>Доставка:</b> ${esc(money(o.delivery))}</p>`:''}${o.courierNote?`<div class="sun-route-order-note"><b>Курьеру:</b><br>${esc(o.courierNote)}</div>`:''}${o.note?`<div class="sun-route-order-note"><b>Комментарий:</b><br>${esc(o.note)}</div>`:''}</div><div class="sun-route-order-section"><b>Меню заказа</b><div style="overflow:auto;margin-top:8px"><table class="sun-route-order-lines"><thead><tr><th>Позиция</th><th>Кол.</th><th>Цена</th><th>Сумма</th></tr></thead><tbody>${lines.length?lines.map(x=>`<tr><td>${esc(x.name)}</td><td>${x.qty}</td><td>${esc(money(x.price))}</td><td><b>${esc(money(x.sum))}</b></td></tr>`).join(''):'<tr><td colspan="4">Позиции не указаны.</td></tr>'}</tbody></table></div></div>`;
|
||||||
@ -253,6 +253,7 @@
|
|||||||
installStyles();patchSupportPermissions();installMenuPage();maintainSupport();syncMenuPermission();enhanceRoutePage();
|
installStyles();patchSupportPermissions();installMenuPage();maintainSupport();syncMenuPermission();enhanceRoutePage();
|
||||||
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('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);
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
(()=>{
|
(()=>{
|
||||||
'use strict';
|
'use strict';
|
||||||
const VERSION='17.7.3';
|
const VERSION='17.7.3';
|
||||||
const RELEASE='20260918-ui-stability';
|
const RELEASE='20260918-ui-stability-20260919-client-menu';
|
||||||
|
|
||||||
const hasStoredSession=()=>{try{return Object.keys(localStorage).some(k=>/^sb-.*-auth-token$/i.test(k)&&String(localStorage.getItem(k)||'').length>20)}catch(_){return false}};
|
const hasStoredSession=()=>{try{return Object.keys(localStorage).some(k=>/^sb-.*-auth-token$/i.test(k)&&String(localStorage.getItem(k)||'').length>20)}catch(_){return false}};
|
||||||
function installAuthBoot(){
|
function installAuthBoot(){
|
||||||
|
|||||||
@ -59,7 +59,7 @@
|
|||||||
if(existing){message(`Пробный заказ №${existing.id} уже создан. Его можно открыть во вкладке «Заказы».`);return;}
|
if(existing){message(`Пробный заказ №${existing.id} уже создан. Его можно открыть во вкладке «Заказы».`);return;}
|
||||||
const spec=read(KEY,{}).scenario?.lines,items=catalog();
|
const spec=read(KEY,{}).scenario?.lines,items=catalog();
|
||||||
if(!Array.isArray(spec)||spec.length!==3){message('Сценарий недоступен. Добавьте боксы в заказ самостоятельно.');return;}
|
if(!Array.isArray(spec)||spec.length!==3){message('Сценарий недоступен. Добавьте боксы в заказ самостоятельно.');return;}
|
||||||
const lines=spec.map(l=>{const box=items.find(b=>String(b.id)===String(l.id));return box?{id:box.id,name:box.name,qty:l.qty,price:Number(box.price)||0}:null});
|
const lines=spec.map(l=>{const box=items.find(b=>String(b.id)===String(l.id));return box?{id:box.id,name:box.name,qty:l.qty,price:(window.CateriumPricing?.price(box)??Number(box?.price||0))}:null});
|
||||||
if(lines.some(l=>!l)){message('Некоторые демо-боксы удалены. Добавьте оставшиеся в заказ самостоятельно.');return;}
|
if(lines.some(l=>!l)){message('Некоторые демо-боксы удалены. Добавьте оставшиеся в заказ самостоятельно.');return;}
|
||||||
const total=lines.reduce((s,l)=>s+l.price*l.qty,0),day=new Date();day.setDate(day.getDate()+1);
|
const total=lines.reduce((s,l)=>s+l.price*l.qty,0),day=new Date();day.setDate(day.getDate()+1);
|
||||||
const date=`${day.getFullYear()}-${String(day.getMonth()+1).padStart(2,'0')}-${String(day.getDate()).padStart(2,'0')}`;
|
const date=`${day.getFullYear()}-${String(day.getMonth()+1).padStart(2,'0')}-${String(day.getDate()).padStart(2,'0')}`;
|
||||||
@ -73,7 +73,7 @@
|
|||||||
$('ctDemoTtk')?.remove();const t=box.ttk,dialog=document.createElement('dialog');dialog.id='ctDemoTtk';
|
$('ctDemoTtk')?.remove();const t=box.ttk,dialog=document.createElement('dialog');dialog.id='ctDemoTtk';
|
||||||
const changed=JSON.stringify(box.ingredients)!==JSON.stringify(t.rows.map(r=>[r.name,r.gross,r.unit]));
|
const changed=JSON.stringify(box.ingredients)!==JSON.stringify(t.rows.map(r=>[r.name,r.gross,r.unit]));
|
||||||
dialog.innerHTML=`<div class="ct-demo-heading"><div><small>${esc(t.number)} · ${esc(t.basis)}</small><h2>${esc(box.name)}</h2></div><button class="outline" data-ttk-close aria-label="Закрыть ТТК">×</button></div>
|
dialog.innerHTML=`<div class="ct-demo-heading"><div><small>${esc(t.number)} · ${esc(t.basis)}</small><h2>${esc(box.name)}</h2></div><button class="outline" data-ttk-close aria-label="Закрыть ТТК">×</button></div>
|
||||||
<p>${changed?'Исходный выход':'Выход'}: <b>${num(t.outputGrams)} г${Number(box.category)===6?' · 1 порция':t.pieces?` · ${num(t.pieces)} шт.`:''}</b> · ${Number(box.category)===6?'Цена порции':'Цена бокса'}: <b>${money(box.price)}</b></p>
|
<p>${changed?'Исходный выход':'Выход'}: <b>${num(t.outputGrams)} г${Number(box.category)===6?' · 1 порция':t.pieces?` · ${num(t.pieces)} шт.`:''}</b> · ${Number(box.category)===6?'Цена порции':'Цена бокса'}: <b>${money((window.CateriumPricing?.price(box)??Number(box?.price||0)))}</b></p>
|
||||||
${changed?'<p class="ct-demo-notice">Состав изменён. Закупка и списание используют текущий состав из редактора. Ниже показана исходная учебная ТТК.</p>':''}
|
${changed?'<p class="ct-demo-notice">Состав изменён. Закупка и списание используют текущий состав из редактора. Ниже показана исходная учебная ТТК.</p>':''}
|
||||||
<div class="ct-demo-table"><table><thead><tr><th>Продукт / упаковка</th><th>Ед.</th><th>Брутто</th><th>Нетто</th><th>Цена за ед.</th><th>Сумма</th></tr></thead><tbody>${t.rows.map(r=>`<tr><td>${esc(r.name)}</td><td>${esc(r.unit)}</td><td>${num(r.gross)}</td><td>${num(r.net)}</td><td>${money(r.unitCost)}</td><td>${money(r.gross*r.unitCost)}</td></tr>`).join('')}</tbody></table></div>
|
<div class="ct-demo-table"><table><thead><tr><th>Продукт / упаковка</th><th>Ед.</th><th>Брутто</th><th>Нетто</th><th>Цена за ед.</th><th>Сумма</th></tr></thead><tbody>${t.rows.map(r=>`<tr><td>${esc(r.name)}</td><td>${esc(r.unit)}</td><td>${num(r.gross)}</td><td>${num(r.net)}</td><td>${money(r.unitCost)}</td><td>${money(r.gross*r.unitCost)}</td></tr>`).join('')}</tbody></table></div>
|
||||||
<p><b>Продукты и упаковка: ${money(t.ingredientCost)}</b></p><p class="hint">Учебные закупочные цены. Работа, доставка и накладные расходы не включены. Расход со склада считается по брутто.</p>
|
<p><b>Продукты и упаковка: ${money(t.ingredientCost)}</b></p><p class="hint">Учебные закупочные цены. Работа, доставка и накладные расходы не включены. Расход со склада считается по брутто.</p>
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
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';
|
const CACHE='sun-catering-pwa-v110-20260918-ui-stability-20260919-client-menu';
|
||||||
const VERSION='20260918-ui-stability';
|
const VERSION='20260918-ui-stability';
|
||||||
const CORE=[
|
const CORE=[
|
||||||
|
'./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}`,
|
||||||
'./','./index.html',`./core/mobile-order.js?v=${VERSION}`,`./core/proposal-layout.js?v=${VERSION}`,'./fonts/Manrope.ttf','./fonts/PlayfairDisplay.ttf','./fonts/PlayfairDisplay-Italic.ttf',`./core/trial-demo.js?v=${VERSION}`,`./core/cloud-transport.js?v=${VERSION}`,`./core/banquet-menu.js?v=${VERSION}`,`./core/access-policy.js?v=${VERSION}`,`./core/import-archive.js?v=${VERSION}`,`./core/company-branding.js?v=${VERSION}`,`./core/signature-offer-pdf-v18.js?v=${VERSION}`,`./core/brand-theme.js?v=${VERSION}`,
|
'./','./index.html',`./core/mobile-order.js?v=${VERSION}`,`./core/proposal-layout.js?v=${VERSION}`,'./fonts/Manrope.ttf','./fonts/PlayfairDisplay.ttf','./fonts/PlayfairDisplay-Italic.ttf',`./core/trial-demo.js?v=${VERSION}`,`./core/cloud-transport.js?v=${VERSION}`,`./core/banquet-menu.js?v=${VERSION}`,`./core/access-policy.js?v=${VERSION}`,`./core/import-archive.js?v=${VERSION}`,`./core/company-branding.js?v=${VERSION}`,`./core/signature-offer-pdf-v18.js?v=${VERSION}`,`./core/brand-theme.js?v=${VERSION}`,
|
||||||
@ -11,6 +12,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/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',
|
||||||
'/core/banquet-menu.js','/core/access-policy.js','/core/import-archive.js','/core/company-branding.js','/core/brand-theme.js','/core/sun-safe.js','/core/performance.js','/core/account-center-v1780.js','/core/login-signature-v1776.js','/core/login-signature-v1776.css','/core/auth-security-v1774.js','/legacy/bootstrap.js','/app-runtime.js'
|
'/core/banquet-menu.js','/core/access-policy.js','/core/import-archive.js','/core/company-branding.js','/core/brand-theme.js','/core/sun-safe.js','/core/performance.js','/core/account-center-v1780.js','/core/login-signature-v1776.js','/core/login-signature-v1776.css','/core/auth-security-v1774.js','/legacy/bootstrap.js','/app-runtime.js'
|
||||||
|
|||||||
131
tests/client-menu.spec.mjs
Normal file
131
tests/client-menu.spec.mjs
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
import fs from 'node:fs';
|
||||||
|
import {test,expect} from '@playwright/test';
|
||||||
|
|
||||||
|
const NOW='2026-09-19T09:00:00.000Z',WEEK=7*86400000;
|
||||||
|
const sample={id:'pricing-box',category:0,name:'Праздничный бокс',price:1000,weight:'600 г',pieces:10,ingredients:[['Томаты',1,'кг']]};
|
||||||
|
async function fixture(page){
|
||||||
|
page.on('dialog',d=>d.accept());
|
||||||
|
await page.route('https://**',r=>r.abort());
|
||||||
|
await page.route('**/api/index.php*',r=>r.abort());
|
||||||
|
await page.addInitScript(item=>{
|
||||||
|
const ws='pricing-workspace',user={id:'pricing-user',email:'pricing@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('sunBoxes'))localStorage.setItem('sunBoxes',JSON.stringify([item]));
|
||||||
|
if(!localStorage.getItem('sunOrders'))localStorage.setItem('sunOrders',JSON.stringify(Array.from({length:32},(_,i)=>({id:i+1,contact:`Клиент ${String(i+1).padStart(2,'0')}`,phone:`+7 900 000 ${String(i).padStart(4,'0')}`,address:`Адрес клиента ${i}`,event:'Фуршет',date:'2099-12-20',time:'12:00',status:'Новый',lines:[{id:item.id,qty:1,price:1000}],total:1000,prepayment:0}))));
|
||||||
|
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(){}
|
||||||
|
})};
|
||||||
|
},sample);
|
||||||
|
await page.clock.setFixedTime(new Date(NOW));
|
||||||
|
await page.goto('/index.html',{waitUntil:'domcontentloaded'});
|
||||||
|
await expect(page.locator('body > header')).toBeVisible();
|
||||||
|
await page.waitForFunction(()=>window.SunOpsUXV1762&&window.CateriumDataV1773&&window.__cateriumOrderEnhancementsV1775);
|
||||||
|
}
|
||||||
|
async function edit(page){
|
||||||
|
await page.locator('header nav').getByRole('button',{name:'Меню',exact:true}).click();
|
||||||
|
await page.locator('[data-menu-item-v1762="pricing-box"]').click();
|
||||||
|
await expect(page.locator('#ctPromotionEditor')).toBeVisible();
|
||||||
|
}
|
||||||
|
const saved=page=>page.evaluate(()=>JSON.parse(localStorage.sunBoxes).find(i=>i.id==='pricing-box'));
|
||||||
|
|
||||||
|
// Pure policy: no backend, no timer is needed to expire a closed application's menu.
|
||||||
|
test('promotion policy validates amounts and dates and never mutates base prices or order snapshots',async({page})=>{
|
||||||
|
await page.setContent('<html><body></body></html>');
|
||||||
|
await page.addScriptTag({content:fs.readFileSync('public/core/catalog-pricing.js','utf8')});
|
||||||
|
const values=await page.evaluate(()=>{
|
||||||
|
const P=CateriumPricing,now=Date.parse('2026-09-19T09:00:00Z'),end=new Date(now+7*86400000).toISOString();
|
||||||
|
const item={price:999,promotion:{type:'percent',value:12.5,startsAt:new Date(now).toISOString(),endsAt:end}},before=JSON.stringify(item);
|
||||||
|
return {start:P.price(item,now),beforeEnd:P.price(item,Date.parse(end)-1),atEnd:P.price(item,Date.parse(end)),future:P.price(item,now-1),unchanged:before===JSON.stringify(item),snapshot:P.linePrice({price:700},item,Date.parse(end)),free:P.price({price:999,promotion:{type:'percent',value:100}},now),badAmount:P.price({price:999,promotion:{type:'amount',value:1000}},now),badDate:P.price({price:999,promotion:{type:'amount',value:100,endsAt:'bad'}},now),legacy:P.price({price:800,oldPrice:1000},now),legacyBase:P.basePrice({price:800,oldPrice:1000}),zero:P.linePrice({price:0},item,now)};
|
||||||
|
});
|
||||||
|
expect(values).toEqual({start:874.13,beforeEnd:874.13,atEnd:999,future:999,unchanged:true,snapshot:700,free:0,badAmount:999,badDate:999,legacy:800,legacyBase:1000,zero:0});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('compact clients retain search, accessible opening and full details',async({page},info)=>{
|
||||||
|
await fixture(page);
|
||||||
|
await page.locator('header nav').getByRole('button',{name:'Клиенты',exact:true}).click();
|
||||||
|
const cards=page.locator('#client-list .client-card');await expect(cards).toHaveCount(32);
|
||||||
|
const size=await cards.first().boundingBox();expect(size.height).toBeLessThanOrEqual(96);expect(size.width).toBeGreaterThanOrEqual(200);
|
||||||
|
expect(await page.locator('#client-list').evaluate(el=>el.scrollWidth<=el.clientWidth)).toBe(true);
|
||||||
|
await page.screenshot({path:info.outputPath('compact-clients.png')});
|
||||||
|
await page.locator('#client-search').fill('Клиент 07');await expect(cards).toHaveCount(1);
|
||||||
|
await page.locator('.client-open').click();await expect(page.locator('#client-card-title')).toHaveText('Клиент 07');
|
||||||
|
await expect(page.locator('#client-card-content')).toContainText('Адрес клиента 6');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('percent discount lasts a week, survives reload and expires without changing saved orders',async({page},info)=>{
|
||||||
|
await fixture(page);await edit(page);
|
||||||
|
await page.locator('#ctPromotionEnabled').check();await page.locator('#ctDiscountValue').fill('15');
|
||||||
|
await expect(page.locator('#ctPromotionDuration')).toHaveValue('7');
|
||||||
|
await expect(page.locator('#ctPromotionPreview')).toContainText('850 ₽');
|
||||||
|
await page.screenshot({path:info.outputPath('promotion-editor.png')});
|
||||||
|
await page.locator('#saveBoxButton').click();
|
||||||
|
await expect(page.locator('[data-menu-item-v1762="pricing-box"]')).toContainText('850 ₽');
|
||||||
|
const item=await saved(page);expect(item.price).toBe(1000);expect(item.promotion).toEqual({type:'percent',value:15,startsAt:NOW,endsAt:new Date(Date.parse(NOW)+WEEK).toISOString()});expect(item.oldPrice).toBeUndefined();
|
||||||
|
await page.locator('header nav').getByRole('button',{name:'Новый заказ',exact:true}).click();
|
||||||
|
await page.locator('#tiles .tile:not(.add)').click();expect(await page.evaluate(()=>draft.lines[0].price)).toBe(850);
|
||||||
|
await page.evaluate(()=>{document.getElementById('event').value='Акционный заказ';document.getElementById('date').value='2099-12-20';window.saveOrder();});
|
||||||
|
const order=await page.evaluate(()=>JSON.parse(localStorage.sunOrders).at(-1));expect(order.lines[0].price).toBe(850);
|
||||||
|
await page.reload();await page.waitForFunction(()=>window.SunOpsUXV1762&&window.CateriumDataV1773);await edit(page);
|
||||||
|
await expect(page.locator('#boxPrice')).toHaveValue('1000');await expect(page.locator('#ctPromotionDuration')).toHaveValue('keep');
|
||||||
|
await page.locator('#boxName').fill('Бокс с прежним сроком');await page.locator('#saveBoxButton').click();
|
||||||
|
expect((await saved(page)).promotion.endsAt).toBe(item.promotion.endsAt);
|
||||||
|
await page.clock.setFixedTime(new Date(Date.parse(NOW)+WEEK));
|
||||||
|
await page.evaluate(()=>window.dispatchEvent(new Event('focus')));
|
||||||
|
await expect(page.locator('[data-menu-item-v1762="pricing-box"]')).toContainText('1 000 ₽');
|
||||||
|
await expect(page.locator('[data-menu-item-v1762="pricing-box"] .ct-promotion-badge')).toHaveCount(0);
|
||||||
|
expect(await page.evaluate(id=>JSON.parse(localStorage.sunOrders).find(o=>o.id===id),order.id)).toEqual(order);
|
||||||
|
await page.reload();await page.waitForFunction(()=>window.SunOpsUXV1762);await page.evaluate(()=>window.resetDraft());
|
||||||
|
await page.locator('header nav').getByRole('button',{name:'Новый заказ',exact:true}).click();
|
||||||
|
await page.locator('#tiles .tile:not(.add)').click();expect(await page.evaluate(()=>draft.lines[0].price)).toBe(1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ruble discount supports an exact end, invalid values do not close or save the editor, disabling restores base',async({page})=>{
|
||||||
|
await fixture(page);await edit(page);
|
||||||
|
await page.locator('#ctPromotionEnabled').check();await page.locator('#ctDiscountType').selectOption('amount');
|
||||||
|
await page.locator('#ctDiscountValue').fill('1500');await page.locator('#saveBoxButton').click();
|
||||||
|
await expect(page.locator('#ctPromotionError')).toContainText('не может превышать');await expect(page.locator('#saveBoxButton')).toBeVisible();expect((await saved(page)).promotion).toBeUndefined();
|
||||||
|
await page.locator('#ctDiscountValue').fill('250');await page.locator('#ctPromotionDuration').selectOption('custom');
|
||||||
|
await page.locator('#ctPromotionEnd').fill('2026-09-18T09:00');await page.locator('#saveBoxButton').click();await expect(page.locator('#ctPromotionError')).toContainText('закончилась');
|
||||||
|
await page.locator('#ctPromotionEnd').fill('2026-10-01T12:00');await page.locator('#saveBoxButton').click();
|
||||||
|
await expect(page.locator('[data-menu-item-v1762="pricing-box"]')).toContainText('750 ₽');
|
||||||
|
await expect(page.locator('[data-menu-item-v1762="pricing-box"] .ct-promotion-badge')).toHaveText('Акция');
|
||||||
|
await expect(page.locator('[data-menu-item-v1762="pricing-box"] s')).toHaveCount(0);
|
||||||
|
const item=await saved(page);expect(item.promotion.type).toBe('amount');expect(item.promotion.value).toBe(250);expect(item.promotion.endsAt).toBeTruthy();
|
||||||
|
await edit(page);await page.locator('#ctPromotionEnabled').uncheck();await page.locator('#saveBoxButton').click();
|
||||||
|
expect((await saved(page)).price).toBe(1000);expect((await saved(page)).promotion).toBeUndefined();
|
||||||
|
await expect(page.locator('[data-menu-item-v1762="pricing-box"] .ct-promotion-badge')).toHaveCount(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('timer expires an open catalog and menu while keeping an unsaved editor intact',async({page})=>{
|
||||||
|
await fixture(page);await edit(page);
|
||||||
|
// Use real timers and a near boundary to check the actual scheduling path.
|
||||||
|
const end=await page.evaluate(()=>{
|
||||||
|
const now=Date.now();boxes[0].promotion={type:'amount',value:100,startsAt:new Date(now).toISOString(),endsAt:new Date(now+1000).toISOString()};persist();CateriumPricing.refresh();return now+1000;
|
||||||
|
});
|
||||||
|
await page.locator('#boxName').fill('Несохранённое название');await page.locator('#boxName').focus();
|
||||||
|
await page.clock.setFixedTime(new Date(end));
|
||||||
|
await expect(page.locator('[data-menu-item-v1762="pricing-box"] .ct-promotion-badge')).toHaveCount(0,{timeout:5000});
|
||||||
|
await expect(page.locator('#boxName')).toHaveValue('Несохранённое название');await expect(page.locator('#boxName')).toBeFocused();
|
||||||
|
expect((await saved(page)).price).toBe(1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test('legacy sale converts without changing its current price and fractional discounts keep cents',async({page})=>{
|
||||||
|
await fixture(page);
|
||||||
|
await page.evaluate(()=>{boxes[0].price=800;boxes[0].oldPrice=1000;boxes[0].sale=true;persist();CateriumPricing.refresh();});
|
||||||
|
await edit(page);
|
||||||
|
await expect(page.locator('#boxPrice')).toHaveValue('1000');
|
||||||
|
await expect(page.locator('#ctDiscountType')).toHaveValue('amount');
|
||||||
|
await expect(page.locator('#ctDiscountValue')).toHaveValue('200');
|
||||||
|
await expect(page.locator('#ctPromotionDuration')).toHaveValue('none');
|
||||||
|
await page.locator('#saveBoxButton').click();
|
||||||
|
const migrated=await saved(page);expect(migrated.price).toBe(1000);expect(migrated.oldPrice).toBeUndefined();expect(migrated.promotion.value).toBe(200);
|
||||||
|
await edit(page);await page.locator('#boxPrice').fill('999');await page.locator('#ctDiscountType').selectOption('percent');await page.locator('#ctDiscountValue').fill('12.5');await page.locator('#saveBoxButton').click();
|
||||||
|
await page.locator('header nav').getByRole('button',{name:'Новый заказ',exact:true}).click();await page.locator('#tiles .tile:not(.add)').click();
|
||||||
|
expect(await page.evaluate(()=>draft.lines[0].price)).toBe(874.13);
|
||||||
|
await expect(page.locator('#tiles .tile:not(.add) .tile-price')).toContainText('874,13');
|
||||||
|
expect(await page.evaluate(()=>window.sunBaseOrderTotal(draft))).toBe(874.13);
|
||||||
|
});
|
||||||
@ -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:['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:['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:['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:['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}}
|
||||||
]
|
]
|
||||||
|
|||||||
@ -22,13 +22,15 @@ try{
|
|||||||
});
|
});
|
||||||
await context.addInitScript(()=>{
|
await context.addInitScript(()=>{
|
||||||
const workspaceId='ui-smoke-workspace';
|
const workspaceId='ui-smoke-workspace';
|
||||||
|
localStorage.setItem('sunBoxes',JSON.stringify([{id:'ui-smoke-box',name:'Праздничный бокс',category:0,price:1000,ingredients:[],weight:'600 г',pieces:12}]));
|
||||||
|
localStorage.setItem('sunOrders',JSON.stringify(Array.from({length:24},(_,i)=>({id:i+1,contact:`Клиент ${String(i+1).padStart(2,'0')}`,phone:`+7 900 000 ${String(i).padStart(4,'0')}`,event:'Фуршет',address:'Адрес клиента',date:'2099-12-20',time:'12:00',status:'Новый',lines:[{id:'ui-smoke-box',qty:1,price:1000}],total:1000}))));
|
||||||
const user={id:'ui-smoke-user',email:'ui-smoke@example.invalid',email_confirmed_at:'2026-01-01T00:00:00Z'};
|
const user={id:'ui-smoke-user',email:'ui-smoke@example.invalid',email_confirmed_at:'2026-01-01T00:00:00Z'};
|
||||||
localStorage.setItem('sunCloudV2Config',JSON.stringify({workspaceId,localWorkspaceId:workspaceId,tenantStorageReady:true,autoSync:false}));
|
localStorage.setItem('sunCloudV2Config',JSON.stringify({workspaceId,localWorkspaceId:workspaceId,tenantStorageReady:true,autoSync:false}));
|
||||||
window.supabase={createClient:()=>({
|
window.supabase={createClient:()=>({
|
||||||
auth:{onAuthStateChange:()=>({data:{subscription:{unsubscribe(){}}}}),getSession:async()=>({data:{session:{user}},error:null}),getUser:async()=>({data:{user},error:null})},
|
auth:{onAuthStateChange:()=>({data:{subscription:{unsubscribe(){}}}}),getSession:async()=>({data:{session:{user}},error:null}),getUser:async()=>({data:{user},error:null})},
|
||||||
rpc:async name=>{
|
rpc:async name=>{
|
||||||
if(name==='sun_my_workspaces')return new Promise(resolve=>{window.finishUiSmokeWorkspace=()=>resolve({data:[{id:workspaceId,name:'UI verification',role:'admin',is_active:true,permissions:{}}],error:null});});
|
if(name==='sun_my_workspaces')return new Promise(resolve=>{window.finishUiSmokeWorkspace=()=>resolve({data:[{id:workspaceId,name:'UI verification',role:'admin',is_active:true,permissions:{}}],error:null});});
|
||||||
if(name==='sun_subscription_snapshot')return {data:{plan_id:'full',plan_name:'Full',status:'active',access_mode:'full',features:{}},error:null};
|
if(name==='sun_subscription_snapshot')return {data:{plan_id:'full',plan_name:'Full',status:'active',access_mode:'full',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]))},error:null};
|
||||||
if(name==='caterium_trial_demo_status')return {data:{canInstall:false,canUpgrade:false},error:null};
|
if(name==='caterium_trial_demo_status')return {data:{canInstall:false,canUpgrade:false},error:null};
|
||||||
if(name==='sun_is_platform_admin')return {data:false,error:null};
|
if(name==='sun_is_platform_admin')return {data:false,error:null};
|
||||||
return {data:null,error:null};
|
return {data:null,error:null};
|
||||||
@ -37,6 +39,7 @@ try{
|
|||||||
})};
|
})};
|
||||||
});
|
});
|
||||||
const page=await context.newPage();
|
const page=await context.newPage();
|
||||||
|
await page.clock.setFixedTime(new Date());
|
||||||
await page.goto(base.href,{waitUntil:'domcontentloaded',timeout:60000});
|
await page.goto(base.href,{waitUntil:'domcontentloaded',timeout:60000});
|
||||||
const gate=page.locator('#sunCloudAuthGateV3');
|
const gate=page.locator('#sunCloudAuthGateV3');
|
||||||
await expect(gate).toHaveAttribute('data-auth-state','loading',{timeout:20000});
|
await expect(gate).toHaveAttribute('data-auth-state','loading',{timeout:20000});
|
||||||
@ -71,8 +74,37 @@ try{
|
|||||||
await expect.poll(()=>page.locator('#ctHelpResults details').count()).toBeGreaterThan(0);
|
await expect.poll(()=>page.locator('#ctHelpResults details').count()).toBeGreaterThan(0);
|
||||||
await page.locator('#ctHelpClose').click();
|
await page.locator('#ctHelpClose').click();
|
||||||
await expect(page.locator('#ctHelpDialog')).not.toBeVisible();
|
await expect(page.locator('#ctHelpDialog')).not.toBeVisible();
|
||||||
results.push({width,quietLoading:true,automaticOpen:true,helpDialog:true,icon});
|
await page.waitForFunction(()=>window.SunOpsUXV1762&&window.CateriumDataV1773&&window.CateriumPricing);
|
||||||
console.log(`PASS published UI ${width}px: quiet loading, automatic open, Help dialog${icon?', native question-circle icon':''}`);
|
const nav=page.locator('header nav');
|
||||||
|
await nav.getByRole('button',{name:'Клиенты',exact:true}).click();
|
||||||
|
const clients=page.locator('#client-list .client-card');
|
||||||
|
await expect(clients).toHaveCount(24);
|
||||||
|
const clientSize=await clients.first().boundingBox();
|
||||||
|
assert(clientSize.height<=96);
|
||||||
|
await page.screenshot({path:`${output}/clients-${width}.png`});
|
||||||
|
await nav.getByRole('button',{name:'Меню',exact:true}).click();
|
||||||
|
await page.locator('[data-menu-item-v1762="ui-smoke-box"]').click();
|
||||||
|
await page.locator('#ctPromotionEnabled').check();
|
||||||
|
await page.locator('#ctDiscountType').selectOption('percent');
|
||||||
|
await page.locator('#ctDiscountValue').fill('15');
|
||||||
|
await expect(page.locator('#ctPromotionPreview')).toContainText('850 ₽');
|
||||||
|
await page.locator('#ctPromotionEditor').screenshot({path:`${output}/promotion-editor-${width}.png`});
|
||||||
|
await page.locator('#saveBoxButton').click();
|
||||||
|
const row=page.locator('[data-menu-item-v1762="ui-smoke-box"]');
|
||||||
|
await expect(row).toContainText('850 ₽');
|
||||||
|
await expect(row.locator('.ct-promotion-badge')).toHaveText('Акция');
|
||||||
|
await row.screenshot({path:`${output}/promotion-card-${width}.png`});
|
||||||
|
const beforeOrders=await page.evaluate(()=>localStorage.sunOrders);
|
||||||
|
const promo=await page.evaluate(()=>JSON.parse(localStorage.sunBoxes)[0]);
|
||||||
|
assert.equal(promo.price,1000);
|
||||||
|
assert.equal(Date.parse(promo.promotion.endsAt)-Date.parse(promo.promotion.startsAt),7*86400000);
|
||||||
|
await page.clock.setFixedTime(new Date(promo.promotion.endsAt));
|
||||||
|
await page.evaluate(()=>window.dispatchEvent(new Event('focus')));
|
||||||
|
await expect(row).toContainText('1 000 ₽');
|
||||||
|
await expect(row.locator('.ct-promotion-badge')).toHaveCount(0);
|
||||||
|
assert.equal(await page.evaluate(()=>localStorage.sunOrders),beforeOrders);
|
||||||
|
results.push({width,quietLoading:true,automaticOpen:true,helpDialog:true,icon,clientSize,clients:24,percentPromotion:true,automaticExpiry:true,savedOrdersUnchanged:true});
|
||||||
|
console.log(`PASS published UI ${width}px: quiet loading, automatic open, Help dialog, compact clients and timed discount${icon?', native question-circle icon':''}`);
|
||||||
}finally{await context.close();}
|
}finally{await context.close();}
|
||||||
}
|
}
|
||||||
await fs.writeFile(`${output}/result.json`,JSON.stringify({base:base.href,checkedAt:new Date().toISOString(),backend:'mocked and network-blocked',results},null,2));
|
await fs.writeFile(`${output}/result.json`,JSON.stringify({base:base.href,checkedAt:new Date().toISOString(),backend:'mocked and network-blocked',results},null,2));
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user