fix: always show the per-guest price in client banquet menu

Per-item prices next to each dish were already removed; the
per-guest price footer is no longer an optional checkbox — it now
always renders, since it should never be hidden, only the per-dish
prices were meant to be removed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
pavlov346346-source 2026-09-22 18:00:52 +03:00
parent ab2bc5f47e
commit 3aa268907e
3 changed files with 14 additions and 15 deletions

View File

@ -99,7 +99,7 @@
} }
throw new Error('Меню не помещается на одну страницу без слишком мелкого текста. Сократите названия или количество выбранных блюд. Ни одно блюдо не было обрезано.'); throw new Error('Меню не помещается на одну страницу без слишком мелкого текста. Сократите названия или количество выбранных блюд. Ни одно блюдо не было обрезано.');
} }
async function render(s,{showPrices=true,themeId='gold'}={}){ async function render(s,{themeId='gold'}={}){
const theme=THEMES[themeId]||THEMES.gold; const theme=THEMES[themeId]||THEMES.gold;
await fonts();const [pdf,logo]=await Promise.all([engine(),logoImage(s.brand.logo)]); await fonts();const [pdf,logo]=await Promise.all([engine(),logoImage(s.brand.logo)]);
const canvas=document.createElement('canvas');canvas.width=W*2;canvas.height=H*2; const canvas=document.createElement('canvas');canvas.width=W*2;canvas.height=H*2;
@ -119,7 +119,7 @@
y+=text(facts,M,y,W-M*2,`600 16px ${SANS}`,gold,23,centered?'center':'left')+24; y+=text(facts,M,y,W-M*2,`600 16px ${SANS}`,gold,23,centered?'center':'left')+24;
line(M,y,W-M);const top=y+20; line(M,y,W-M);const top=y+20;
c.font=`13px ${SANS}`;const contacts=wrap(c,s.brand.contacts,W-M*2),contactHeight=contacts.length*19; c.font=`13px ${SANS}`;const contacts=wrap(c,s.brand.contacts,W-M*2),contactHeight=contacts.length*19;
const footerHeight=(showPrices?108:62)+contactHeight+(s.estimated?22:0),bottom=H-64-footerHeight; const footerHeight=108+contactHeight+(s.estimated?22:0),bottom=H-64-footerHeight;
const layout=plan(c,s,top,bottom); const layout=plan(c,s,top,bottom);
for(let j=0;j<layout.columns.length;j++){ for(let j=0;j<layout.columns.length;j++){
const col=layout.columns[j],x=M+j*(layout.width+42);let yy=top; const col=layout.columns[j],x=M+j*(layout.width+42);let yy=top;
@ -133,12 +133,11 @@
let fy=bottom+22;line(M,fy,W-M,gold);fy+=20; let fy=bottom+22;line(M,fy,W-M,gold);fy+=20;
// Only the per-guest price is ever shown to the client — never a per-item price // Only the per-guest price is ever shown to the client — never a per-item price
// and never the grand total, so the menu can't be read as a full price list. // and never the grand total, so the menu can't be read as a full price list.
if(showPrices){ // It is not optional: the client always sees what one guest costs.
text('ЦЕНА НА ОДНОГО ГОСТЯ',M,fy,W-M*2,`700 12px ${SANS}`,muted,17,centered?'center':'left');fy+=24; text('ЦЕНА НА ОДНОГО ГОСТЯ',M,fy,W-M*2,`700 12px ${SANS}`,muted,17,centered?'center':'left');fy+=24;
let size=34;c.font=`600 ${size}px ${SERIF}`;const v=money(s.cents/100);while(size>16&&c.measureText(v).width>W-M*2)c.font=`600 ${--size}px ${SERIF}`; let size=34;c.font=`600 ${size}px ${SERIF}`;const v=money(s.cents/100);while(size>16&&c.measureText(v).width>W-M*2)c.font=`600 ${--size}px ${SERIF}`;
text(v,M,fy,W-M*2,c.font,ink,size+4,centered?'center':'left');fy+=size+16; text(v,M,fy,W-M*2,c.font,ink,size+4,centered?'center':'left');fy+=size+16;
text('За гостя, только выбранные блюда. Доставка и услуги не включены.',M,fy,W-M*2,`12px ${SANS}`,muted,18,centered?'center':'left');fy+=24; text('За гостя, только выбранные блюда. Доставка и услуги не включены.',M,fy,W-M*2,`12px ${SANS}`,muted,18,centered?'center':'left');fy+=24;
}else if(s.grams){text(`Выход по указанным порциям: ${s.grams.toLocaleString('ru-RU')} г на гостя`,M,fy,W-M*2,`14px ${SANS}`,muted,20,centered?'center':'left');fy+=26;}
if(s.estimated){text('≈ Вес и стоимость предварительные — уточняются при согласовании.',M,fy,W-M*2,`12px ${SANS}`,muted,18);fy+=22;} if(s.estimated){text('≈ Вес и стоимость предварительные — уточняются при согласовании.',M,fy,W-M*2,`12px ${SANS}`,muted,18);fy+=22;}
if(s.brand.contacts)text(s.brand.contacts,M,fy+5,W-M*2,`13px ${SANS}`,muted,19); if(s.brand.contacts)text(s.brand.contacts,M,fy+5,W-M*2,`13px ${SANS}`,muted,19);
const jpeg=await new Promise((resolve,reject)=>canvas.toBlob(b=>b?resolve(b):reject(new Error('Не удалось сформировать страницу меню.')),'image/jpeg',.96)); const jpeg=await new Promise((resolve,reject)=>canvas.toBlob(b=>b?resolve(b):reject(new Error('Не удалось сформировать страницу меню.')),'image/jpeg',.96));
@ -162,21 +161,21 @@
const s=snapshot(input),started=scope();close();style(); const s=snapshot(input),started=scope();close();style();
const dialog=document.createElement('dialog');dialog.id='ctBanquetClientDialog';dialog.setAttribute('aria-labelledby','ctBanquetClientTitle'); const dialog=document.createElement('dialog');dialog.id='ctBanquetClientDialog';dialog.setAttribute('aria-labelledby','ctBanquetClientTitle');
const themeOptions=themeIds.map(id=>`<option value="${id}">${THEMES[id].label}</option>`).join(''); const themeOptions=themeIds.map(id=>`<option value="${id}">${THEMES[id].label}</option>`).join('');
dialog.innerHTML=`<div class="ct-bcm-toolbar"><h2 id="ctBanquetClientTitle">Меню для клиента</h2><div class="ct-bcm-actions"><label>Оформление<select data-bcm-theme>${themeOptions}</select></label><label><input type="checkbox" data-bcm-prices checked>Показывать цену за гостя</label><button type="button" data-bcm-download disabled>Скачать PDF</button><button type="button" data-bcm-share hidden disabled>Поделиться</button><button type="button" data-bcm-close aria-label="Закрыть меню для клиента">Закрыть</button></div></div><p data-bcm-status role="status" aria-live="polite">Оформляю меню на одной странице…</p><div data-bcm-preview></div>`; dialog.innerHTML=`<div class="ct-bcm-toolbar"><h2 id="ctBanquetClientTitle">Меню для клиента</h2><div class="ct-bcm-actions"><label>Оформление<select data-bcm-theme>${themeOptions}</select></label><button type="button" data-bcm-download disabled>Скачать PDF</button><button type="button" data-bcm-share hidden disabled>Поделиться</button><button type="button" data-bcm-close aria-label="Закрыть меню для клиента">Закрыть</button></div></div><p data-bcm-status role="status" aria-live="polite">Оформляю меню на одной странице…</p><div data-bcm-preview></div>`;
const state={dialog,snapshot:s,generation:0,url:null,result:null,restore:document.activeElement};panel=state; const state={dialog,snapshot:s,generation:0,url:null,result:null,restore:document.activeElement};panel=state;
const get=q=>dialog.querySelector(q),status=get('[data-bcm-status]'),download=get('[data-bcm-download]'),share=get('[data-bcm-share]'),check=get('[data-bcm-prices]'),themeSelect=get('[data-bcm-theme]'); const get=q=>dialog.querySelector(q),status=get('[data-bcm-status]'),download=get('[data-bcm-download]'),share=get('[data-bcm-share]'),themeSelect=get('[data-bcm-theme]');
document.body.append(dialog);dialog.addEventListener('cancel',e=>{e.preventDefault();close();});get('[data-bcm-close]').onclick=close;dialog.showModal(); document.body.append(dialog);dialog.addEventListener('cancel',e=>{e.preventDefault();close();});get('[data-bcm-close]').onclick=close;dialog.showModal();
const valid=()=>panel===state&&scope()===started&&allowed(); const valid=()=>panel===state&&scope()===started&&allowed();
const build=async()=>{ const build=async()=>{
const ticket=++state.generation;download.disabled=true;share.disabled=true;state.result=null;status.textContent='Оформляю меню на одной странице…'; const ticket=++state.generation;download.disabled=true;share.disabled=true;state.result=null;status.textContent='Оформляю меню на одной странице…';
try{const result=await render(s,{showPrices:check.checked,themeId:themeSelect.value});if(!valid()||ticket!==state.generation)return; try{const result=await render(s,{themeId:themeSelect.value});if(!valid()||ticket!==state.generation)return;
if(state.url)URL.revokeObjectURL(state.url);state.url=URL.createObjectURL(result.jpeg);state.result=result; if(state.url)URL.revokeObjectURL(state.url);state.url=URL.createObjectURL(result.jpeg);state.result=result;
const img=document.createElement('img');img.src=state.url;img.alt='Банкетное меню: одна страница A4';get('[data-bcm-preview]').replaceChildren(img); const img=document.createElement('img');img.src=state.url;img.alt='Банкетное меню: одна страница A4';get('[data-bcm-preview]').replaceChildren(img);
status.textContent=`Готово: одна страница A4 · ${s.items.length} позиций. ${check.checked?'Показана только цена за гостя.':'Без указания цены.'}`;download.disabled=false; status.textContent=`Готово: одна страница A4 · ${s.items.length} позиций. Показана только цена за гостя.`;download.disabled=false;
const file=new File([result.blob],'Банкетное меню.pdf',{type:'application/pdf'});share.hidden=!(navigator.share&&navigator.canShare?.({files:[file]}));share.disabled=false; const file=new File([result.blob],'Банкетное меню.pdf',{type:'application/pdf'});share.hidden=!(navigator.share&&navigator.canShare?.({files:[file]}));share.disabled=false;
}catch(error){if(valid()&&ticket===state.generation){status.textContent=error.message;get('[data-bcm-preview]').replaceChildren();}} }catch(error){if(valid()&&ticket===state.generation){status.textContent=error.message;get('[data-bcm-preview]').replaceChildren();}}
}; };
check.onchange=build;themeSelect.onchange=build; themeSelect.onchange=build;
download.onclick=()=>{if(!valid()||!state.result)return;const url=URL.createObjectURL(state.result.blob),a=document.createElement('a');a.href=url;a.download=`Банкетное меню${s.date?' '+s.date:''}.pdf`;document.body.append(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(url),60000);}; download.onclick=()=>{if(!valid()||!state.result)return;const url=URL.createObjectURL(state.result.blob),a=document.createElement('a');a.href=url;a.download=`Банкетное меню${s.date?' '+s.date:''}.pdf`;document.body.append(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(url),60000);};
share.onclick=async()=>{if(!valid()||!state.result)return;try{await navigator.share({files:[new File([state.result.blob],'Банкетное меню.pdf',{type:'application/pdf'})],title:'Банкетное меню'});}catch(error){if(valid()&&error.name!=='AbortError')status.textContent='Не удалось открыть отправку. Сохраните PDF и отправьте его вручную.';}}; share.onclick=async()=>{if(!valid()||!state.result)return;try{await navigator.share({files:[new File([state.result.blob],'Банкетное меню.pdf',{type:'application/pdf'})],title:'Банкетное меню'});}catch(error){if(valid()&&error.name!=='AbortError')status.textContent='Не удалось открыть отправку. Сохраните PDF и отправьте его вручную.';}};
await build();return state.result; await build();return state.result;

View File

@ -48,7 +48,7 @@
if(!window.CateriumBanquetClientMenu)await new Promise((resolve,reject)=>{ if(!window.CateriumBanquetClientMenu)await new Promise((resolve,reject)=>{
const script=document.createElement('script');let timer; const script=document.createElement('script');let timer;
const fail=()=>{clearTimeout(timer);script.remove();reject(new Error('Не удалось загрузить оформление меню. Повторите попытку.'));}; const fail=()=>{clearTimeout(timer);script.remove();reject(new Error('Не удалось загрузить оформление меню. Повторите попытку.'));};
script.src='core/banquet-client-menu.js?v=20260922-no-item-prices';script.onload=()=>{clearTimeout(timer);window.CateriumBanquetClientMenu?resolve():fail();};script.onerror=fail;timer=setTimeout(fail,12000);document.head.append(script); script.src='core/banquet-client-menu.js?v=20260922-guest-price-always';script.onload=()=>{clearTimeout(timer);window.CateriumBanquetClientMenu?resolve():fail();};script.onerror=fail;timer=setTimeout(fail,12000);document.head.append(script);
}); });
if(!root.isConnected||who!==JSON.stringify([window.SunCloudV2?.getSession?.()?.user?.id,window.SunCloudV2?.getWorkspace?.()?.id]))return; if(!root.isConnected||who!==JSON.stringify([window.SunCloudV2?.getSession?.()?.user?.id,window.SunCloudV2?.getWorkspace?.()?.id]))return;
await window.CateriumBanquetClientMenu.open(input); await window.CateriumBanquetClientMenu.open(input);

View File

@ -4,7 +4,7 @@ const CORE=[
'./core/support-form.js?v=20260921-support-recipient', './core/support-form.js?v=20260921-support-recipient',
'./core/trial-promo-developer-v181.js?v=20260921-promo-entry', './core/trial-promo-developer-v181.js?v=20260921-promo-entry',
'./core/single-item-pdf.js?v=20260922-single-item-pdf-v1', './core/single-item-pdf.js?v=20260922-single-item-pdf-v1',
'./core/banquet-client-menu.js?v=20260922-no-item-prices', './core/banquet-client-menu.js?v=20260922-guest-price-always',
'./core/training-catalog.js?v=20260922-training-photos', './core/training-catalog.js?v=20260922-training-photos',
'./core/catalog-pricing.js?v=20260919-client-menu','./core/client-menu.css?v=20260919-client-menu', './core/catalog-pricing.js?v=20260919-client-menu','./core/client-menu.css?v=20260919-client-menu',
'./vendor/supabase-2.112.4.min.js', './vendor/supabase-2.112.4.min.js',