caterium-app/public/core/ops-ux-v1762.js
pavlov346346-source 5b2926c36b
Caterium v17.6.2 — operations UX
Add read-only developer support refresh, calendar overflow panel, full-page menu editor, configurable route start, and in-route order popup.
2026-09-07 18:53:23 +03:00

260 lines
40 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

(()=>{
'use strict';
if(window.SunOpsUXV1762)return;
const VERSION='17.6.2';
const RELEASE='20260907-v17-6-2-ops-ux';
const SUPPORT_POLL_MS=12000;
const ROUTE_BASE_KEY='sunRouteBaseV1';
const ROUTE_OVERRIDE_KEY='sunRouteStartOverridesV1';
const ROUTE_DEFAULT={address:'Ленинградская область, Ломоносовский район, деревня Телези, 20',lat:59.701058,lon:29.977269};
const CATEGORIES=[
{id:0,label:'Боксы'},
{id:5,label:'Премиум боксы'},
{id:1,label:'Посуда и упаковка'},
{id:2,label:'Дополнительно'},
{id:3,label:'Напитки'},
{id:4,label:'Доставка'}
];
const $=id=>document.getElementById(id);
const qa=(s,r=document)=>[...r.querySelectorAll(s)];
const esc=v=>window.SunSafe?.escapeHTML?window.SunSafe.escapeHTML(String(v??'')):String(v??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
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 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 orders=()=>{const v=readJson('sunOrders',[]);return Array.isArray(v)?v:[]};
const boxes=()=>{const v=readJson('sunBoxes',[]);return Array.isArray(v)?v:[]};
const cloud=()=>window.SunCloudV2||null;
const support=()=>cloud()?.getSupportMode?.()||null;
const can=(perm)=>Boolean(cloud()?.hasPermission?.(perm));
const validGeo=x=>x&&Number.isFinite(Number(x.lat))&&Number.isFinite(Number(x.lon));
const fmtDate=raw=>{if(!raw)return'—';const d=new Date(`${raw}T12:00:00`);return Number.isNaN(d.getTime())?raw:d.toLocaleDateString('ru-RU')};
function installStyles(){
if($('sunOpsUXV1762Style'))return;
const style=document.createElement('style');style.id='sunOpsUXV1762Style';style.textContent=`
.cal-more{cursor:pointer;text-decoration:underline;text-underline-offset:2px;font-weight:700}.cal-more:hover{color:var(--n)}
.sun-v1762-modal{position:fixed;inset:0;z-index:100200;background:rgba(18,28,38,.55);display:grid;place-items:center;padding:18px}.sun-v1762-modal-card{width:min(900px,100%);max-height:92vh;overflow:auto;background:#fff;border-radius:18px;box-shadow:0 24px 80px rgba(0,0,0,.28);padding:20px}.sun-v1762-modal-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px}.sun-v1762-modal-head h2{margin:0;color:var(--n)}.sun-v1762-modal-close{border:0;background:#f1f3f4;border-radius:10px;width:38px;height:38px;font-size:23px}.sun-v1762-modal-actions{display:flex;gap:8px;flex-wrap:wrap;justify-content:flex-end;margin-top:16px}
.sun-cal-day-list{display:grid;gap:8px;margin-top:15px}.sun-cal-day-row{display:grid;grid-template-columns:72px minmax(0,1fr) 130px auto;gap:10px;align-items:center;border:1px solid #e2e5e8;border-radius:12px;padding:11px 12px}.sun-cal-day-time{font-weight:850;color:var(--n);font-size:16px}.sun-cal-day-row small{display:block;color:#727b83;margin-top:3px}.sun-cal-day-money{text-align:right;font-weight:800;color:var(--n)}
#sun-menu-editor-v1762{max-width:1500px;margin:18px auto}.sun-menu-head{display:flex;justify-content:space-between;align-items:flex-start;gap:12px;flex-wrap:wrap}.sun-menu-head h1{margin:0}.sun-menu-tabs{display:flex;gap:7px;overflow-x:auto;margin:16px 0 12px;padding-bottom:3px}.sun-menu-tabs button{white-space:nowrap;border:1px solid #d8dde1;background:#fff;border-radius:999px;padding:9px 13px;font-weight:800;color:#56626d}.sun-menu-tabs button.on{background:var(--n);border-color:var(--n);color:#fff}.sun-menu-toolbar{display:grid;grid-template-columns:minmax(220px,1fr) auto;gap:9px;margin-bottom:12px}.sun-menu-layout{display:grid;grid-template-columns:minmax(380px,.9fr) minmax(430px,1.1fr);gap:12px;align-items:start}.sun-menu-list-pane,.sun-menu-detail-pane{border:1px solid #e1e5e8;border-radius:15px;background:#fff;padding:13px;min-height:620px}.sun-menu-list{display:grid;gap:7px;max-height:72vh;overflow:auto;padding-right:3px}.sun-menu-row{width:100%;display:grid;grid-template-columns:72px minmax(0,1fr) auto;gap:10px;align-items:center;border:1px solid #e5e8eb;border-radius:12px;background:#fff;padding:8px;text-align:left}.sun-menu-row:hover{border-color:#c9a45c;background:#fffdf8}.sun-menu-row img,.sun-menu-row .ph{width:72px;height:58px;object-fit:cover;border-radius:8px;background:#eef1f3}.sun-menu-row b{display:block;color:#263744}.sun-menu-row small{display:block;color:#77818a;margin-top:3px;line-height:1.3}.sun-menu-row-price{text-align:right;font-weight:850;color:var(--n);white-space:nowrap}.sun-menu-empty{padding:34px;text-align:center;color:#7b848c}.sun-menu-detail-placeholder{display:grid;place-items:center;min-height:560px;text-align:center;color:#78838b}.sun-menu-readonly h2{margin-top:0;color:var(--n)}.sun-menu-readonly-photo{width:min(360px,100%);aspect-ratio:4/3;object-fit:cover;border-radius:13px;background:#eef1f2}.sun-menu-readonly-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin:14px 0}.sun-menu-readonly-box{background:#f7f8f9;border-radius:10px;padding:10px}.sun-menu-readonly-box small{display:block;color:#747d85}.sun-menu-readonly-box b{display:block;margin-top:4px}.sun-menu-readonly-list{margin:6px 0 0;padding-left:18px}.sun-menu-editor-pane>.dialog{display:block!important;width:100%!important;max-width:none!important;max-height:none!important;overflow:visible!important;border-radius:0!important;padding:0!important;box-shadow:none!important}.sun-menu-editor-pane>.dialog .dialog-head{position:sticky;top:0;background:#fff;z-index:2;padding-bottom:10px}.sun-menu-editor-pane>.dialog .dialog-head button{display:block}.sun-menu-readonly-badge{display:inline-flex;padding:5px 9px;border-radius:999px;background:#fff4da;color:#7b5812;font-size:11px;font-weight:850;margin-left:7px}
.sun-route-start-editor{border:1px solid #dfe4e8;border-radius:12px;padding:12px;background:#f8f9fb;margin-bottom:10px}.sun-route-start-editor label{margin-bottom:8px}.sun-route-start-actions{display:flex;gap:6px;flex-wrap:wrap}.sun-route-start-actions button{flex:1;min-width:120px}.sun-route-start-state{font-size:11px;color:#6f7981;margin-top:7px;min-height:15px}.sun-route-start-active{padding:8px 10px;border-radius:9px;background:#eef5ef;color:#355e43;margin-bottom:9px;font-size:12px}.sun-route-order-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:9px;margin:14px 0}.sun-route-order-kpi{background:#f7f8fa;border-radius:10px;padding:10px}.sun-route-order-kpi small{display:block;color:#737d86}.sun-route-order-kpi b{display:block;margin-top:5px;color:var(--n);font-size:16px}.sun-route-order-section{border-top:1px solid #e4e7ea;padding-top:12px;margin-top:12px}.sun-route-order-lines{width:100%;border-collapse:collapse}.sun-route-order-lines th,.sun-route-order-lines td{padding:8px;border-bottom:1px solid #e8ebed;text-align:left}.sun-route-order-lines th:nth-child(n+2),.sun-route-order-lines td:nth-child(n+2){text-align:right}.sun-route-order-note{padding:10px 12px;border-radius:10px;background:#fff7e8;margin-top:9px}.sun-route-print-v1762{display:none}
.sun-support-live-status{font-size:11px;color:#cdd7dc;white-space:nowrap}.sun-support-refresh{background:#eef2f3!important;color:#172c38!important}
@media(max-width:950px){.sun-menu-layout{grid-template-columns:1fr}.sun-menu-list-pane,.sun-menu-detail-pane{min-height:0}.sun-menu-list{max-height:none}.sun-menu-detail-placeholder{min-height:220px}.sun-route-order-grid{grid-template-columns:1fr 1fr}}
@media(max-width:650px){.sun-cal-day-row{grid-template-columns:55px minmax(0,1fr)}.sun-cal-day-money,.sun-cal-day-row>button{grid-column:2;text-align:left;justify-self:start}.sun-menu-toolbar{grid-template-columns:1fr}.sun-menu-row{grid-template-columns:58px minmax(0,1fr)}.sun-menu-row img,.sun-menu-row .ph{width:58px;height:52px}.sun-menu-row-price{grid-column:2;text-align:left}.sun-menu-readonly-grid{grid-template-columns:1fr}.sun-route-order-grid{grid-template-columns:1fr 1fr}}
@media print{body.sun-route-print-v1762 *{visibility:hidden!important}body.sun-route-print-v1762 #sunRoutePrintDocV1762,body.sun-route-print-v1762 #sunRoutePrintDocV1762 *{visibility:visible!important}body.sun-route-print-v1762 #sunRoutePrintDocV1762{display:block!important;position:absolute;left:0;top:0;width:100%;padding:8mm;background:#fff}.sun-route-print-table-v1762{width:100%;border-collapse:collapse}.sun-route-print-table-v1762 th,.sun-route-print-table-v1762 td{border:1px solid #ccd3d8;padding:6px;text-align:left}.sun-route-print-table-v1762 th{background:#f4f6f7}@page{size:A4 portrait;margin:8mm}}
`;document.head.appendChild(style);
}
function modal(id,title,body,actions=''){
$(`sunV1762Modal_${id}`)?.remove();
const host=document.createElement('div');host.id=`sunV1762Modal_${id}`;host.className='sun-v1762-modal';host.innerHTML=`<div class="sun-v1762-modal-card"><div class="sun-v1762-modal-head"><div><h2>${esc(title)}</h2></div><button class="sun-v1762-modal-close" type="button" data-v1762-close>×</button></div>${body}${actions?`<div class="sun-v1762-modal-actions">${actions}</div>`:''}</div>`;document.body.appendChild(host);
const close=()=>host.remove();host.addEventListener('click',e=>{if(e.target===host||e.target.closest('[data-v1762-close]'))close()});return {host,close};
}
// ---------- Developer support: full read-only visibility + live refresh ----------
let supportBusy=false,supportLastAt=0,supportTimer=0,originalHasPermission=null;
function supportReadPermission(permission){
const p=String(permission||'');
if(p==='app.read'||p.endsWith('.view'))return true;
return false;
}
function patchSupportPermissions(){
const c=cloud();if(!c||c.__sunV1762SupportPermissions)return false;
originalHasPermission=typeof c.hasPermission==='function'?c.hasPermission.bind(c):()=>false;
c.hasPermission=permission=>c.isSupportMode?.()?supportReadPermission(permission):originalHasPermission(permission);
Object.defineProperty(c,'__sunV1762SupportPermissions',{value:true,configurable:true});
return true;
}
function renderSupportStatus(){
const banner=$('sunDevSupportBannerV22'),sm=support();if(!banner||!sm)return;
let refresh=$('sunSupportRefreshV1762');if(!refresh){refresh=document.createElement('button');refresh.type='button';refresh.id='sunSupportRefreshV1762';refresh.className='sun-support-refresh';refresh.textContent='Обновить';const exit=qa('button',banner).find(b=>b!==refresh);banner.insertBefore(refresh,exit||null);refresh.onclick=()=>refreshSupport(true);}
let state=$('sunSupportLiveStatusV1762');if(!state){state=document.createElement('span');state.id='sunSupportLiveStatusV1762';state.className='sun-support-live-status';refresh.before(state);}
state.textContent=supportBusy?'обновляю…':supportLastAt?`live · ${new Date(supportLastAt).toLocaleTimeString('ru-RU',{hour:'2-digit',minute:'2-digit',second:'2-digit'})}`:'live';
}
async function refreshSupport(manual=false){
const c=cloud(),sm=support();if(!c||!sm||supportBusy||!navigator.onLine||document.hidden)return false;
supportBusy=true;renderSupportStatus();
try{
await c.enterSupportWorkspace(sm.workspaceId,sm.name||'Компания');
supportLastAt=Date.now();patchSupportPermissions();
try{window.dispatchEvent(new CustomEvent('sun:cloud-permissions-changed',{detail:{workspace:c.getWorkspace?.(),support:true,refresh:true}}));}catch(_){}
for(const name of ['render','renderOrders','renderStats','renderClients','sunRefreshStock']){try{window[name]?.()}catch(_){}}
if(manual)toast('Данные компании обновлены. Режим только для чтения сохранён.','success',3200);
return true;
}catch(error){if(manual)toast(error?.message||String(error),'error',6000);return false}
finally{supportBusy=false;renderSupportStatus();}
}
function maintainSupport(){
patchSupportPermissions();
const sm=support();
if(sm){renderSupportStatus();if(!supportTimer)supportTimer=setInterval(()=>refreshSupport(false),SUPPORT_POLL_MS);}
else if(supportTimer){clearInterval(supportTimer);supportTimer=0;supportLastAt=0;}
}
// ---------- Calendar +more day panel ----------
const MONTHS={'январь':0,'февраль':1,'март':2,'апрель':3,'май':4,'июнь':5,'июль':6,'август':7,'сентябрь':8,'октябрь':9,'ноябрь':10,'декабрь':11};
function calendarCellDate(cell){
const title=$('calendar')?.querySelector('.cal-period-title')?.textContent?.trim().toLowerCase()||'';
const match=title.match(/([а-яё]+)\s+(\d{4})/i);if(!match||MONTHS[match[1]]==null)return null;
const month=MONTHS[match[1]],year=Number(match[2]),cells=qa('#calendar .cal-day'),index=cells.indexOf(cell);if(index<0)return null;
const first=new Date(year,month,1),offset=(first.getDay()+6)%7,start=new Date(year,month,1-offset);start.setDate(start.getDate()+index);return start;
}
function orderTotal(order){
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]));
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);
}
function showCalendarDay(cell){
const date=calendarCellDate(cell);if(!date)return;
const key=`${date.getFullYear()}-${String(date.getMonth()+1).padStart(2,'0')}-${String(date.getDate()).padStart(2,'0')}`;
const list=orders().filter(o=>o.date===key).sort((a,b)=>String(a.time||'').localeCompare(String(b.time||'')));
const label=date.toLocaleDateString('ru-RU',{weekday:'long',day:'numeric',month:'long',year:'numeric'});
const body=`<div class="sun-cal-day-list">${list.map(o=>{const total=orderTotal(o),paid=Math.max(0,Number(o.prepayment||0));return `<div class="sun-cal-day-row"><div class="sun-cal-day-time">${esc(o.time||'—')}</div><div><b>№${esc(o.id)} · ${esc(o.event||'Заказ')}</b><small>${esc(o.contact||'Клиент не указан')} · ${esc(o.address||'Адрес не указан')}</small><small>${esc(o.status||'Новый')}${paid?` · предоплата ${esc(money(paid))}`:''}</small></div><div class="sun-cal-day-money">${esc(money(total))}</div><button class="outline" type="button" data-cal-open-v1762="${esc(o.id)}">Открыть</button></div>`}).join('')||'<div class="sun-menu-empty">Заказов нет.</div>'}</div>`;
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=>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=`<div class="sun-menu-detail-placeholder"><div><b>${esc(text)}</b>${!menuCanEdit()?'<div class="sun-menu-readonly-badge" style="margin-top:9px">только просмотр</div>':''}</div></div>`;
}
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=`<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>`;
}
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=>`<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>';
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=`<div class="sun-menu-head"><div><h1>Редактор меню</h1><p class="hint" style="margin:5px 0 0">Полноэкранное управление каталогом без всплывающих окон.</p></div><span id="sunMenuCountV1762" class="hint"></span></div><div class="sun-menu-tabs">${CATEGORIES.map(c=>`<button type="button" data-menu-cat-v1762="${c.id}" class="${c.id===menuCategory?'on':''}">${esc(c.label)}</button>`).join('')}</div><div class="sun-menu-toolbar"><input id="sunMenuSearchV1762" type="search" placeholder="Поиск по названию, составу или весу"><button class="primary" id="sunMenuAddV1762" type="button">Добавить позицию</button></div><div class="sun-menu-layout"><section class="sun-menu-list-pane"><div id="sunMenuListV1762" class="sun-menu-list"></div></section><section id="sunMenuDetailV1762" class="sun-menu-detail-pane"></section></div>`;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&&!old.__sunV1762){old.__sunV1762=true;old.textContent='Меню';old.onclick=e=>{e.preventDefault();e.stopPropagation();openMenu();};}};setupOldButton();setInterval(setupOldButton,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;i<pts.length;i++)total+=hav(pts[i-1],pts[i]);return total}
function yandexRouteUrl(list,start){const pts=[];pts.push(validGeo(start)?`${Number(start.lat)},${Number(start.lon)}`:String(start.address||''));list.forEach(o=>pts.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=`<div class="sun-route-start-active"><b>${isOverride?'Старт этого маршрута':'Основная база'}</b><br>${esc(start.address||'—')}</div><label>Адрес начала<input id="sunRouteStartAddressV1762" value="${esc(start.address||'')}"></label><div class="sun-route-start-actions"><button class="primary" id="sunRouteStartApplyV1762" type="button">Для этого маршрута</button><button class="outline" id="sunRouteStartDefaultV1762" type="button">Сделать основной</button>${isOverride?'<button class="outline" id="sunRouteStartResetV1762" type="button">Вернуть базу</button>':''}</div><div id="sunRouteStartStateV1762" class="sun-route-start-state">${isOverride?`Основная база: ${esc(base.address||'—')}`:'Можно задать другой старт только для выбранной даты/курьера.'}</div>`;
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=`<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 phone=String(o.phone||'').replace(/[^+\d]/g,'');const mapUrl=validGeo(o.geo)?`https://yandex.ru/maps/?ll=${encodeURIComponent(`${o.geo.lon},${o.geo.lat}`)}&z=16&pt=${encodeURIComponent(`${o.geo.lon},${o.geo.lat},pm2rdm`)}`:`https://yandex.ru/maps/?text=${encodeURIComponent(o.address||'')}`;
const actions=`${phone?`<button class="outline" type="button" data-route-call-v1762>Позвонить</button>`:''}<button class="outline" type="button" data-route-map-v1762>Открыть адрес</button>${editable?'<button class="primary" type="button" data-route-edit-v1762>Редактировать заказ</button>':''}<button class="outline" type="button" data-v1762-close>Закрыть</button>`;
const ui=modal('route-order',`Заказ №${o.id}`,body,actions);ui.host.querySelector('[data-route-call-v1762]')?.addEventListener('click',()=>{location.href=`tel:${phone}`});ui.host.querySelector('[data-route-map-v1762]')?.addEventListener('click',()=>window.open(mapUrl,'_blank','noopener'));ui.host.querySelector('[data-route-edit-v1762]')?.addEventListener('click',()=>{ui.close();window.openOrder?.(Number(o.id))});
}
async function copyCurrentRoute(){const list=routeOrders(),start=effectiveRouteStart(),courier=$('routeCourier')?.selectedOptions?.[0]?.textContent||'Все курьеры',text=[`Маршрут ${$('routeDate')?.value||''} · ${courier}`,`Старт: ${start.address||'—'}`,...list.map((o,i)=>`${i+1}. ${o.time||'—'} · Заказ №${o.id} · ${o.contact||''}\n${o.address||''}\n${o.phone||''}${o.courierNote?`\nКомментарий: ${o.courierNote}`:''}`),`Яндекс Карты: ${yandexRouteUrl(list,start)}`].join('\n\n');try{await navigator.clipboard.writeText(text);toast('Маршрут скопирован.','success')}catch(_){const ta=document.createElement('textarea');ta.value=text;document.body.appendChild(ta);ta.select();document.execCommand('copy');ta.remove();toast('Маршрут скопирован.','success')}}
function printCurrentRoute(){
$('sunRoutePrintDocV1762')?.remove();const list=routeOrders(),start=effectiveRouteStart(),courier=$('routeCourier')?.selectedOptions?.[0]?.textContent||'Все курьеры',doc=document.createElement('section');doc.id='sunRoutePrintDocV1762';doc.className='sun-route-print-v1762';doc.innerHTML=`<h1>Маршрутный лист</h1><p><b>Дата:</b> ${esc($('routeDate')?.value||'—')} · <b>Курьер:</b> ${esc(courier)}</p><p><b>Старт:</b> ${esc(start.address||'—')}</p><table class="sun-route-print-table-v1762"><thead><tr><th>№</th><th>Время</th><th>Заказ / клиент</th><th>Адрес</th><th>Телефон</th></tr></thead><tbody>${list.map((o,i)=>`<tr><td>${i+1}</td><td>${esc(o.time||'—')}</td><td>№${esc(o.id)} · ${esc(o.contact||o.event||'')}</td><td>${esc(o.address||'')}</td><td>${esc(o.phone||'')}</td></tr>`).join('')}</tbody></table><p style="margin-top:12px"><b>Ссылка:</b> ${esc(yandexRouteUrl(list,start))}</p>`;document.body.appendChild(doc);document.body.classList.add('sun-route-print-v1762');window.print();setTimeout(()=>{document.body.classList.remove('sun-route-print-v1762');doc.remove();},160);
}
function interceptRouteActions(e){
const open=e.target.closest('#sun-routes-view [data-route-open]');if(open){e.preventDefault();e.stopImmediatePropagation();showRouteOrder(open.dataset.routeOpen);return}
const start=effectiveRouteStart(),list=routeOrders();
if(e.target.closest('#sun-routes-view #routeOpenYandex')){e.preventDefault();e.stopImmediatePropagation();window.open(yandexRouteUrl(list,start),'_blank','noopener');return}
if(e.target.closest('#sun-routes-view #routeCopy')){e.preventDefault();e.stopImmediatePropagation();copyCurrentRoute();return}
if(e.target.closest('#sun-routes-view #routePrint')){e.preventDefault();e.stopImmediatePropagation();printCurrentRoute();return}
}
function loadSelfChecks(){
return {
version:VERSION,release:RELEASE,
supportReadOnly:Boolean(cloud()?.isSupportMode?.()?can('orders.view')&&!can('orders.edit'):true),
menuPage:Boolean($('sun-menu-editor-v1762')),
routeStartKey:ROUTE_BASE_KEY,
routeOrderPopup:true,
calendarMore:true
};
}
function boot(){
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);
window.addEventListener('sun:cloud-permissions-changed',()=>{patchSupportPermissions();maintainSupport();syncMenuPermission();if(menuActive){renderMenuList();emptyMenuDetail();}});
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});
setInterval(()=>{maintainSupport();syncMenuPermission();if($('sun-routes-view')?.classList.contains('on'))enhanceRoutePage();},4000);
window.SunOpsUXV1762={VERSION,RELEASE,refreshSupport,openMenu,showRouteOrder,enhanceRoutePage,checks:loadSelfChecks,disconnect:()=>mo.disconnect()};
}
const waitForApp=()=>{if(window.SunCloudV2&&document.querySelector('header nav')&&typeof window.editBox==='function')boot();else setTimeout(waitForApp,120)};
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',waitForApp,{once:true});else waitForApp();
})();