From ad69c852edd017753d560320a858aabd400e83ab Mon Sep 17 00:00:00 2001 From: pavlov346346-source Date: Mon, 7 Sep 2026 18:43:27 +0300 Subject: [PATCH] Add v17.6.2 operations UX module --- public/core/ops-ux-v1762.js | 259 ++++++++++++++++++++++++++++++++++++ 1 file changed, 259 insertions(+) create mode 100644 public/core/ops-ux-v1762.js diff --git a/public/core/ops-ux-v1762.js b/public/core/ops-ux-v1762.js new file mode 100644 index 0000000..7b2b076 --- /dev/null +++ b/public/core/ops-ux-v1762.js @@ -0,0 +1,259 @@ +(()=>{ + '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=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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=`

${esc(title)}

${body}${actions?`
${actions}
`:''}
`;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=`
${list.map(o=>{const total=orderTotal(o),paid=Math.max(0,Number(o.prepayment||0));return `
${esc(o.time||'—')}
№${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=>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=`

${esc(item.name||'Позиция')}только просмотр

${item.photo?``:''}
Категория${esc(currentCategory().label)}
Цена${esc(money(item.price||0))}
Вес${esc(item.weight||'—')}
Количество${Number(item.pieces||0)||'—'}${item.pieces?' шт.':''}
Состав для клиента${comp.length?`
    ${comp.map(x=>`
  • ${esc(x)}
  • `).join('')}
`:'

Не заполнен.

'}
Состав / ТТК${ingredients.length?`
    ${ingredients.map(x=>`
  • ${esc(x?.[0]||'')} — ${esc(x?.[1]??'')} ${esc(x?.[2]||'')}
  • `).join('')}
`:'

Не заполнен.

'}
`; + } + 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&&!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;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||'—')}
Статус${esc(o.status||'Новый')}
Сумма${esc(money(total))}
Остаток${esc(money(balance))}

${esc(o.event||'Заказ')}${o.guestsCount?` · ${esc(o.guestsCount)} гостей`:''}

Клиент: ${esc(o.contact||'—')} · ${esc(o.phone||'—')}

Адрес: ${esc(o.address||'—')}

${o.delivery?`

Доставка: ${esc(money(o.delivery))}

`:''}${o.courierNote?`
Курьеру:
${esc(o.courierNote)}
`:''}${o.note?`
Комментарий:
${esc(o.note)}
`:''}
Меню заказа
${lines.length?lines.map(x=>``).join(''):''}
ПозицияКол.ЦенаСумма
${esc(x.name)}${x.qty}${esc(money(x.price))}${esc(money(x.sum))}
Позиции не указаны.
`; + 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?``:''}${editable?'':''}`; + 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=`

Маршрутный лист

Дата: ${esc($('routeDate')?.value||'—')} · Курьер: ${esc(courier)}

Старт: ${esc(start.address||'—')}

${list.map((o,i)=>``).join('')}
ВремяЗаказ / клиентАдресТелефон
${i+1}${esc(o.time||'—')}№${esc(o.id)} · ${esc(o.contact||o.event||'')}${esc(o.address||'')}${esc(o.phone||'')}

Ссылка: ${esc(yandexRouteUrl(list,start))}

`;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(); +})();