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>
87 lines
21 KiB
JavaScript
87 lines
21 KiB
JavaScript
(()=>{
|
||
'use strict';
|
||
const groups=[['cold','Холодные закуски'],['salads','Салаты'],['starters','Горячие закуски'],['main','Горячее'],['sides','Гарниры'],['desserts','Десерты'],['fruit','Фрукты и ягоды'],['bread','Хлеб и масло'],['other','Другие блюда']];
|
||
let packageId='all',groupId='all';
|
||
const esc=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||
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 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 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 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 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 apply(draft,catalog){const selected=selection(draft,catalog),rows=catalog.filter(i=>selected.has(String(i.id))),ids=new Set(catalog.map(i=>String(i.id))),count=guestCount(draft);if(!rows.length)return;const lines=rows.map(i=>({id:i.id,name:i.name,category:6,qty:count,price:unitPrice(i,draft)}));draft.lines=[...(draft.lines||[]).filter(l=>!ids.has(String(l.id))&&Number(l.category)!==6),...lines];draft.guestsCount=count;return lines}
|
||
function render({items,catalog,draft}){
|
||
const menus=packages(catalog);if(packageId!=='all'&&!menus.some(m=>m.id===packageId))packageId='all';
|
||
const candidates=items.filter(i=>packageId==='all'||i.banquet?.packageId===packageId),available=groups.filter(([id])=>candidates.some(i=>group(i)===id));
|
||
if(groupId!=='all'&&!available.some(([id])=>id===groupId))groupId='all';
|
||
const visible=candidates.filter(i=>groupId==='all'||group(i)===groupId),ids=selection(draft,catalog),selected=catalog.filter(i=>ids.has(String(i.id))),sum=totals(selected,draft),count=guestCount(draft);
|
||
const menu=menus.find(m=>m.id===packageId),sections=new Map();
|
||
for(const [id] of groups)for(const item of visible.filter(i=>group(i)===id)){const section=item.catalogSection||'Без раздела';if(!sections.has(section))sections.set(section,[]);sections.get(section).push(item)}
|
||
const estimates=catalog.some(i=>i.banquet?.estimated),hasExisting=(draft.lines||[]).some(l=>catalog.some(i=>String(i.id)===String(l.id))||Number(l.category)===6);
|
||
return `<div class="ct-banquet" aria-label="Конструктор банкетного меню">
|
||
<div class="ct-banquet-intro"><div><span class="ct-eyebrow">Банкетное меню</span><h2>Соберите меню для гостей</h2><p>Выберите готовое меню или отметьте отдельные блюда.</p></div><button class="outline" type="button" data-banquet-add>+ Добавить блюдо</button></div>
|
||
${menus.length?`<div class="ct-banquet-packages" aria-label="Пакеты меню"><button type="button" data-banquet-package="all" aria-pressed="${packageId==='all'}"><b>Все блюда</b><small>${catalog.length} позиций</small></button>${menus.map(m=>`<button type="button" data-banquet-package="${esc(m.id)}" aria-pressed="${packageId===m.id}"><b>${esc(m.name)}</b><span>${m.price?money(m.price):'Своя подборка'}</span><small>${m.grams?`${Number(m.grams).toLocaleString('ru-RU')} г · `:''}на 1 гостя</small></button>`).join('')}</div>`:''}
|
||
${catalog.some(i=>i.demo&&i.ttk)?'<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>`:''}
|
||
<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)}${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><button class="outline" type="button" data-banquet-client-menu ${selected.length?'':'disabled'}>Меню для клиента</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){
|
||
const {catalog,draft,rerender}=context;
|
||
root.querySelectorAll('[data-banquet-package]').forEach(b=>b.onclick=()=>{packageId=b.dataset.banquetPackage;groupId='all';rerender()});
|
||
root.querySelectorAll('[data-banquet-group]').forEach(b=>b.onclick=()=>{groupId=b.dataset.banquetGroup;rerender()});
|
||
root.querySelectorAll('[data-banquet-item]').forEach(b=>b.onchange=()=>{toggle(draft,catalog,b.dataset.banquetItem);rerender()});
|
||
root.querySelectorAll('[data-banquet-ttk]').forEach(b=>b.onclick=()=>window.CateriumTrialDemo?.showTTK(b.dataset.banquetTtk));
|
||
root.querySelectorAll('[data-banquet-edit]').forEach(b=>b.onclick=()=>window.editBox?.(b.dataset.banquetEdit));
|
||
const clientMenu=root.querySelector('[data-banquet-client-menu]');
|
||
clientMenu.onclick=async()=>{
|
||
const who=JSON.stringify([window.SunCloudV2?.getSession?.()?.user?.id,window.SunCloudV2?.getWorkspace?.()?.id]);
|
||
const input=JSON.parse(JSON.stringify({catalog,draft,event:document.getElementById('event')?.value??draft.event,date:document.getElementById('date')?.value??draft.date}));
|
||
clientMenu.disabled=true;
|
||
try{
|
||
if(!window.CateriumBanquetClientMenu)await new Promise((resolve,reject)=>{
|
||
const script=document.createElement('script');let timer;
|
||
const fail=()=>{clearTimeout(timer);script.remove();reject(new Error('Не удалось загрузить оформление меню. Повторите попытку.'));};
|
||
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;
|
||
await window.CateriumBanquetClientMenu.open(input);
|
||
}catch(error){if(root.isConnected)window.alert(error.message);}
|
||
finally{if(clientMenu.isConnected)clientMenu.disabled=false;}
|
||
};
|
||
const preset=root.querySelector('[data-banquet-preset]');if(preset)preset.onclick=()=>{chooseMenu(draft,catalog,preset.dataset.banquetPreset);rerender()};
|
||
const clear=root.querySelector('[data-banquet-clear]');if(clear)clear.onclick=()=>{draft.banquetSelection=[];rerender()};
|
||
root.querySelector('[data-banquet-add]').onclick=()=>window.editBox?.(null);
|
||
const guests=root.querySelector('[data-banquet-guests]');guests.oninput=()=>{draft.guestsCount=Math.min(10000,Math.max(1,Math.round(Number(guests.value)||1)));const field=document.getElementById('eventGuests');if(field)field.value=draft.guestsCount;const ids=selection(draft,catalog),sum=totals(catalog.filter(i=>ids.has(String(i.id))),draft);root.querySelector('[data-banquet-total]').textContent=money(sum.price*draft.guestsCount);root.querySelector('[data-banquet-total-weight]').textContent=(sum.grams*draft.guestsCount/1000).toLocaleString('ru-RU',{maximumFractionDigits:2})+' кг'};guests.onchange=()=>{guests.value=guestCount(draft)};
|
||
root.querySelector('[data-banquet-apply]').onclick=()=>{if(window.SunAdminRBACV3&&!window.SunAdminRBACV3.hasPermission(draft.id?'orders.edit':'orders.create'))return;apply(draft,catalog);const field=document.getElementById('eventGuests');if(field)field.value=draft.guestsCount;rerender();window.SunEnterprise?.toast?.('Банкетное меню добавлено в заказ.','success')};
|
||
}
|
||
function editor(category,item,catalog){
|
||
const section=document.getElementById('sunBanquetSectionLabel');if(!section)return;
|
||
let extra=document.getElementById('ctBanquetEditor');
|
||
if(!extra){extra=document.createElement('div');extra.id='ctBanquetEditor';extra.innerHTML='<label>Пакет меню<input id="ctBanquetPackage" list="ctBanquetPackageNames" placeholder="Например, Стандарт"><datalist id="ctBanquetPackageNames"></datalist></label><label class="ct-banquet-estimate-check"><input type="checkbox" id="ctBanquetEstimated"> Вес и цена предварительные</label>';section.insertAdjacentElement('afterend',extra)}
|
||
extra.hidden=Number(category)!==6;if(extra.hidden)return;
|
||
document.getElementById('ctBanquetPackage').value=item?.banquet?.packageName||'';
|
||
document.getElementById('ctBanquetEstimated').checked=item?.banquet?.estimated===true;
|
||
document.getElementById('ctBanquetPackageNames').innerHTML=packages(catalog).map(m=>`<option value="${esc(m.name)}"></option>`).join('');
|
||
let list=document.getElementById('ctBanquetSections');if(!list){list=document.createElement('datalist');list.id='ctBanquetSections';list.innerHTML=['Рыбные закуски','Мясные закуски','Сыры','Овощные закуски','Салаты','Горячие закуски','Горячие блюда','Гарниры','Десерты','Фрукты и ягоды','Хлеб и масло'].map(s=>`<option value="${s}"></option>`).join('');extra.appendChild(list)}
|
||
document.getElementById('banquetSection')?.setAttribute('list',list.id);
|
||
}
|
||
function saveEditor(item,catalog){if(Number(item.category)!==6)return;const input=document.getElementById('ctBanquetPackage');if(!input)return;const name=input.value.trim(),known=packages(catalog).find(m=>m.name===name),same=item.banquet?.packageName===name;item.banquet={...(same?item.banquet:{}),packageId:name?(known?.id||'custom-'+name.toLowerCase().replace(/\s+/g,'-')):'',packageName:name,estimated:document.getElementById('ctBanquetEstimated')?.checked===true};if(known){item.banquet.packagePrice=known.price;item.banquet.packageGrams=known.grams}}
|
||
const style=document.createElement('style');style.textContent=`
|
||
.ct-banquet-summary [data-banquet-client-menu]{width:100%;margin-top:10px;font-size:12px;min-height:42px}
|
||
#tiles .ct-banquet{grid-column:1/-1;min-width:0;width:100%;color:#34352e}.ct-banquet-intro{display:flex;justify-content:space-between;align-items:center;gap:12px;margin:6px 0 20px}.ct-eyebrow{font-size:10px;font-weight:800;letter-spacing:.1em;text-transform:uppercase;color:#8b775c}.ct-banquet h2{font-size:24px;margin:6px 0}.ct-banquet-intro p{font-size:12px;color:#7a7d75;margin:0}.ct-banquet-intro button{white-space:nowrap;font-size:12px}.ct-banquet-packages{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px}.ct-banquet-packages button{display:flex;flex-direction:column;align-items:flex-start;gap:6px;padding:14px 12px;border:1px solid #e3e1d9;border-radius:12px;background:#fff;color:#3e4238;text-align:left;min-width:0}.ct-banquet-packages b{font-size:14px}.ct-banquet-packages span{font-size:20px;font-weight:700}.ct-banquet-packages small{font-size:11px;color:#777c70}.ct-banquet-packages button[aria-pressed=true]{background:#edf1e7;border:1.5px solid #667956;box-shadow:0 3px 12px #3344250a}.ct-banquet-estimate{font-size:11px;line-height:1.5;color:#81735f;margin:12px 0 18px}.ct-banquet-preset{display:flex;gap:10px;align-items:center;justify-content:space-between;background:#f6f4ed;border-radius:10px;padding:10px 12px;font-size:12px;margin:14px 0}.ct-banquet-preset button{font-size:11px;white-space:nowrap}.ct-banquet-groups{display:flex;flex-wrap:wrap;gap:7px;margin:16px 0}.ct-banquet-groups button{border:1px solid #e5e4df;border-radius:20px;padding:8px 10px;background:white;color:#707467;font-size:11px;font-weight:600}.ct-banquet-groups button[aria-pressed=true]{background:#3c4933;border-color:#3c4933;color:white}.ct-banquet-groups span{opacity:.65;margin-left:4px}.ct-banquet-layout{display:grid;grid-template-columns:minmax(0,1fr);gap:16px;align-items:start}.ct-banquet-dishes{min-width:0}.ct-banquet-section{margin-bottom:20px}.ct-banquet-section h3{font-size:12px;color:#7b806e;font-weight:700;letter-spacing:.025em;margin:0 0 8px;padding-top:8px}.ct-banquet-section h3 small{display:block;font-size:10px;font-weight:400;letter-spacing:0;margin-top:4px}.ct-banquet-dish{display:flex;align-items:center;border:1px solid #ecebe6;border-radius:10px;margin:6px 0;padding:10px 8px;background:#fff;gap:3px}.ct-banquet-dish.selected{border-color:#b0bd9e;background:#f6f8f1}.ct-banquet-dish label{display:grid;grid-template-columns:17px minmax(0,1fr) 46px 67px;align-items:center;gap:9px;flex:1;min-width:0;cursor:pointer;font-size:12px;color:#3e4439}.ct-banquet-dish input{accent-color:#65794c;width:16px;height:16px;margin:0}.ct-banquet-name{line-height:1.4;font-weight:600;overflow-wrap:anywhere}.ct-banquet-name small{display:block;color:#9a9d90;font-size:10px;font-weight:400;margin-top:3px}.ct-banquet-weight{font-size:11px;color:#8a8e82;text-align:right;white-space:nowrap}.ct-banquet-price{text-align:right;white-space:nowrap;font-size:12px}.ct-banquet-dish>button{border:0;background:none;color:#8c947f;padding:5px;font-size:14px}.ct-banquet-dish>[data-banquet-ttk]{font-size:10px;font-weight:700}.ct-banquet-summary{border:1px solid #e4e4d9;border-radius:14px;background:#fcfcf8;padding:17px;min-width:0}.ct-banquet-summary h3{font-size:22px;margin:6px 0 18px}.ct-banquet-summary label{font-size:11px;gap:7px}.ct-banquet-summary input{width:100%;border-radius:7px;font-size:15px;background:white}.ct-banquet-metric,.ct-banquet-total{display:grid;grid-template-columns:1fr auto;gap:7px;margin:18px 0}.ct-banquet-metric span,.ct-banquet-total span{font-size:12px;color:#767e6a}.ct-banquet-metric small,.ct-banquet-total small{grid-column:2;text-align:right;font-size:12px;color:#8c927f}.ct-banquet-total{border-top:1px solid #e3e4d9;padding-top:14px}.ct-banquet-total b{font-size:19px}.ct-banquet-summary .primary{width:100%;font-size:12px;background:#43543a;border-color:#43543a;line-height:1.4}.ct-banquet-summary .primary:disabled{opacity:.4;cursor:default}.ct-banquet-help{display:block;font-size:11px;line-height:1.5;color:#919581;margin-top:10px}.ct-banquet-summary details{font-size:11px;line-height:1.5;margin-top:15px}.ct-banquet-summary li{margin:6px 0}.ct-banquet-summary ul{padding-left:16px}.ct-banquet-clear{border:0;background:none;color:#92977f;font-size:11px;padding:12px 0 0}.ct-banquet-estimate-check{flex-direction:row!important;align-items:center;gap:8px;margin-top:12px}.ct-banquet-estimate-check input{width:16px;height:16px}.ct-banquet-summary:focus-within{outline:none}
|
||
@container (min-width:760px){.ct-banquet-layout{grid-template-columns:minmax(0,1fr) 230px}.ct-banquet-summary{position:sticky;top:12px}}
|
||
#new.sun-banquet-active .catalog{container-type:inline-size;width:100%;min-width:0}
|
||
#new.sun-banquet-active #sunCatalogSearchV1775,#new.sun-banquet-active .sun-catalog-view-switch{display:none!important}
|
||
@media(max-width:600px){.ct-banquet-intro{align-items:flex-start;flex-direction:column}.ct-banquet h2{font-size:21px}.ct-banquet-packages{grid-template-columns:repeat(2,minmax(0,1fr))}.ct-banquet-dish label{grid-template-columns:16px minmax(0,1fr) 67px;gap:7px}.ct-banquet-name{grid-column:2/4}.ct-banquet-dish input{grid-row:1/3}.ct-banquet-weight{grid-column:2;text-align:left}.ct-banquet-price{grid-column:3}.ct-banquet-preset{align-items:flex-start;flex-direction:column}}
|
||
`;document.head.appendChild(style);
|
||
window.addEventListener('sun:cloud-tenant-changing',()=>{packageId='all';groupId='all'});
|
||
window.CateriumBanquet={render,bind,editor,saveEditor,packages,group,grams,totals,chooseMenu,toggle,apply};
|
||
})();
|