263 lines
42 KiB
JavaScript
263 lines
42 KiB
JavaScript
(()=>{
|
||
'use strict';
|
||
if(window.SunOpsUXV1762)return;
|
||
|
||
const VERSION='17.6.2';
|
||
const RELEASE='20260907-v17-6-2-ops-ux';
|
||
const SUPPORT_POLL_MS=20000;
|
||
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=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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,oldButtonTimer=0;
|
||
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{
|
||
if(typeof c.refreshSupportWorkspace==='function')await c.refreshSupportWorkspace();else 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=>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=`<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)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;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
|
||
};
|
||
}
|
||
let maintenanceTimer=0,waitTimer=0,waitAttempts=0;
|
||
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});
|
||
if(!maintenanceTimer)maintenanceTimer=setInterval(()=>{if(document.hidden)return;maintainSupport();syncMenuPermission();if($('sun-routes-view')?.classList.contains('on'))enhanceRoutePage();},8000);
|
||
window.SunOpsUXV1762={VERSION,RELEASE,refreshSupport,openMenu,showRouteOrder,enhanceRoutePage,checks:loadSelfChecks,disconnect:()=>{mo.disconnect();if(maintenanceTimer){clearInterval(maintenanceTimer);maintenanceTimer=0}if(supportTimer){clearInterval(supportTimer);supportTimer=0}if(waitTimer){clearTimeout(waitTimer);waitTimer=0}if(oldButtonTimer){clearInterval(oldButtonTimer);oldButtonTimer=0}}};
|
||
}
|
||
const waitForApp=()=>{if(window.SunCloudV2&&document.querySelector('header nav')&&typeof window.editBox==='function'){if(waitTimer){clearTimeout(waitTimer);waitTimer=0}boot();return}waitAttempts++;if(waitAttempts>=120){console.warn('[Caterium ops UX] app prerequisites timed out; waiting for a cloud-state event');return}waitTimer=setTimeout(waitForApp,250)};
|
||
window.addEventListener('sun:cloud-state-applied',()=>{if(!window.SunOpsUXV1762&&waitAttempts>=120){waitAttempts=0;waitForApp()}});
|
||
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',waitForApp,{once:true});else waitForApp();
|
||
})();
|