№${esc(o.id)} · ${esc(o.event||'Заказ')}${esc(o.contact||'Клиент не указан')} · ${esc(o.address||'Адрес не указан')}${esc(o.status||'Новый')}${paid?` · предоплата ${esc(money(paid))}`:''}
${esc(money(total))}
`}).join('')||'
Заказов нет.
'}
`;
const ui=modal('calendar-day',label,body);qa('[data-cal-open-v1762]',ui.host).forEach(b=>b.onclick=()=>{ui.close();const id=Number(b.dataset.calOpenV1762);if(typeof window.calendarOpenOrder==='function')window.calendarOpenOrder(id);else window.openOrder?.(id);});
}
// ---------- Full-page Menu editor ----------
let menuCategory=0,menuQuery='',menuActive=false,menuButton=null,menuView=null,editorHome=null,editorDialog=null;
let originalModal=null,originalCloseModal=null,originalSaveBox=null,originalRemoveBox=null;
function menuCanEdit(){if(support())return false;const c=cloud();if(!c?.getSession?.()?.user)return true;return can('catalog.edit')}
function currentCategory(){return CATEGORIES.find(x=>x.id===menuCategory)||CATEGORIES[0]}
function menuItems(){const q=menuQuery.trim().toLowerCase();return boxes().filter(x=>x.hidden!==true&&Number(x.category||0)===menuCategory&&(!q||[x.name,x.weight,x.catalogSection,...(x.composition||[])].join(' ').toLowerCase().includes(q)))}
function restoreEditorDialog(){if(editorHome&&editorDialog&&editorDialog.parentNode!==editorHome){editorHome.appendChild(editorDialog);editorDialog.classList.remove('sun-menu-editor-docked');}editorHome?.classList.remove('on');}
function emptyMenuDetail(text='Выберите позицию слева, чтобы открыть карточку.'){
const pane=$('sunMenuDetailV1762');if(!pane)return;restoreEditorDialog();pane.innerHTML=`
${esc(text)}${!menuCanEdit()?'
только просмотр
':''}
`;
}
function showReadOnlyMenuItem(item){
restoreEditorDialog();const pane=$('sunMenuDetailV1762');if(!pane)return;
const comp=Array.isArray(item.composition)?item.composition.filter(Boolean):[],ingredients=Array.isArray(item.ingredients)?item.ingredients:[];
pane.innerHTML=`
`;
}
function dockEditor(){
const pane=$('sunMenuDetailV1762'),home=$('editor'),dialog=home?.querySelector('.dialog')||editorDialog;if(!pane||!home||!dialog)return;
editorHome=home;editorDialog=dialog;home.classList.remove('on');pane.innerHTML='';pane.classList.add('sun-menu-editor-pane');pane.appendChild(dialog);dialog.classList.add('sun-menu-editor-docked');
}
function renderMenuList(){
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));
const items=menuItems();if(count)count.textContent=`${items.length} поз.`;
list.innerHTML=items.length?items.map(item=>``).join(''):'
В этом разделе ничего не найдено.
';
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(){
if(originalModal)return;
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 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 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 openMenu(){
if(!menuView)return;menuActive=true;patchMenuLegacyEditor();qa('.view').forEach(v=>v.classList.remove('on'));menuView.classList.add('on');qa('header nav button').forEach(b=>b.classList.remove('on'));menuButton?.classList.add('on');renderMenuList();emptyMenuDetail();window.scrollTo({top:0,behavior:'instant'});
}
function installMenuPage(){
if($('sun-menu-editor-v1762'))return;
const nav=document.querySelector('header nav');if(!nav)return;
const group=nav.querySelector('.sun-nav-operations')||nav;
menuButton=document.createElement('button');menuButton.type='button';menuButton.textContent='Меню';menuButton.dataset.navLabel='Меню';menuButton.className='sun-nav-button nav-menu';group.appendChild(menuButton);
menuView=document.createElement('section');menuView.id='sun-menu-editor-v1762';menuView.className='view page';menuView.innerHTML=`
Редактор меню
Полноэкранное управление каталогом без всплывающих окон.
${CATEGORIES.map(c=>``).join('')}
`;document.body.appendChild(menuView);
menuButton.onclick=openMenu;
qa('[data-menu-cat-v1762]',menuView).forEach(b=>b.onclick=()=>{menuCategory=Number(b.dataset.menuCatV1762);menuQuery='';$('sunMenuSearchV1762').value='';emptyMenuDetail();renderMenuList();});
$('sunMenuSearchV1762').oninput=e=>{menuQuery=e.target.value||'';renderMenuList()};
$('sunMenuAddV1762').onclick=()=>{if(!menuCanEdit())return toast('В этом режиме каталог доступен только для просмотра.','warn');window.editBox?.(null);setTimeout(()=>{const select=$('itemCategory');if(select){select.value=String(menuCategory);select.dispatchEvent(new Event('change',{bubbles:true}))}dockEditor();},0)};
document.addEventListener('click',e=>{const b=e.target.closest('header nav button');if(b&&b!==menuButton)leaveMenu();},true);
const setupOldButton=()=>{const old=qa('#new .catalog-head button').find(b=>/настроить|редактор/i.test(b.textContent||''));if(!old)return false;if(!old.__sunV1762){old.__sunV1762=true;old.textContent='Меню';old.onclick=e=>{e.preventDefault();e.stopPropagation();openMenu();};}return true;};
if(!setupOldButton()&&!oldButtonTimer)oldButtonTimer=setInterval(()=>{if(setupOldButton()){clearInterval(oldButtonTimer);oldButtonTimer=0;}},4000);
syncMenuPermission();
}
function syncMenuPermission(){if(!menuButton)return;const c=cloud();const signed=Boolean(c?.getSession?.()?.user);const allowed=!signed||support()||can('catalog.view')||can('catalog.edit');menuButton.hidden=!allowed;menuButton.style.display=allowed?'':'none';const add=$('sunMenuAddV1762');if(add){add.disabled=!menuCanEdit();add.title=menuCanEdit()?'':'Только просмотр';}}
// ---------- Route start + order popup ----------
function routeKey(){return `${$('routeDate')?.value||''}|${$('routeCourier')?.value||'all'}`}
function routeBase(){const x=readJson(ROUTE_BASE_KEY,null);return x&&x.address?x:ROUTE_DEFAULT}
function routeOverrides(){const x=readJson(ROUTE_OVERRIDE_KEY,{});return x&&typeof x==='object'&&!Array.isArray(x)?x:{}}
function effectiveRouteStart(){const over=routeOverrides()[routeKey()];return over?.address?over:routeBase()}
function routeOrders(){
const date=$('routeDate')?.value||'',courier=$('routeCourier')?.value||'';let list=orders().filter(o=>!['Отменён','Отдан заказчику'].includes(o.status||'Новый')&&o.date===date&&(o.address||validGeo(o.geo)));
if(courier)list=list.filter(o=>(o.staffIds||[]).map(String).includes(String(courier)));
const plans=readJson('sunRoutePlansV1',{}),ids=plans[`${date}|${courier||'all'}`]||[],map=new Map(list.map(o=>[String(o.id),o])),ordered=ids.map(id=>map.get(String(id))).filter(Boolean);list.filter(o=>!ids.map(String).includes(String(o.id))).sort((a,b)=>String(a.time||'').localeCompare(String(b.time||''))).forEach(o=>ordered.push(o));return ordered;
}
function hav(a,b){const R=6371,rad=x=>x*Math.PI/180,dLat=rad(Number(b.lat)-Number(a.lat)),dLon=rad(Number(b.lon)-Number(a.lon)),la1=rad(Number(a.lat)),la2=rad(Number(b.lat));const h=Math.sin(dLat/2)**2+Math.cos(la1)*Math.cos(la2)*Math.sin(dLon/2)**2;return 2*R*Math.asin(Math.sqrt(h))}
function routeDistance(list,start){const pts=[];if(validGeo(start))pts.push(start);list.filter(o=>validGeo(o.geo)).forEach(o=>pts.push(o.geo));let total=0;for(let i=1;ipts.push(validGeo(o.geo)?`${Number(o.geo.lat)},${Number(o.geo.lon)}`:String(o.address||'')));return `https://yandex.ru/maps/?rtext=${encodeURIComponent(pts.filter(Boolean).join('~'))}&rtt=auto`;}
async function geocodeRouteBase(address){
const clean=String(address||'').trim();if(!clean)throw new Error('Введите адрес начала маршрута.');
const query=/санкт[- ]?петербург|\bспб\b|москва|ленинградск|область|край|республика|телези|гатчин|пушкин|петергоф|ломоносов/i.test(clean)?clean:`Санкт-Петербург, ${clean}`;
const request=window.sunFetchWithTimeout||((url,opts)=>fetch(url,opts));
let row=null,provider='nominatim';
try{const url=`https://nominatim.openstreetmap.org/search?format=jsonv2&limit=5&countrycodes=ru&accept-language=ru&q=${encodeURIComponent(query)}`;const res=await request(url,{method:'GET',mode:'cors',credentials:'omit',cache:'default'},6000);if(res.ok){const rows=await res.json();row=Array.isArray(rows)?rows[0]:null;}}catch(_){row=null}
if(!row||!Number.isFinite(Number(row.lon))||!Number.isFinite(Number(row.lat))){try{const url=`https://photon.komoot.io/api/?limit=1&lang=ru&q=${encodeURIComponent(query)}`;const res=await request(url,{method:'GET',mode:'cors',credentials:'omit',cache:'default'},6000);if(res.ok){const data=await res.json(),f=data?.features?.[0],c=f?.geometry?.coordinates,p=f?.properties||{};if(Array.isArray(c)&&Number.isFinite(Number(c[0]))&&Number.isFinite(Number(c[1]))){row={lon:Number(c[0]),lat:Number(c[1]),display_name:[p.name,p.street,p.housenumber,p.city,p.state].filter(Boolean).join(', ')||query};provider='photon';}}}catch(_){row=null}}
if(!row||!Number.isFinite(Number(row.lon))||!Number.isFinite(Number(row.lat)))throw new Error('Не удалось найти адрес начала маршрута. Проверьте адрес и интернет.');
return {address:clean,lat:Number(row.lat),lon:Number(row.lon),formatted:String(row.display_name||clean),verifiedAt:new Date().toISOString(),provider};
}
async function saveRouteStart(asDefault=false){
if(support())return toast('В режиме разработчика данные компании доступны только для просмотра.','warn');
const input=$('sunRouteStartAddressV1762'),state=$('sunRouteStartStateV1762');if(!input)return;
const text=input.value.trim();if(!text)return toast('Введите адрес начала маршрута.','warn');
if(state)state.textContent='Ищу адрес на карте…';
try{
const geo=await geocodeRouteBase(text);
if(asDefault){writeJson(ROUTE_BASE_KEY,geo);const over=routeOverrides();delete over[routeKey()];writeJson(ROUTE_OVERRIDE_KEY,over);toast('Основная база маршрутов сохранена.','success');}
else{const over=routeOverrides();over[routeKey()]=geo;writeJson(ROUTE_OVERRIDE_KEY,over);toast('Начало маршрута сохранено для выбранной даты и курьера.','success');}
enhanceRoutePage();
}catch(error){if(state)state.textContent=error?.message||String(error);toast(error?.message||String(error),'error',6000)}
}
function resetRouteStart(){if(support())return toast('В режиме разработчика данные компании доступны только для просмотра.','warn');const over=routeOverrides();delete over[routeKey()];writeJson(ROUTE_OVERRIDE_KEY,over);enhanceRoutePage();toast('Для маршрута снова используется основная база.','success')}
function enhanceRoutePage(){
const view=$('sun-routes-view'),side=view?.querySelector('.route-side');if(!view?.classList.contains('on')||!side)return;
const start=effectiveRouteStart(),base=routeBase(),isOverride=Boolean(routeOverrides()[routeKey()]);
let box=$('sunRouteStartEditorV1762');if(!box){box=document.createElement('div');box.id='sunRouteStartEditorV1762';box.className='sun-route-start-editor';const h=side.querySelector('h2');h?.insertAdjacentElement('afterend',box);}
box.innerHTML=`
${isOverride?'Старт этого маршрута':'Основная база'} ${esc(start.address||'—')}
${isOverride?'':''}
${isOverride?`Основная база: ${esc(base.address||'—')}`:'Можно задать другой старт только для выбранной даты/курьера.'}
`;
const readOnly=Boolean(support());const addrInput=$('sunRouteStartAddressV1762');if(addrInput)addrInput.disabled=readOnly;const apply=$('sunRouteStartApplyV1762'),def=$('sunRouteStartDefaultV1762'),reset=$('sunRouteStartResetV1762');if(apply){apply.disabled=readOnly;apply.onclick=()=>saveRouteStart(false)}if(def){def.disabled=readOnly;def.onclick=()=>saveRouteStart(true)}if(reset){reset.disabled=readOnly;reset.addEventListener('click',resetRouteStart)}if(readOnly&&$('sunRouteStartStateV1762'))$('sunRouteStartStateV1762').textContent='Режим разработчика: точка старта доступна только для просмотра.';
qa(':scope > div',side).filter(x=>x!==box&&/Телези,\s*20|Постоянная точка старта/i.test(x.textContent||'')).forEach(x=>x.style.display='none');
qa(':scope > p.hint',side).filter(x=>/Телези,\s*20/i.test(x.textContent||'')).forEach(x=>x.textContent='Маршрут и печать используют выбранную выше точку старта.');
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 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 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 body=`
Дата и время${esc(fmtDate(o.date))} · ${esc(o.time||'—')}