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.
This commit is contained in:
parent
672a8e2237
commit
5b2926c36b
@ -1,10 +1,10 @@
|
||||
{
|
||||
"name": "caterium-app",
|
||||
"private": true,
|
||||
"version": "17.6.1",
|
||||
"version": "17.6.2",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"check:syntax": "node --check public/app-runtime.js && node --check public/service-worker.js && node --check public/legacy/bootstrap.js && node --check public/core/sun-safe.js && node --check public/core/performance.js && node --check public/core/pdf-engine.js",
|
||||
"check:syntax": "node --check public/app-runtime.js && node --check public/service-worker.js && node --check public/legacy/bootstrap.js && node --check public/core/sun-safe.js && node --check public/core/performance.js && node --check public/core/ops-ux-v1762.js && node --check public/core/pdf-engine.js",
|
||||
"test:static": "node tests/static-security.mjs",
|
||||
"check:release": "node tests/release-check.mjs",
|
||||
"check:deploy": "npm run check:syntax && npm run test:static && npm run check:release",
|
||||
|
||||
259
public/core/ops-ux-v1762.js
Normal file
259
public/core/ops-ux-v1762.js
Normal file
@ -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=`<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();
|
||||
})();
|
||||
@ -53,7 +53,7 @@
|
||||
const input=e.target;if(!(input instanceof HTMLInputElement)||input.id!=='sunChatFilesV29'||guardedInputs.has(input)||typeof DataTransfer==='undefined')return;
|
||||
e.preventDefault();e.stopImmediatePropagation();guardChatFiles(input).catch(err=>{console.error('[Caterium photo compression]',err);guardedInputs.add(input);input.dispatchEvent(new Event('change',{bubbles:true}));guardedInputs.delete(input);});
|
||||
},true);
|
||||
window.SunAttachmentGuard={VERSION:'17.6.1',MAX_FILE,TARGET,MAX_SIDE,compressImage,prepareAttachment};
|
||||
window.SunAttachmentGuard={VERSION:'17.6.2',MAX_FILE,TARGET,MAX_SIDE,compressImage,prepareAttachment};
|
||||
|
||||
let memoryData=null,memoryAt=0,memoryLoading=false;
|
||||
const esc=v=>window.SunSafe?.escapeHTML?window.SunSafe.escapeHTML(String(v??'')):String(v??'');
|
||||
@ -65,19 +65,23 @@
|
||||
async function enhanceDeveloperMemory(){
|
||||
const view=document.getElementById('sun-developer-console-v22');if(!view?.classList.contains('on'))return;
|
||||
const body=document.getElementById('sunDevBody');if(!body)return;
|
||||
const version=document.getElementById('sunDevReleaseVersion');if(version)version.textContent='17.6.1';
|
||||
const version=document.getElementById('sunDevReleaseVersion');if(version)version.textContent='17.6.2';
|
||||
const d=await loadMemory();if(!d||!view.classList.contains('on'))return;
|
||||
let box=document.getElementById('sunDevMemoryV1761');if(!box){box=document.createElement('div');box.id='sunDevMemoryV1761';box.className='sun-dev-grid';box.style.marginBottom='12px';body.prepend(box);}
|
||||
box.innerHTML=`<div class="sun-dev-kpi"><small>Память сервера</small><b>${esc(d.server_size||d.database_size||'—')}</b></div><div class="sun-dev-kpi"><small>База PostgreSQL</small><b>${esc(d.database_size||'—')}</b></div><div class="sun-dev-kpi"><small>Файлы Storage</small><b>${esc(d.storage_size||'—')}</b></div><div class="sun-dev-kpi"><small>Объектов Storage</small><b>${Number(d.storage_objects||0)}</b></div>`;
|
||||
}
|
||||
function loadOpsUX(){
|
||||
if(window.SunOpsUXV1762||document.getElementById('sunOpsUXV1762Script'))return;
|
||||
const script=document.createElement('script');script.id='sunOpsUXV1762Script';script.src='core/ops-ux-v1762.js?v=20260907-v17-6-2-ops-ux';script.async=true;script.onerror=()=>console.error('[Caterium] Не загрузился модуль ops-ux-v1762.js');document.head.appendChild(script);
|
||||
}
|
||||
const start=()=>{
|
||||
scan(document);
|
||||
loadOpsUX();scan(document);
|
||||
const mo=new MutationObserver(records=>{records.forEach(r=>r.addedNodes.forEach(n=>{if(n.nodeType===1)scan(n)}));enhanceDeveloperMemory();});
|
||||
mo.observe(document.documentElement,{childList:true,subtree:true});
|
||||
document.addEventListener('click',e=>{if(e.target.closest('#sunDeveloperNavV22,[data-dev-tab],#sunDevRefresh'))setTimeout(enhanceDeveloperMemory,100)},true);
|
||||
window.addEventListener('sun:cloud-state-applied',()=>setTimeout(enhanceDeveloperMemory,150));
|
||||
setInterval(()=>{if(document.getElementById('sun-developer-console-v22')?.classList.contains('on'))enhanceDeveloperMemory();},5000);
|
||||
window.SunPerformance={scanImages:()=>scan(document),refreshDeveloperMemory:enhanceDeveloperMemory,disconnect:()=>mo.disconnect()};
|
||||
window.SunPerformance={scanImages:()=>scan(document),refreshDeveloperMemory:enhanceDeveloperMemory,loadOpsUX,disconnect:()=>mo.disconnect()};
|
||||
};
|
||||
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',start,{once:true});else start();
|
||||
})();
|
||||
})();
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
const CACHE='sun-catering-pwa-v66-20260907-v17-6-1-chat-storage-guard';
|
||||
const VERSION='20260907-v17-6-1-chat-storage-guard';
|
||||
const CACHE='sun-catering-pwa-v67-20260907-v17-6-2-ops-ux';
|
||||
const VERSION='20260907-v17-6-2-ops-ux';
|
||||
const CORE=[
|
||||
'./','./index.html',
|
||||
`./core/sun-safe.js?v=${VERSION}`,`./core/performance.js?v=${VERSION}`,`./core/pdf-engine.js?v=${VERSION}`,`./legacy/bootstrap.js?v=${VERSION}`,`./app-runtime.js?v=${VERSION}`,
|
||||
`./core/sun-safe.js?v=${VERSION}`,`./core/performance.js?v=${VERSION}`,`./core/ops-ux-v1762.js?v=${VERSION}`,`./core/pdf-engine.js?v=${VERSION}`,`./legacy/bootstrap.js?v=${VERSION}`,`./app-runtime.js?v=${VERSION}`,
|
||||
'./offer-gallery/001.jpg','./offer-gallery/002.jpg',
|
||||
'./catalog/001.jpg','./catalog/002.jpg','./catalog/003.jpg',
|
||||
'./sun-logo.png','./caterium-login-logo.png','./pwa-icon-192.png','./pwa-icon-512.png','./manifest.webmanifest',
|
||||
|
||||
@ -37,6 +37,34 @@ test('chat photo guard compresses a large camera image', async ({ page }) => {
|
||||
});
|
||||
expect(Math.max(result.width,result.height)).toBeLessThanOrEqual(2048);expect(result.size).toBeLessThan(result.original);expect(result.size).toBeLessThanOrEqual(15*1024*1024);expect(result.type).toBe('image/jpeg');expect(result.name.endsWith('.jpg')).toBeTruthy();
|
||||
});
|
||||
test('v17.6.2 operations UX boots with menu and route features', async ({ page }) => {
|
||||
await page.goto('/index.html', { waitUntil:'domcontentloaded' });
|
||||
await page.evaluate(()=>{
|
||||
if(!document.querySelector('header nav')){
|
||||
const header=document.createElement('header');
|
||||
const nav=document.createElement('nav');
|
||||
header.appendChild(nav);
|
||||
document.body.appendChild(header);
|
||||
}
|
||||
window.SunCloudV2={
|
||||
getSupportMode:()=>null,
|
||||
isSupportMode:()=>false,
|
||||
hasPermission:()=>true,
|
||||
getSession:()=>({user:{id:'e2e'}})
|
||||
};
|
||||
window.editBox=()=>{};
|
||||
});
|
||||
const content=fs.readFileSync(path.join(process.cwd(),'public','core','ops-ux-v1762.js'),'utf8');
|
||||
await page.addScriptTag({content});
|
||||
await page.waitForFunction(()=>Boolean(window.SunOpsUXV1762),null,{timeout:5000});
|
||||
const checks=await page.evaluate(()=>window.SunOpsUXV1762.checks());
|
||||
expect(checks.version).toBe('17.6.2');
|
||||
expect(checks.menuPage).toBeTruthy();
|
||||
expect(checks.routeStartKey).toBe('sunRouteBaseV1');
|
||||
expect(checks.routeOrderPopup).toBeTruthy();
|
||||
expect(checks.calendarMore).toBeTruthy();
|
||||
expect(await page.locator('header nav button', {hasText:'Меню'}).count()).toBeGreaterThan(0);
|
||||
});
|
||||
test('mobile body does not overflow viewport', async ({ page }, testInfo) => {
|
||||
test.skip(testInfo.project.name!=='mobile-390'); await page.goto('/index.html', { waitUntil:'domcontentloaded' }); await page.waitForTimeout(500);
|
||||
const dims=await page.evaluate(()=>({innerWidth,scrollWidth:document.documentElement.scrollWidth,bodyWidth:document.body.scrollWidth}));
|
||||
|
||||
@ -3,16 +3,22 @@ import path from 'node:path';
|
||||
const root=process.cwd(), pub=path.join(root,'public');
|
||||
const read=p=>fs.readFileSync(path.join(pub,p),'utf8');
|
||||
let bad=0;const check=(v,m)=>{console.log(`${v?'OK':'FAIL'}: ${m}`);if(!v)bad++};
|
||||
const index=read('index.html'),runtime=read('app-runtime.js'),sw=read('service-worker.js'),css=read('core/stability-v1760.css'),performance=read('core/performance.js');
|
||||
const index=read('index.html'),runtime=read('app-runtime.js'),sw=read('service-worker.js'),css=read('core/stability-v1760.css'),performance=read('core/performance.js'),ops=read('core/ops-ux-v1762.js');
|
||||
check(index.includes('core/stability-v1760.css'),'mobile stability stylesheet loaded');
|
||||
check(css.includes('overflow-x:hidden')&&css.includes('.cats'),'mobile overflow guard present');
|
||||
check(!index.includes('offer-gallery-data.js'),'blocking Base64 gallery absent');
|
||||
check((runtime.match(/\/Type \/Catalog/g)||[]).length===0,'runtime contains no PDF binary writer');
|
||||
check(read('core/pdf-engine.js').includes('595.28')&&read('core/pdf-engine.js').includes('841.89'),'PDF engine uses A4 MediaBox');
|
||||
check([...index.matchAll(/@page\{([^}]*)\}/g)].every(m=>/size:A4/i.test(m[1])),'compact @page rules use A4');
|
||||
check(sw.includes('v17-6-1-chat-storage-guard'),'service worker cache is v17.6.1');
|
||||
check(sw.includes('v17-6-2-ops-ux')&&sw.includes('ops-ux-v1762.js'),'service worker cache is v17.6.2');
|
||||
check(performance.includes('SunAttachmentGuard')&&performance.includes('TARGET=2*1024*1024'),'chat photo auto-compression is versioned');
|
||||
check(performance.includes("rpc('sun_dev_dashboard')")&&performance.includes('server_size')&&performance.includes('storage_size'),'Developer Console server/storage counters are versioned');
|
||||
check(performance.includes('ops-ux-v1762.js'),'operations UX module is loaded by performance core');
|
||||
check(ops.includes('supportReadPermission')&&ops.includes('SUPPORT_POLL_MS=12000'),'developer support read-only live refresh is versioned');
|
||||
check(ops.includes('sun-menu-editor-v1762')&&ops.includes('Премиум боксы'),'full-page menu editor is versioned');
|
||||
check(ops.includes('showCalendarDay')&&ops.includes('.cal-more'),'calendar overflow panel is versioned');
|
||||
check(ops.includes("ROUTE_BASE_KEY='sunRouteBaseV1'")&&ops.includes('saveRouteStart'),'configurable route origin is versioned');
|
||||
check(ops.includes('showRouteOrder')&&ops.includes('data-route-open'),'route order modal is versioned');
|
||||
check(fs.existsSync(path.join(root,'supabase/functions/caterium-create-employee/index.ts')),'employee Edge Function source is versioned');
|
||||
check(!/sb_secret_[A-Za-z0-9_-]{20,}|service_role\s*[:=]\s*["'][A-Za-z0-9._-]{30,}/i.test(index+runtime+performance),'no client secret-like token');
|
||||
check(!/sb_secret_[A-Za-z0-9_-]{20,}|service_role\s*[:=]\s*["'][A-Za-z0-9._-]{30,}/i.test(index+runtime+performance+ops),'no client secret-like token');
|
||||
if(bad)process.exit(1);
|
||||
|
||||
@ -6,7 +6,7 @@ const readPub=p=>fs.readFileSync(path.join(pub,p),'utf8');
|
||||
const readRoot=p=>fs.readFileSync(path.join(root,p),'utf8');
|
||||
const fail=m=>{console.error('FAIL:',m);process.exitCode=1};
|
||||
const ok=m=>console.log('OK:',m);
|
||||
const html=readPub('index.html'),legacy=readPub('legacy/bootstrap.js'),runtime=readPub('app-runtime.js'),safe=readPub('core/sun-safe.js'),sw=readPub('service-worker.js'),performance=readPub('core/performance.js');
|
||||
const html=readPub('index.html'),legacy=readPub('legacy/bootstrap.js'),runtime=readPub('app-runtime.js'),safe=readPub('core/sun-safe.js'),sw=readPub('service-worker.js'),performance=readPub('core/performance.js'),ops=readPub('core/ops-ux-v1762.js');
|
||||
if(!html.includes('core/sun-safe.js'))fail('SunSafe must load before legacy modules');else ok('shared SunSafe loaded');
|
||||
if(html.includes('offer-gallery-data.js')||fs.existsSync(path.join(pub,'offer-gallery-data.js')))fail('blocking offer-gallery-data.js still present');else ok('base64 gallery removed');
|
||||
for(const raw of ['<span>${b.name}</span>','<span>${o.event}</span>','<span>${o.address||','value="${x[0]}"','value="${x[2]}"']) if(legacy.includes(raw)) fail(`legacy bootstrap contains raw HTML interpolation ${raw}`);
|
||||
@ -27,7 +27,10 @@ if(current!==113)fail(`current catalog photo count ${current}, expected 113`);el
|
||||
if(legacyCount!==60)fail(`legacy catalog photo count ${legacyCount}, expected 60`);else ok('60 legacy catalog photos');
|
||||
const gallery=fs.readdirSync(path.join(pub,'offer-gallery')).filter(x=>/\.jpg$/i.test(x));
|
||||
if(gallery.length!==2)fail(`offer gallery contains ${gallery.length} jpg files, expected 2`);else ok('offer gallery trimmed');
|
||||
if(!sw.includes('v17-6-1-chat-storage-guard')||sw.includes('offer-gallery-data.js'))fail('service worker cache is stale');else ok('PWA cache updated');
|
||||
if(!sw.includes('v17-6-2-ops-ux')||!sw.includes('ops-ux-v1762.js')||sw.includes('offer-gallery-data.js'))fail('service worker cache is stale');else ok('PWA cache updated');
|
||||
if(!performance.includes('SunAttachmentGuard')||!performance.includes('MAX_SIDE=2048'))fail('chat photo compression guard missing');else ok('chat photo compression guard present');
|
||||
if(!performance.includes("rpc('sun_dev_dashboard')")||!performance.includes('storage_size')||!performance.includes('server_size'))fail('Developer Console memory counters missing');else ok('Developer Console memory counters present');
|
||||
if(!performance.includes('ops-ux-v1762.js')||!performance.includes('SunOpsUXV1762'))fail('ops UX loader missing');else ok('ops UX loader present');
|
||||
for(const marker of ['SUPPORT_POLL_MS=12000','supportReadPermission','sun-menu-editor-v1762','showCalendarDay',"ROUTE_BASE_KEY='sunRouteBaseV1'",'showRouteOrder','routeOpenYandex']) if(!ops.includes(marker)) fail(`ops UX marker missing: ${marker}`); else ok(`ops UX marker: ${marker}`);
|
||||
if(/service_role\s*[:=]\s*['"][A-Za-z0-9._-]{20,}/i.test(ops)||/eyJ[a-zA-Z0-9_-]{30,}/.test(ops))fail('possible secret in ops UX module');else ok('ops UX module has no hard-coded secret');
|
||||
if(process.exitCode)process.exit(process.exitCode);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user