caterium-app/public/index.html
2026-09-18 18:13:00 +03:00

8453 lines
733 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

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

<!doctype html><html lang="ru"><head><link rel="stylesheet" href="core/login-signature-v1776.css?v=20260918-safari-login"><script src="core/cloud-transport.js?v=20260918-safari-login"></script><script src="core/banquet-menu.js?v=20260918-safari-login"></script><script src="core/access-policy.js?v=20260918-safari-login"></script><style>body.sun-cloud-auth-required>header,body.sun-cloud-auth-required>.view{visibility:hidden!important;pointer-events:none!important}</style><meta charset="utf-8"><script defer src="core/import-archive.js?v=20260918-safari-login"></script><script src="core/company-branding.js?v=20260918-safari-login"></script><script src="core/brand-theme.js?v=20260918-safari-login"></script><script id="sun-startup-stability-guard">
(()=>{
'use strict';
const stamp=()=>new Date().toISOString().replace(/[:.]/g,'-');
const schemas={
sunBoxes:'array',sunOrders:'array',sunStock:'array',sunStockMoves:'array',
sunEmployees:'array',sunSuppliers:'array',sunFinanceRecordsV2:'array',
sunRoutePlansV1:'object',sunClientLoyaltyV1:'object',sunEnterpriseSettingsV1:'object',
sunStaticMapGeocodeCacheV1:'object'
};
try{
Object.entries(schemas).forEach(([key,type])=>{
const raw=localStorage.getItem(key);if(raw==null||raw==='')return;
let value,valid=true;
try{value=JSON.parse(raw);}catch(_){valid=false;}
if(valid){valid=type==='array'?Array.isArray(value):Boolean(value)&&typeof value==='object'&&!Array.isArray(value);}
if(valid)return;
try{localStorage.setItem(`sunRecoveryCorrupt.${key}.${stamp()}`,raw.slice(0,250000));}catch(_){ }
try{localStorage.removeItem(key);}catch(_){ }
});
}catch(_){ }
window.sunFetchWithTimeout=window.sunFetchWithTimeout||async function(url,options={},timeout=8000){
const controller=new AbortController();
const timer=setTimeout(()=>controller.abort(),Math.max(1000,Number(timeout)||8000));
try{
return await fetch(url,{...options,signal:options.signal||controller.signal});
}finally{clearTimeout(timer);}
};
})();
</script><meta name="viewport" content="width=device-width,initial-scale=1"><title>Caterium</title><meta name="theme-color" content="#15364c"><link rel="icon" href="caterium-mark-light.svg" type="image/svg+xml"><style>:root{--n:#15364c;--g:#c99a32;--p:#f5f2eb;--l:#d9dde0}*{box-sizing:border-box}body{margin:0;background:var(--p);font:15px Arial;color:#203040}header{height:70px;background:var(--n);color:#fff;display:flex;align-items:center;padding:0 24px;gap:40px}.brand{font-size:22px;font-weight:bold}.sun{color:#f7d45d;font-size:32px;margin-right:8px}nav button{height:46px;background:transparent;color:#dce8ec;border:0;border-bottom:3px solid transparent;padding:0 15px;font-weight:bold}nav button.on{color:#fff;border-color:var(--g)}button,input,select,textarea{font:inherit}button{cursor:pointer}.view{display:none}.view.on{display:block}.work{min-height:calc(100vh - 70px);padding:14px;display:grid;grid-template-columns:minmax(320px,1.2fr) 180px minmax(360px,1fr);gap:10px}.card,.page{background:#fff;border-radius:9px;padding:16px;box-shadow:0 1px 3px #0001}.catalog-head{display:flex;justify-content:space-between;align-items:center}.catalog h1,.page h1{margin:0;color:var(--n);font-size:23px}.tiles{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:10px;margin-top:14px}.tile{min-height:152px;border:1px solid var(--l);background:#f0f2f4;border-radius:6px;padding:7px;font-weight:bold;color:#203040}.tile:hover{border-color:var(--g)}.tile img,.ph{width:100%;height:88px;object-fit:cover;border-radius:4px}.ph{display:grid;place-items:center;color:var(--g);background:#e2e9eb;font-size:36px}.add{background:#fffaf0;color:var(--n);font-size:16px}.cats{padding:12px;background:#e9efe8;border-radius:9px}.cats button{width:100%;border:0;padding:15px 7px;border-radius:5px;background:#c9d0d8;color:#fff;font-weight:bold;margin-bottom:8px}.cats button:first-child{background:var(--g)}.cats p{color:#68717a;font-size:13px}.subnav{border-bottom:1px solid var(--l);margin-bottom:14px}.subnav button{background:#fff;border:0;padding:9px 14px;color:#68717a;font-weight:bold}.subnav button.on{color:var(--n);border-bottom:3px solid var(--g)}label{display:flex;flex-direction:column;gap:5px;color:#68717a;font-size:13px}input,select,textarea{border:1px solid #cfd5da;border-radius:4px;padding:8px;color:#203040;background:#fff}.form{display:grid;grid-template-columns:1fr 135px 90px;gap:9px}.money-form{display:grid;grid-template-columns:repeat(3,1fr);gap:9px;margin-top:14px}.order-summary{margin-top:14px;padding:12px;border:1px solid var(--l);border-radius:7px;background:#fafafa}.order-summary p{display:flex;justify-content:space-between;margin:5px 0}.order-summary .balance{font-size:18px;font-weight:bold;color:var(--n)}.order-actions{display:flex;gap:6px;align-items:center}.mini{padding:6px 9px;border-radius:4px;border:1px solid var(--l);background:#fff;color:var(--n)}.lines{margin-top:18px;border-top:1px solid var(--l);min-height:230px}.line{display:grid;grid-template-columns:1fr 30px 48px 30px;align-items:center;gap:5px;padding:8px 0;border-bottom:1px solid var(--l)}.line button{height:28px;border:1px solid #c8cdd1;background:#f1f3f4;border-radius:4px}.line input{text-align:center;padding:4px}.actions{display:flex;gap:8px;margin-top:15px}.primary,.outline,.danger{padding:9px 13px;border-radius:5px;font-weight:bold}.primary{background:var(--n);color:#fff;border:1px solid var(--n)}.outline{background:#fff;color:var(--n);border:1px solid var(--g)}.danger{color:#a33;border:1px solid #dca;background:#fff4f4}.empty{color:#879198;padding:18px 0}.page{margin:18px;min-height:calc(100vh - 106px)}.row,.head{display:grid;grid-template-columns:65px 1.2fr 150px 1.2fr 175px;gap:8px;align-items:center;padding:10px;border-bottom:1px solid var(--l)}.head{color:#68717a;font-size:13px;font-weight:bold}.row{background:#fff;border:0;width:100%;text-align:left}.row:hover{background:#f8f8f7}.modal{display:none;position:fixed;inset:0;background:#102c3d88;place-items:center;padding:18px}.modal.on{display:grid}.dialog{width:min(820px,100%);max-height:90vh;overflow:auto;background:#fff;padding:20px;border-radius:10px}.dialog-head{display:flex;justify-content:space-between;align-items:center}.dialog-head h2{margin:0;color:var(--n)}.dialog-head button{border:0;background:#fff;font-size:28px}.manager{display:grid;grid-template-columns:repeat(auto-fill,minmax(190px,1fr));gap:8px;margin-top:14px}.manager button{border:1px solid var(--l);background:#fff;text-align:left;padding:9px;display:grid;grid-template-columns:48px 1fr;gap:7px}.manager img,.manager .ph{width:46px;height:46px}.manager small{grid-column:2;color:#68717a}.ingredient{display:grid;grid-template-columns:1fr 90px 85px 30px;gap:5px;margin:6px 0}.price-field{margin:10px 0}.ingredient button{border:1px solid #e3bcbc;color:#a33;background:#fff4f4}.editphoto{max-height:150px;max-width:220px;margin:8px 0;border-radius:5px}.prep{display:grid;grid-template-columns:1fr 1fr;gap:0 24px}.prep p{display:flex;justify-content:space-between;border-bottom:1px solid var(--l);padding:8px 0;margin:0}.hint{color:#68717a;font-size:13px}@media(max-width:900px){.money-form{grid-template-columns:1fr}.order-actions{flex-wrap:wrap}header{height:auto;flex-direction:column;align-items:flex-start;padding:12px 16px;gap:4px}nav{overflow:auto;white-space:nowrap}.work{grid-template-columns:1fr;min-height:0}.cats{display:flex;gap:8px}.cats button{margin:0;min-width:150px}.cats p{display:none}.form{grid-template-columns:1fr}.row,.head{grid-template-columns:55px 1fr 125px}.row span:nth-child(4),.head span:nth-child(4){display:none}.row select{grid-column:2/4}.prep{grid-template-columns:1fr}}@media print{header,.catalog,.cats,.subnav,.form,.lines,.actions,.modal:not(#preview){display:none!important}.work{display:block;padding:0}.card{box-shadow:none}.modal#preview{display:block;position:static;background:#fff;padding:0}.modal#preview .dialog{width:100%;max-height:none}.modal#preview .dialog button{display:none}}</style><script>function sunUUID(){const c=globalThis.crypto,b=new Uint8Array(16);if(c&&typeof c.getRandomValues==='function')c.getRandomValues(b);else for(let i=0;i<16;i++)b[i]=Math.floor(Math.random()*256);b[6]=(b[6]&15)|64;b[8]=(b[8]&63)|128;const h=[...b].map(x=>x.toString(16).padStart(2,'0')).join('');return `${h.slice(0,8)}-${h.slice(8,12)}-${h.slice(12,16)}-${h.slice(16,20)}-${h.slice(20)}`}</script>
<style id="sun-delivery-details-style">
.sun-delivery-details{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px 12px;margin-bottom:10px}.sun-delivery-details label{margin:0}.sun-delivery-details .wide{grid-column:1/-1}.sun-address-hidden{display:none!important}@media(max-width:680px){.sun-delivery-details{grid-template-columns:1fr}.sun-delivery-details .wide{grid-column:auto}}
</style>
<style id="sun-order-lines-table-style">
#orderForm .lines{border-top:0!important;margin-top:16px!important;min-height:180px!important}
.sun-order-lines-table{border:1px solid #e2e6ea;border-radius:12px;overflow:hidden;background:#fff;box-shadow:0 1px 3px #102c3d0a}
.sun-order-line-head,.sun-order-line-row{display:grid;grid-template-columns:minmax(0,1fr) 150px 112px 126px;gap:10px;align-items:center}
.sun-order-line-head{padding:10px 12px;background:#f6f8fa;border-bottom:1px solid #e2e6ea;color:#77818a;font-size:11px;font-weight:800;text-transform:uppercase;letter-spacing:.045em}
.sun-order-line-head span:nth-child(n+2){text-align:right}
.sun-order-line-row{padding:10px 12px;border-bottom:1px solid #edf0f2;min-height:54px}
.sun-order-line-row:last-child{border-bottom:0}
.sun-order-line-name{font-weight:700;color:#243746;line-height:1.28;min-width:0}
.sun-order-line-qty{display:grid;grid-template-columns:30px 54px 30px;justify-content:end;gap:4px;align-items:center}
.sun-order-line-qty button{width:30px;height:30px;padding:0;border:1px solid #d7dde2;border-radius:7px;background:#f7f9fa;color:#15364c;font-size:17px;font-weight:800}
.sun-order-line-qty input{width:54px;height:30px;padding:3px 4px;text-align:center;border-radius:7px;font-weight:700}
.sun-order-line-price,.sun-order-line-sum{text-align:right;white-space:nowrap;color:#52616d}
.sun-order-line-sum{color:#15364c;font-size:14px}
.sun-order-line-total{display:grid;grid-template-columns:minmax(0,1fr) 126px;gap:10px;align-items:center;padding:12px;background:linear-gradient(90deg,#fafbfc,#f5f7f8);border-top:1px solid #dfe4e7}
.sun-order-line-total span{text-align:right;color:#69747e;font-size:13px;font-weight:800}
.sun-order-line-total b{text-align:right;color:#15364c;font-size:18px;white-space:nowrap}
#clients .clients-toolbar{margin-top:14px!important}
@media(max-width:760px){
.sun-order-line-head{display:none}
.sun-order-line-row{grid-template-columns:minmax(0,1fr) 112px;gap:7px 10px;padding:10px}
.sun-order-line-name{grid-column:1/-1}
.sun-order-line-qty{justify-content:start;grid-template-columns:28px 48px 28px}
.sun-order-line-qty button{width:28px;height:28px}
.sun-order-line-qty input{width:48px;height:28px}
.sun-order-line-price{align-self:center;text-align:right;font-size:12px}
.sun-order-line-price::before{content:'Цена: ';color:#8a939a;font-weight:400}
.sun-order-line-sum{grid-column:2;text-align:right}
.sun-order-line-sum::before{content:'Стоимость: ';color:#8a939a;font-weight:400;font-size:12px}
.sun-order-line-total{grid-template-columns:1fr auto;padding:11px}
}
</style>
<link rel="manifest" href="manifest.webmanifest"><link rel="apple-touch-icon" href="pwa-icon-192.png"><meta name="apple-mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"><meta name="apple-mobile-web-app-title" content="Caterium"><style id="sun-mobile-pwa-inline">/* Classic responsive layer: preserve the original app design on phones. */
@media screen and (max-width:900px){
html{-webkit-text-size-adjust:100%}
input,select,textarea{font-size:16px!important}
button{touch-action:manipulation}
#sunGlobalSearchBtn kbd{display:none!important}
#sunSyncSettingsCard>div[style*="grid-template-columns"]{grid-template-columns:1fr!important}
}</style><style id="sun-composition-v1757">#sunBoxCompositionLabel{display:flex;flex-direction:column;gap:6px;margin-top:10px}#sunBoxCompositionLabel textarea{width:100%;min-height:120px;resize:vertical;font:inherit;line-height:1.4;padding:10px;border:1px solid #dfe3ea;border-radius:10px;box-sizing:border-box}#sunBoxCompositionLabel small{color:#7b8390;font-size:11px}</style></head><body class="sun-cloud-auth-required"><header><div class="brand"><img class="real-logo caterium-product-mark" src="caterium-mark-light.svg" alt="Caterium"><span>Caterium</span></div><nav><button class="on" onclick="show('new',this)">Новый заказ</button><button onclick="show('orders',this)">Заказы</button><button onclick="show('stock',this)">Склад</button><button onclick="show('stats',this)">Статистика</button></nav></header><section id="new" class="view on"><div class="work"><aside class="card catalog"><div class="catalog-head"><h1>Боксы</h1><button class="outline" onclick="openManager()">Настроить</button></div><div class="tiles" id="tiles"></div></aside><aside class="cats"><button>Боксы</button><button>Посуда и упаковка</button><button>Дополнительно</button><p>Для создания и изменения боксов нажмите «Настроить».</p></aside><section class="card"><div class="subnav"><button class="on" id="orderTab" onclick="details(false)">Заказ</button><button id="detailTab" onclick="details(true)">Детали</button></div><div id="orderForm"><div class="form"><label>Мероприятие<input id="event" value=""></label><label>Выдача<input id="date" type="date"></label><label>Время<select id="time"></select></label></div><div class="lines" id="lines"></div></div><div id="details" style="display:none"><div class="sun-delivery-details"><label>Имя<input id="contact" autocomplete="name"></label><label>Телефон<input id="phone" autocomplete="tel"></label><label class="wide">Улица<input id="deliveryStreet"></label><label>Дом<input id="deliveryHouse"></label><label>Подъезд<input id="deliveryEntrance"></label><label>Этаж<input id="deliveryFloor"></label><label>Квартира<input id="deliveryApartment"></label><label>Офис<input id="deliveryOffice"></label></div><label class="sun-address-hidden">Адрес доставки<input id="address" type="hidden"></label><label>Примечание<textarea id="note" rows="4"></textarea></label></div><div class="money-form"><label>Сумма заказа<input id="orderTotal" type="number" min="0" step="1" readonly></label><label>Предоплата<input id="prepayment" type="number" min="0" step="1" value="0" oninput="updateOrderSummary()"></label><label>Остаток<input id="balance" type="number" readonly></label></div><div class="order-summary"><p><span>Стоимость позиций</span><b id="summaryTotal">0 ₽</b></p><p><span>Предоплата</span><b id="summaryPrepayment">0 ₽</b></p><p class="balance"><span>К оплате</span><b id="summaryBalance">0 ₽</b></p></div><div class="actions"><button class="primary" onclick="saveOrder()">Сохранить</button><button class="outline" onclick="openPreview()">Заготовки</button><button id="deleteOrderBtn" class="danger" onclick="deleteOrder()" style="display:none">Удалить заказ</button></div></section></div></section><section id="orders" class="view page"><h1>Заказы</h1><div class="head" style="grid-template-columns:65px 1.15fr 145px 1fr 110px 175px 150px"><span></span><span>Мероприятие</span><span>Дата и время</span><span>Адрес</span><span>Сумма</span><span>Статус</span><span>Действия</span></div><div id="ordersList"></div><p class="hint">Нажмите «Изменить», чтобы открыть заказ. Сумма, предоплата и остаток сохраняются вместе с заказом.</p></section><section id="stock" class="view page"><h1>Склад</h1><p>В этой версии каталог продуктов пополняется из составов боксов. Остатки и цены добавим следующим шагом.</p></section><section id="stats" class="view page"><h1>Статистика</h1><div id="statsList"></div></section><div class="modal" id="manager"><div class="dialog"><div class="dialog-head"><h2>Боксы</h2><button onclick="closeModal('manager')">×</button></div><button class="primary" onclick="editBox(null)">Добавить бокс</button><div class="manager" id="managerList"></div></div></div><div class="modal" id="editor"><div class="dialog"><div class="dialog-head"><h2 id="editTitle">Бокс</h2><button onclick="closeModal('editor')">×</button></div><label>Название<input id="boxName"></label><label class="price-field"><span id="boxPriceCaption">Цена, ₽</span><input id="boxPrice" type="number" min="0" step="1" value="0"></label><label class="price-field"><span>Цена до скидки, ₽ <small style="font-weight:400;color:#8a91a0">(необязательно)</small></span><input id="boxOldPrice" type="number" min="0" step="1" value="0" placeholder="Перечёркнутая цена"></label><label>Фотография<input id="boxPhoto" type="file" accept="image/*" onchange="readPhoto(this)"></label><img id="editPhoto" class="editphoto" style="display:none"><h3>Состав / ТТК</h3><div id="ingredients"></div><button class="outline" onclick="addIngredient()">Добавить продукт</button><div class="actions"><button class="primary" id="saveBoxButton" onclick="saveBox()">Сохранить</button><button id="deleteBox" class="danger" onclick="removeBox()">Удалить</button></div></div></div><div class="modal" id="preview"><div class="dialog"><div class="dialog-head"><h2>Документ компании</h2><button onclick="closeModal('preview')">×</button></div><h3 id="prepTitle"></h3><div class="prep" id="prep"></div><div class="actions"><button class="primary" onclick="print()">Печать</button></div></div></div><link rel="stylesheet" href="core/stability-v1760.css?v=20260918-safari-login"><script src="core/sun-safe.js?v=20260918-safari-login"></script><script defer src="core/mobile-order.js?v=20260918-safari-login"></script><script defer src="core/trial-demo.js?v=20260918-safari-login"></script><script src="core/performance.js?v=20260918-safari-login"></script><script src="core/pdf-engine.js?v=20260918-safari-login"></script><script src="core/proposal-layout.js?v=20260918-safari-login"></script><script src="core/classic-offer-pdf-v1767.js?v=20260918-safari-login"></script><script src="core/signature-offer-pdf-v18.js?v=20260918-safari-login"></script><script src="legacy/bootstrap.js?v=20260918-safari-login"></script><script>
/* BEGIN category-fix.js — PORTABLE INLINE */
// Compatibility for local/offline browsers where crypto.randomUUID is unavailable.
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID !== 'function') {
crypto.randomUUID = () => 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
const r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 3 | 8);
return v.toString(16);
});
}
(() => {
const categories = [
{ name: "Боксы", singular: "бокс", accent: "#d49a2a", prep: true },
{ name: "Посуда", singular: "позицию", accent: "#a78bfa", prep: false },
{ name: "Дополнения", singular: "позицию", accent: "#56a889", prep: false },
{ name: "Напитки", singular: "напиток", accent: "#4f9bd8", prep: false },
{ name: "Доставка", singular: "вариант доставки", accent: "#cf9f83", prep: false },
{ name: "Премиум", singular: "премиум позицию", accent: "#7a5aa6", prep: true }
];
// Visual order: Premium sits between Boxes and Drinks. Existing category ids stay unchanged.
const categoryDisplayOrder = [0, 5, 3, 2, 1, 4];
// --- Branding ---
const extraStyle = document.createElement("style");
extraStyle.textContent = `
.brand .real-logo,#preview .real-logo{width:42px;height:42px;object-fit:contain;vertical-align:middle;margin-right:9px}
#preview .real-logo{width:38px;height:38px}
.catalog .tile{position:relative;display:flex;flex-direction:column;gap:7px;text-align:left}
.catalog .tile span{line-height:1.2}
.catalog .tile .tile-price{margin-top:auto;color:#15364c;font-size:13px}.catalog .tile .tile-meta{display:block;color:#68717a;font-size:11px;line-height:1.25}.catalog .tile .tile-section{display:block;color:#8a6a25;font-size:10px;text-transform:uppercase;letter-spacing:.03em}.manager-card-meta{grid-column:2;color:#68717a;font-size:11px}
.catalog .tile.add{align-items:center;justify-content:center;text-align:center;min-height:152px}
.catalog-empty{grid-column:1/-1;border:1px dashed #cfd5da;border-radius:7px;padding:26px;text-align:center;color:#68717a;background:#fafbfb}
.category-badge{display:inline-flex;align-items:center;gap:6px;padding:5px 9px;border-radius:999px;background:#f2f4f5;color:#68717a;font-size:12px;margin:6px 0 12px}
.manager-card-price{font-weight:bold;color:#15364c}
.tile-price-wrap,.manager-price-wrap{display:flex;align-items:baseline;gap:7px;flex-wrap:wrap}.tile-price-wrap{margin-top:auto}.tile-price-wrap .tile-price{margin-top:0}.old-price{text-decoration:line-through;color:#9aa0aa!important;font-weight:500!important;font-size:.88em}.sale-badge{position:absolute;top:7px;right:7px;border-radius:999px;background:#b9473f;color:#fff;padding:4px 7px;font-size:9px;font-weight:800;letter-spacing:.04em;z-index:2}
.editor-category{margin:10px 0}
.ingredient-zone{margin-top:12px;padding-top:2px}
.catalog-help{margin-top:10px;color:#68717a;font-size:13px}
.clients-toolbar{display:flex;gap:10px;align-items:center;justify-content:space-between;margin:14px 0}
.clients-toolbar input{width:min(420px,100%)}
.client-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:12px}
.client-card{border:1px solid #d9dde0;border-radius:9px;padding:14px;background:#fff;box-shadow:0 1px 2px #0000000d}
.client-card h3{margin:0 0 5px;color:#15364c;font-size:17px}
.client-card p{margin:4px 0;color:#68717a}
.client-card .client-metrics{display:grid;grid-template-columns:1fr 1fr;gap:7px;margin:12px 0}
.client-card .metric{background:#f7f8f8;border-radius:7px;padding:9px}
.client-card .metric b{display:block;color:#15364c;font-size:16px}
.client-card .metric small{color:#7c858b}
.client-card .actions{margin-top:10px}
.client-summary{display:grid;grid-template-columns:repeat(4,minmax(120px,1fr));gap:10px;margin:14px 0 18px}
.client-summary .summary-box{background:#f7f8f8;border-radius:8px;padding:12px}
.client-summary b{display:block;font-size:19px;color:#15364c;margin-top:4px}
.client-detail-grid{display:grid;grid-template-columns:1fr 1fr;gap:18px}
.client-detail-grid h3{margin:4px 0 10px;color:#15364c}
.client-address{padding:8px 0;border-bottom:1px solid #e3e6e8}
.client-order{display:grid;grid-template-columns:70px 1fr 130px 120px 110px;gap:8px;align-items:center;padding:9px 0;border-bottom:1px solid #e3e6e8}
.client-order small{color:#68717a}
.client-empty{color:#879198;padding:12px 0}
.client-loyalty-mini{margin-top:10px;padding:8px 10px;border-radius:9px;background:#f5f4fb;color:#756f87;font-size:11px;display:flex;align-items:center;justify-content:space-between;gap:8px}.client-loyalty-mini.on{background:linear-gradient(135deg,#f2effd,#fff9e9);color:#4f466f}.client-loyalty-mini b{font-size:13px;color:#5b49ad}.client-loyalty-panel{margin:4px 0 18px;border:1px solid #e1def0;border-radius:14px;background:linear-gradient(145deg,#fcfbff,#fffdf8);padding:15px}.client-loyalty-top{display:flex;align-items:flex-start;justify-content:space-between;gap:14px;flex-wrap:wrap}.client-loyalty-kicker{text-transform:uppercase;letter-spacing:.11em;font-size:10px;color:#8b83a8;font-weight:800}.client-loyalty-current{font-size:30px;line-height:1;color:#4e3f92;margin:7px 0 4px}.client-loyalty-sub{color:#73798a;font-size:12px;margin:0}.client-loyalty-switch{display:flex;flex-direction:row;align-items:center;gap:7px;padding:7px 10px;border-radius:999px;background:#f1eff8;color:#4d4760;font-weight:700}.client-loyalty-switch input{width:17px;height:17px}.client-loyalty-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:14px}.client-loyalty-grid.three{grid-template-columns:repeat(3,1fr)}.client-loyalty-cardline{display:grid;grid-template-columns:1fr auto;gap:6px}.client-loyalty-cardline button{white-space:nowrap}.client-loyalty-progress{margin-top:12px;padding:10px 12px;border-radius:10px;background:#f5f6f8;color:#626a78;font-size:12px}.client-loyalty-rule{margin-top:8px;color:#81798e;font-size:11px}.client-loyalty-panel .actions{justify-content:flex-end}.client-loyalty-panel h3{margin:0}.client-loyalty-off{color:#979dab;font-weight:700}.client-loyalty-fields[hidden]{display:none!important}
@media(max-width:900px){.cats{overflow:auto}.cats button{flex:0 0 auto}.client-summary{grid-template-columns:1fr 1fr}.client-detail-grid{grid-template-columns:1fr}.client-order{grid-template-columns:60px 1fr 100px}.client-order .client-order-date,.client-order .client-order-status{display:none}.client-loyalty-grid,.client-loyalty-grid.three{grid-template-columns:1fr}}
`;
document.head.appendChild(extraStyle);
const brand = document.querySelector(".brand");
if (brand) window.CateriumBranding.renderSidebar();
const receiptLogo = document.querySelector("#preview h2");
if (receiptLogo) receiptLogo.innerHTML = ("<img class=\"real-logo\" src=\""+window.CateriumBranding.logoHTML()+"\" alt=\"Логотип компании\">"+window.CateriumBranding.nameHTML());
// --- Catalog migration: all positions live in the existing boxes array for compatibility ---
boxes = boxes.map(item => ({
...item,
category: Number.isInteger(item.category) ? item.category : 0,
price: Number(item.price || 0),
ingredients: Array.isArray(item.ingredients) ? item.ingredients : []
}));
// Catalogs belong to a workspace; newly created companies start empty.
window.SUN_OFFICIAL_CATALOG_VERSION = 'workspace-owned-v1';
window.SUN_OFFICIAL_CATALOG = [];
// Pull current human-readable box compositions from the public Solnce Catering catalog.
// The server endpoint is fixed to solnce-keytering.ru; custom URLs are never accepted.
function sunCatalogNormalizeName(value){
return String(value||'').toLowerCase().replace(/ё/g,'е').replace(/[«»“”"']/g,' ').replace(/(?:на\s+)?\d+\s*[-–—]\s*\d+\s*(?:гост(?:ей|я)|персон)/g,' ').replace(/\b\d+\s*(?:гост(?:ей|я)|персон)\b/g,' ').replace(/\bфуршетн(?:ый|ая|ое|ые)\b/g,' ').replace(/\b(?:бокс|набор)\b/g,' ').replace(/[^a-zа-я0-9№]+/gi,' ').replace(/\s+/g,' ').trim();
}
function sunCatalogBoxNo(value){const m=String(value||'').match(/№\s*0*(\d+)/i);return m?Number(m[1]):null;}
function sunCatalogTokens(value){return new Set(sunCatalogNormalizeName(value).split(' ').filter(x=>x.length>1 && !/^№?\d+$/.test(x)));}
function sunCatalogNameScore(a,b){
const na=sunCatalogNormalizeName(a),nb=sunCatalogNormalizeName(b); if(!na||!nb)return 0; if(na===nb)return 100;
const A=sunCatalogTokens(a),B=sunCatalogTokens(b),inter=[...A].filter(x=>B.has(x)).length,uni=new Set([...A,...B]).size;
let score=uni?inter/uni*70:0; const an=sunCatalogBoxNo(a),bn=sunCatalogBoxNo(b); if(an!==null&&bn!==null)score+=an===bn?25:-35;
if(na.includes(nb)||nb.includes(na))score+=12; return score;
}
function sunCatalogPieceCount(lines){let total=0;for(const line of lines||[]){const re=/(\d+(?:[.,]\d+)?)\s*(?:шт\.?|пор\.?|кус\.?)/ig;let m;while((m=re.exec(String(line||''))))total+=Number(String(m[1]).replace(',','.'))||0;}return Math.round(total);}
function sunFindCatalogItemForSiteProduct(product){
let best=null,bestScore=0; for(const item of boxes){
if(![0,5].includes(Number(item.category||0)))continue; if(!String(item.id||'').startsWith('catalog-20260902-'))continue;
const score=sunCatalogNameScore(item.name,product.title||product.name); if(score>bestScore){best=item;bestScore=score;}
}
return bestScore>=58?best:null;
}
async function sunSyncCatalogCompositionsFromSite(force=false){
if(window.SunCloudV2?.getWorkspace?.()?.id!=='30f65746-0c47-4cfe-9939-7f7c1501ae55')return {ok:true,skipped:true};
const now=Date.now(),last=Number(localStorage.sunCatalogCompositionSyncAt||0);
const premiumMissing=boxes.some(item=>Number(item.category||0)===5&&String(item.id||'').startsWith('catalog-20260902-')&&item.compositionSource!=='manual'&&!(Array.isArray(item.composition)&&item.composition.filter(Boolean).length));
if(!force&&!premiumMissing&&last&&now-last<6*60*60*1000)return {ok:true,skipped:true};
try{
const response=await fetch('/api/catalog-compositions?ts='+now+(force?'&force=1':''),{cache:'no-store'}); if(!response.ok)throw new Error('HTTP '+response.status);
const data=await response.json(); const products=Array.isArray(data.products)?data.products:[]; let changed=0,matched=0,matchedPremium=0,changedPremium=0;
for(const product of products){
const comp=Array.isArray(product.composition)?product.composition.map(x=>String(x||'').trim()).filter(Boolean):[]; if(!comp.length)continue;
const item=sunFindCatalogItemForSiteProduct(product); if(!item)continue; matched++; if(Number(item.category||0)===5)matchedPremium++;
if(item.compositionSource==='manual')continue;
let itemChanged=false;
if(JSON.stringify(item.composition||[])!==JSON.stringify(comp)){item.composition=comp;changed++;itemChanged=true;if(Number(item.category||0)===5)changedPremium++;}
const pc=sunCatalogPieceCount(comp); if(pc&&Number(item.pieces||0)!==pc){item.pieces=pc;itemChanged=true;}
item.compositionSource='solnce-keytering.ru'; item.compositionSyncedAt=new Date().toISOString();
if(itemChanged)window.sunClientOfferCatalogChanged?.(item.id);
}
localStorage.sunCatalogCompositionSyncAt=String(now); localStorage.sunCatalogCompositionSyncStatus=JSON.stringify({at:now,matched,changed,matchedPremium,changedPremium,received:products.length,source:data.source||'solnce-keytering.ru'});
if(changed)persist();
if(force) alert(changed?('Составы обновлены с сайта: '+changed+'. Премиум: '+changedPremium+'.'):('Сайт проверен. Новых изменений состава нет. Совпало позиций: '+matched+', Премиум: '+matchedPremium+'.'));
return {ok:true,changed,matched,matchedPremium,changedPremium,received:products.length};
}catch(error){
localStorage.sunCatalogCompositionSyncStatus=JSON.stringify({at:now,error:String(error?.message||error)});
if(force)alert('Не удалось получить составы с сайта. Проверьте интернет и запустите приложение через start-local-test.bat / server.js.\n\n'+String(error?.message||error));
return {ok:false,error};
}
}
window.sunSyncCatalogCompositionsFromSite=sunSyncCatalogCompositionsFromSite;
// Keep catalog composition updates, but defer network-heavy work until the first screen is idle.
// This avoids a noticeable startup pause on slower computers/connections.
const sunScheduleCatalogCompositionSync=()=>{
if(!navigator.onLine||document.hidden)return;
const run=()=>{if(navigator.onLine&&!document.hidden)sunSyncCatalogCompositionsFromSite(false).catch(()=>{});};
if('requestIdleCallback' in window)requestIdleCallback(run,{timeout:15000});else setTimeout(run,2500);
};
setTimeout(sunScheduleCatalogCompositionSync,8000);
let activeCategory = 0;
const tileArea = document.getElementById("tiles");
const categoryPanel = document.querySelector(".cats");
const catalogTitle = document.querySelector(".catalog h1");
const managerTitle = document.querySelector("#manager .dialog-head h2");
const managerAddButton = document.querySelector("#manager .primary");
// Manual composition refresh control removed from catalog manager.
const catalogCompositionSyncButton = document.createElement("span");
catalogCompositionSyncButton.hidden = true;
const editorDialog = document.querySelector("#editor .dialog");
const editorTitle = document.getElementById("editTitle");
const ingredientsHeading = [...editorDialog.querySelectorAll("h3")].find(x => ["Состав","Состав / ТТК"].includes(x.textContent.trim()));
if(ingredientsHeading) ingredientsHeading.textContent = "Состав / ТТК";
const ingredientsList = document.getElementById("ingredients");
const addIngredientButton = [...editorDialog.querySelectorAll("button")].find(x => x.textContent.includes("Добавить продукт"));
// Add category selector into the editor so a position can be moved between sections.
const categoryLabel = document.createElement("label");
categoryLabel.className = "editor-category";
categoryLabel.innerHTML = `Категория<select id="itemCategory">${categoryDisplayOrder.map(i => `<option value="${i}">${categories[i].name}</option>`).join("")}</select>`;
const nameLabel = document.getElementById("boxName").closest("label");
nameLabel.insertAdjacentElement("afterend", categoryLabel);
// Human-readable composition shown in client offer/PDF. Kept separate from prep ingredients.
const compositionLabel = document.createElement("label");
compositionLabel.id = "sunBoxCompositionLabel";
compositionLabel.className = "sun-box-composition-field";
compositionLabel.innerHTML = '<span>Состав для клиента</span><textarea id="boxCompositionText" rows="6" placeholder="Каждая позиция с новой строки"></textarea><small>Этот текст показывается в предложении клиенту и PDF. Заготовочные ингредиенты редактируются ниже отдельно.</small>';
const priceLabel = document.getElementById("boxOldPrice")?.closest("label") || document.getElementById("boxPrice")?.closest("label");
priceLabel?.insertAdjacentElement("afterend", compositionLabel);
// Wrap ingredient controls so they can be hidden for non-food positions.
const ingredientZone = document.createElement("div");
ingredientZone.className = "ingredient-zone";
window.SunSafe.insertBefore(ingredientsHeading.parentNode,ingredientZone,ingredientsHeading);
ingredientZone.appendChild(ingredientsHeading);
ingredientZone.appendChild(ingredientsList);
ingredientZone.appendChild(addIngredientButton);
const ingredientHelp = document.createElement("p");
ingredientHelp.className = "hint";
ingredientHelp.textContent = "Состав / ТТК используется для кухни и автоматического расчёта заготовок по заказу.";
ingredientZone.appendChild(ingredientHelp);
categoryPanel.innerHTML = categoryDisplayOrder.map(i => `<button type="button" data-category="${i}">${categories[i].name}</button>`).join("") + '<p>Выберите раздел. Любую позицию можно добавить, изменить или удалить.</p>';
const categoryButtons = [...categoryPanel.querySelectorAll("button")];
function moneyLocal(value) {
return typeof money === "function" ? money(value) : `${Math.round(Number(value || 0)).toLocaleString("ru-RU")}`;
}
function activeItems() {
return boxes.filter(item => Number(item.category || 0) === activeCategory);
}
function syncCategoryUI() {
const cat = categories[activeCategory];
if (catalogTitle) catalogTitle.textContent = cat.name;
categoryButtons.forEach(button => {
const categoryIndex = Number(button.dataset.category);
const isActive = categoryIndex === activeCategory;
button.style.background = categories[categoryIndex].accent;
button.style.color = "#fff";
button.style.opacity = isActive ? "1" : ".72";
button.style.transform = isActive ? "translateY(-1px)" : "none";
button.style.boxShadow = isActive ? "0 4px 10px #0002" : "none";
button.style.outline = isActive ? "2px solid #15364c" : "none";
button.style.outlineOffset = isActive ? "2px" : "0";
});
}
function compactOrderRowName(name) {
let text = String(name || "").trim().replace(/\s+/g, " ");
text = text.replace(/^(?:Фуршетный\s+)?бокс\s*№\s*(\d+)\s*(?:[—–-]\s*)?/i, "№ $1 ");
text = text.replace(/^№\s*(\d+)\s*(?:[—–-]\s*)?/i, "№ $1 ");
return text.trim();
}
function renderCatalog() {
syncCategoryUI();
const items = activeItems();
const cat = categories[activeCategory];
const addText = [0,5].includes(activeCategory) ? (activeCategory===0?"Добавить бокс":"Добавить премиум") : "Добавить позицию";
const cards = items.map(item => `
<button class="tile" type="button" onclick="add('${item.id}')" title="Добавить в заказ">
${item.photo ? `<img src="${item.photo}" alt="" loading="lazy" decoding="async">` : ("<div class=\"ph\"><img src=\""+window.CateriumBranding.logoHTML()+"\" class=\"sun-ph-logo\" alt=\"Логотип "+window.CateriumBranding.nameHTML()+"\"></div>")}
<span>${sunEsc(item.name)}</span>
${item.catalogSection ? `<small class="tile-section">${sunEsc(item.catalogSection)}</small>` : ""}
${item.weight ? `<small class="tile-meta">${sunEsc(item.weight)}</small>` : ""}
<span class="tile-price-wrap"><small class="tile-price">${moneyLocal(item.price || 0)}</small>${Number(item.oldPrice||0)>Number(item.price||0)?`<small class="old-price">${moneyLocal(item.oldPrice)}</small>`:""}</span>${Number(item.oldPrice||0)>Number(item.price||0)?`<span class="sale-badge">АКЦИЯ</span>`:""}
</button>`).join("");
const empty = items.length ? "" : `<div class="catalog-empty">В разделе «${cat.name}» пока нет позиций.<br>Нажмите «${addText}».</div>`;
tileArea.innerHTML = `<button class="tile add" type="button" onclick="editBox(null)"><br>${addText}</button>${cards}${empty}`;
if (draft.lines.length) {
const orderRows = draft.lines.map(line => {
const item = boxes.find(x => x.id == line.id);
if (!item) return "";
const qtyValue = Math.max(1, Number(line.qty || 1));
const priceValue = Number(item.price || 0);
const sum = priceValue * qtyValue;
return `<div class="sun-order-line-row">
<div class="sun-order-line-name" title="${sunAttr(item.name)}">${sunEsc(compactOrderRowName(item.name))}</div>
<div class="sun-order-line-qty"><button type="button" onclick="qty('${line.id}',${qtyValue - 1})"></button><input type="number" min="1" value="${qtyValue}" onchange="qty('${line.id}',+this.value)"><button type="button" onclick="qty('${line.id}',${qtyValue + 1})"></button></div>
<div class="sun-order-line-price">${moneyLocal(priceValue)}</div>
<div class="sun-order-line-sum"><b>${moneyLocal(sum)}</b></div>
</div>`;
}).join("");
lines.innerHTML = `<div class="sun-order-lines-table">
<div class="sun-order-line-head"><span>Наименование</span><span>Кол.</span><span>Цена</span><span>Стоимость</span></div>
<div class="sun-order-line-body">${orderRows}</div>
<div class="sun-order-line-total"><span>Всего</span><b>${moneyLocal(calcOrderTotal())}</b></div>
</div>`;
} else {
lines.innerHTML = '<p class="empty">Выберите позиции из каталога.</p>';
}
updateOrderSummary();
}
window.render = renderCatalog;
categoryButtons.forEach(button => {
button.onclick = () => {
activeCategory = Number(button.dataset.category);
renderCatalog();
};
});
window.openManager = () => {
const cat = categories[activeCategory];
managerTitle.textContent = cat.name;
managerAddButton.textContent = activeCategory === 0 ? "Добавить бокс" : (activeCategory===5 ? "Добавить премиум" : "Добавить позицию");
catalogCompositionSyncButton.style.display = "none";
const items = activeItems();
managerList.innerHTML = items.length ? items.map(item => `
<button type="button" onclick="editBox('${item.id}')">
${item.photo ? `<img src="${item.photo}" alt="" loading="lazy" decoding="async">` : ("<div class=\"ph\"><img src=\""+window.CateriumBranding.logoHTML()+"\" class=\"sun-ph-logo\" alt=\"Логотип "+window.CateriumBranding.nameHTML()+"\"></div>")}
<b>${sunEsc(item.name)}</b>
<span class="manager-price-wrap"><small class="manager-card-price">${moneyLocal(item.price || 0)}</small>${Number(item.oldPrice||0)>Number(item.price||0)?`<small class="old-price">${moneyLocal(item.oldPrice)}</small>`:""}</span>
${item.weight ? `<small class="manager-card-meta">${item.weight}</small>` : ""}
${categories[activeCategory].prep ? `<small>${item.ingredients.length} позиций в составе</small>` : ""}
</button>`).join("") : `<p class="empty">В разделе «${cat.name}» пока нет позиций.</p>`;
modal("manager");
};
window.editBox = (id) => {
edited = id ? structuredClone(boxes.find(x => x.id == id)) : {
id: "",
name: activeCategory === 0 ? "Новый бокс" : (activeCategory===5 ? "Новая премиум позиция" : "Новая позиция"),
category: activeCategory,
price: 0,
oldPrice: 0,
ingredients: []
};
if (!edited) return;
if (!Number.isInteger(edited.category)) edited.category = activeCategory;
if (!Array.isArray(edited.ingredients)) edited.ingredients = [];
const cat = categories[edited.category];
editorTitle.textContent = edited.id ? `Изменить: ${cat.name}` : (edited.category === 0 ? "Новый бокс" : (edited.category===5 ? "Новая премиум позиция" : "Новая позиция"));
boxName.value = edited.name || "";
boxPrice.value = Number(edited.price || 0);
boxOldPrice.value = Number(edited.oldPrice || 0);
itemCategory.value = String(edited.category);
const compositionText = document.getElementById("boxCompositionText");
if (compositionText) compositionText.value = (Array.isArray(edited.composition) ? edited.composition : []).join("\n");
compositionLabel.style.display = [0,5].includes(Number(edited.category)) ? "flex" : "none";
editPhoto.style.display = edited.photo ? "block" : "none";
editPhoto.src = edited.photo || "";
deleteBox.style.display = edited.id ? "block" : "none";
ingredientZone.style.display = cat.prep ? "block" : "none";
renderIngredients();
modal("editor");
};
itemCategory.onchange = () => {
edited.category = Number(itemCategory.value);
const cat = categories[edited.category];
ingredientZone.style.display = cat.prep ? "block" : "none";
compositionLabel.style.display = [0,5].includes(Number(edited.category)) ? "flex" : "none";
editorTitle.textContent = edited.id ? `Изменить: ${cat.name}` : (edited.category === 0 ? "Новый бокс" : (edited.category===5 ? "Новая премиум позиция" : "Новая позиция"));
};
window.saveBox = () => {
edited.name = boxName.value.trim();
edited.price = Math.max(0, Number(boxPrice.value || 0));
edited.oldPrice = Math.max(0, Number(boxOldPrice.value || 0));
if (!(edited.oldPrice > edited.price)) delete edited.oldPrice;
edited.sale = Boolean(edited.oldPrice && edited.oldPrice > edited.price);
edited.category = Number(itemCategory.value);
edited.ingredients = Array.isArray(edited.ingredients) ? edited.ingredients : [];
const compositionText = document.getElementById("boxCompositionText");
if ([0,5].includes(Number(edited.category)) && compositionText) {
const nextComposition = compositionText.value.split(/\r?\n/).map(x=>x.trim()).filter(Boolean);
const beforeComposition = JSON.stringify(Array.isArray(edited.composition)?edited.composition:[]);
edited.composition = nextComposition;
if (JSON.stringify(nextComposition) !== beforeComposition) edited.compositionSource = "manual";
}
if (!edited.name) return alert("Введите название.");
if (!edited.id) {
edited.id = sunUUID();
boxes.push(edited);
} else {
const index = boxes.findIndex(x => x.id == edited.id);
if (index >= 0) boxes[index] = edited;
}
activeCategory = edited.category;
persist();
closeModal("editor");
closeModal("manager");
renderCatalog();
renderOrders();
};
window.removeBox = () => {
if (!edited?.id) return;
if (!confirm(`Удалить «${edited.name}» из каталога?`)) return;
boxes = boxes.filter(x => x.id != edited.id);
// Keep old orders readable: lines referencing a deleted item are removed only from the current draft.
draft.lines = draft.lines.filter(x => x.id != edited.id);
persist();
closeModal("editor");
closeModal("manager");
renderCatalog();
renderOrders();
};
// Make preparation view safe for non-food catalog positions.
window.openPreview = () => {
const prepMap = {};
draft.lines.forEach(line => {
const item = boxes.find(x => x.id == line.id);
if (!item || !Array.isArray(item.ingredients)) return;
item.ingredients.forEach(part => {
const key = `${part[0]}|${part[2]}`;
prepMap[key] = (prepMap[key] || 0) + Number(part[1] || 0) * Number(line.qty || 0);
});
});
prepTitle.textContent = "Заготовки для: " + document.getElementById('event').value;
const entries = Object.entries(prepMap);
prep.innerHTML = entries.length ? entries.map(([key, value]) => {
const [product, unit] = key.split("|");
return `<p><b>${product}</b><span>${value} ${unit}</span></p>`;
}).join("") : '<p class="empty">В заказе нет позиций с составом для заготовок.</p>';
modal("preview");
};
// --- Clients ---
const nav = document.querySelector("nav");
const clientButton = document.createElement("button");
clientButton.textContent = "Клиенты";
nav.appendChild(clientButton);
const clientView = document.createElement("section");
clientView.id = "clients";
clientView.className = "view page";
clientView.innerHTML = `
<div class="catalog-head">
<div><h1>Клиенты</h1><p class="hint" style="margin:5px 0 0">База формируется автоматически из сохранённых заказов.</p></div>
</div>
<div class="clients-toolbar"><input id="client-search" type="search" placeholder="Поиск по имени, телефону или адресу"><span class="hint" id="client-count"></span></div>
<div class="client-grid" id="client-list"></div>`;
document.body.appendChild(clientView);
const clientModal = document.createElement("div");
clientModal.id = "client-card";
clientModal.className = "modal";
clientModal.innerHTML = `<div class="dialog"><div class="dialog-head"><h2 id="client-card-title">Клиент</h2><button type="button" onclick="closeModal('client-card')">×</button></div><div id="client-card-content"></div></div>`;
document.body.appendChild(clientModal);
function normalizePhone(value) {
return String(value || "").replace(/\D/g, "");
}
function clientKey(order) {
const phoneKey = normalizePhone(order.phone);
if (phoneKey) return `p:${phoneKey}`;
const nameKey = String(order.contact || "").trim().replace(/\s+/g, " ").toLowerCase();
return nameKey ? `n:${nameKey}` : "";
}
// --- Loyalty cards / personal cumulative rules ---
const CLIENT_LOYALTY_KEY = "sunClientLoyaltyV1";
const loyaltyDefaults = () => ({enabled:false,cardNumber:"",mode:"fixed",fixedDiscount:0,baseDiscount:0,stepAmount:50000,stepPercent:1,maxDiscount:10});
function readLoyaltyStore(){ try { const v=JSON.parse(localStorage.getItem(CLIENT_LOYALTY_KEY)||"{}"); return v&&typeof v==="object"?v:{}; } catch(_) { return {}; } }
function writeLoyaltyStore(value){ localStorage.setItem(CLIENT_LOYALTY_KEY,JSON.stringify(value||{})); }
function clientLoyalty(key){ return {...loyaltyDefaults(),...(readLoyaltyStore()[key]||{})}; }
function clampDiscount(value){ return Math.max(0,Math.min(100,Number(value||0))); }
function loyaltyDiscountFor(person, loyalty=clientLoyalty(person.key)){
if(!loyalty.enabled) return 0;
if(loyalty.mode!=="cumulative") return clampDiscount(loyalty.fixedDiscount);
const turnover=Math.max(0,Number(person.total||0));
const base=clampDiscount(loyalty.baseDiscount);
const step=Math.max(1,Number(loyalty.stepAmount||0));
const plus=Math.max(0,Number(loyalty.stepPercent||0));
const max=Math.max(base,clampDiscount(loyalty.maxDiscount));
return Math.min(max,base+Math.floor(turnover/step)*plus);
}
function loyaltyProgressText(person, loyalty){
if(!loyalty.enabled) return "Карта выключена. Скидка не рассчитывается.";
const discount=loyaltyDiscountFor(person,loyalty);
if(loyalty.mode!=="cumulative") return `Постоянная скидка ${discount.toLocaleString("ru-RU",{maximumFractionDigits:2})}%.`;
const turnover=Math.max(0,Number(person.total||0)),step=Math.max(1,Number(loyalty.stepAmount||0)),plus=Math.max(0,Number(loyalty.stepPercent||0)),max=Math.max(clampDiscount(loyalty.baseDiscount),clampDiscount(loyalty.maxDiscount));
if(discount>=max || plus<=0) return `Накоплено ${moneyLocal(turnover)} · достигнута максимальная скидка ${discount.toLocaleString("ru-RU",{maximumFractionDigits:2})}%.`;
const next=(Math.floor(turnover/step)+1)*step;
return `Накоплено ${moneyLocal(turnover)} · до следующего уровня осталось ${moneyLocal(Math.max(0,next-turnover))}.`;
}
function orderValue(order) {
if (typeof orderTotalValue === "function") return Number(orderTotalValue(order) || 0);
if (Number.isFinite(Number(order.total))) return Number(order.total);
return (order.lines || []).reduce((sum, line) => {
const item = boxes.find(x => x.id == line.id);
return sum + Number(item?.price || 0) * Number(line.qty || 0);
}, 0);
}
function collectClients() {
const repo=window.CateriumDataV1773?.clients||window.CateriumDataV1772?.clients||window.CateriumData?.clients;
const profiles=repo?.list?.({source:'auto'});
if(Array.isArray(profiles)&&profiles.length){
const result=[];
profiles.forEach(profile=>{
const key=String(profile?.key||'');if(!key)return;
const linked=orders.filter(order=>clientKey(order)===key);
if(!linked.length&&key.startsWith('o:'))return;
const valid=linked.filter(order=>order.status!=='Отменён');
const addresses=new Set([...(Array.isArray(profile.addresses)?profile.addresses:[]),profile.latestAddress,...linked.map(order=>order.address)].map(v=>String(v||'').trim()).filter(Boolean));
const lastDate=linked.reduce((max,order)=>{const stamp=`${order.date||''}T${order.time||'00:00'}`;return stamp>max?stamp:max},'');
const latest=[...linked].sort((a,b)=>`${b.date||''}T${b.time||''}`.localeCompare(`${a.date||''}T${a.time||''}`))[0]||{};
result.push({key,name:String(profile.name||latest.contact||'Без имени').trim()||'Без имени',phone:String(profile.phone||latest.phone||'').trim()||'—',addresses:[...addresses],orders:linked,total:valid.reduce((sum,order)=>sum+orderValue(order),0),paid:valid.reduce((sum,order)=>sum+Math.max(0,Number(order.prepayment||0)),0),activeOrders:valid.length,lastDate});
});
return result.sort((a,b)=>(b.lastDate||'').localeCompare(a.lastDate||''));
}
const people = {};
orders.forEach(order => {
const key = clientKey(order);
if (!key) return;
if (!people[key]) people[key] = {key,name:String(order.contact || "Без имени").trim() || "Без имени",phone:String(order.phone || "").trim() || "—",addresses:new Set(),orders:[],total:0,paid:0,activeOrders:0,lastDate:""};
const person = people[key];
if (order.contact && String(order.contact).trim()) person.name = String(order.contact).trim();
if (order.phone && String(order.phone).trim()) person.phone = String(order.phone).trim();
if (order.address && String(order.address).trim()) person.addresses.add(String(order.address).trim());
person.orders.push(order);
if (order.status !== "Отменён") { person.total += orderValue(order); person.paid += Math.max(0, Number(order.prepayment || 0)); person.activeOrders++; }
const dateKey = `${order.date || ""}T${order.time || "00:00"}`; if (dateKey > person.lastDate) person.lastDate = dateKey;
});
return Object.values(people).map(person => ({ ...person, addresses: [...person.addresses] })).sort((a, b) => (b.lastDate || "").localeCompare(a.lastDate || ""));
}
function renderClientSummary(list) {
// Верхняя сводка удалена: список клиентов начинается сразу с поиска и карточек.
}
function renderClients(filter = "") {
const all = collectClients();
renderClientSummary(all);
const query = String(filter || "").trim().toLowerCase();
const list = query ? all.filter(person => {
const haystack = [person.name, person.phone, ...person.addresses].join(" ").toLowerCase();
return haystack.includes(query);
}) : all;
document.getElementById("client-count").textContent = query ? `Найдено: ${list.length}` : `Клиентов: ${list.length}`;
document.getElementById("client-list").innerHTML = list.length ? list.map(person => {
const loyalty=clientLoyalty(person.key),discount=loyaltyDiscountFor(person,loyalty);
return `
<article class="client-card">
<h3>${person.name}</h3>
<p>${person.phone}</p>
<p>${person.addresses[0] || "Адрес пока не указан"}${person.addresses.length > 1 ? ` · ещё ${person.addresses.length - 1}` : ""}</p>
<div class="client-metrics">
<div class="metric"><b>${person.activeOrders}</b><small>заказов</small></div>
<div class="metric"><b>${moneyLocal(person.total)}</b><small>на сумму</small></div>
</div>
<div class="client-loyalty-mini ${loyalty.enabled?'on':''}"><span>${loyalty.enabled?`Карта ${loyalty.cardNumber||'без номера'}`:'Лояльность не настроена'}</span>${loyalty.enabled?`<b>${discount.toLocaleString('ru-RU',{maximumFractionDigits:2})}%</b>`:''}</div>
<div class="actions"><button class="outline" type="button" onclick="openClientCard('${encodeURIComponent(person.key)}')">Открыть карточку</button></div>
</article>`;
}).join("") : '<p class="empty">Клиенты не найдены.</p>';
}
window.addEventListener('caterium:clients-server-refresh',()=>{if(clientView.classList.contains('on'))renderClients(document.getElementById("client-search")?.value||'')});
window.openClientCard = encodedKey => {
const key = decodeURIComponent(encodedKey);
const person = collectClients().find(x => x.key === key);
if (!person) return;
const loyalty=clientLoyalty(key),discount=loyaltyDiscountFor(person,loyalty);
document.getElementById("client-card-title").textContent = person.name;
const ordersHtml = [...person.orders].sort((a, b) => `${b.date || ""}T${b.time || ""}`.localeCompare(`${a.date || ""}T${a.time || ""}`)).map(order => `
<div class="client-order">
<b>№ ${order.id}</b>
<span>${order.event || "Заказ"}<small style="display:block">${order.address || "Без адреса"}</small></span>
<span class="client-order-date">${order.date || "—"} ${order.time || ""}</span>
<span class="client-order-status">${order.status || "Новый"}</span>
<button class="outline" type="button" onclick="openClientOrder(${order.id})">Открыть</button>
</div>`).join("");
document.getElementById("client-card-content").innerHTML = `
<div class="client-summary">
<div class="summary-box"><small>Телефон</small><b style="font-size:16px">${person.phone}</b></div>
<div class="summary-box"><small>Заказов</small><b>${person.activeOrders}</b></div>
<div class="summary-box"><small>Общий оборот</small><b>${moneyLocal(person.total)}</b></div>
<div class="summary-box"><small>Текущая скидка</small><b>${loyalty.enabled?discount.toLocaleString('ru-RU',{maximumFractionDigits:2})+'%':'—'}</b></div>
</div>
<section class="client-loyalty-panel">
<div class="client-loyalty-top"><div><div class="client-loyalty-kicker">Карта лояльности</div><div class="client-loyalty-current">${loyalty.enabled?discount.toLocaleString('ru-RU',{maximumFractionDigits:2})+'%':'—'}</div><p class="client-loyalty-sub">${loyalty.enabled?(loyalty.mode==='cumulative'?'Накопительная система':'Постоянная скидка'):'Правило пока не включено'}</p></div><label class="client-loyalty-switch"><input id="clientLoyaltyEnabled" type="checkbox" ${loyalty.enabled?'checked':''}> Активна</label></div>
<div class="client-loyalty-grid"><label>Номер карты<div class="client-loyalty-cardline"><input id="clientLoyaltyCard" value="${loyalty.cardNumber||''}" placeholder="Например SUN-00001234"><button class="outline" type="button" onclick="generateClientLoyaltyCard()">Создать</button></div></label><label>Тип программы<select id="clientLoyaltyMode" onchange="toggleClientLoyaltyFields()"><option value="fixed" ${loyalty.mode==='fixed'?'selected':''}>Постоянная скидка</option><option value="cumulative" ${loyalty.mode==='cumulative'?'selected':''}>Накопительная</option></select></label></div>
<div id="clientLoyaltyFixedFields" class="client-loyalty-fields client-loyalty-grid"><label>Скидка, %<input id="clientLoyaltyFixed" type="number" min="0" max="100" step="0.5" value="${Number(loyalty.fixedDiscount||0)}"></label></div>
<div id="clientLoyaltyCumulativeFields" class="client-loyalty-fields client-loyalty-grid three"><label>Стартовая скидка, %<input id="clientLoyaltyBase" type="number" min="0" max="100" step="0.5" value="${Number(loyalty.baseDiscount||0)}"></label><label>Каждые, ₽<input id="clientLoyaltyStepAmount" type="number" min="1" step="1000" value="${Number(loyalty.stepAmount||50000)}"></label><label>Добавлять, %<input id="clientLoyaltyStepPercent" type="number" min="0" max="100" step="0.5" value="${Number(loyalty.stepPercent||1)}"></label><label>Максимальная скидка, %<input id="clientLoyaltyMax" type="number" min="0" max="100" step="0.5" value="${Number(loyalty.maxDiscount||10)}"></label></div>
<div class="client-loyalty-progress">${loyaltyProgressText(person,loyalty)}<div class="client-loyalty-rule">Для накопительной системы учитывается весь оборот клиента по заказам без статуса «Отменён». Правило задаётся отдельно для этого клиента.</div></div>
<div class="actions"><button class="primary" type="button" onclick="saveClientLoyalty('${encodeURIComponent(key)}')">Сохранить правило</button></div>
</section>
<div class="client-detail-grid">
<div><h3>Адреса</h3>${person.addresses.length ? person.addresses.map(address => `<div class="client-address">${address}</div>`).join("") : '<p class="client-empty">Адресов пока нет.</p>'}</div>
<div><h3>История заказов</h3><div>${ordersHtml || '<p class="client-empty">Заказов пока нет.</p>'}</div></div>
</div>`;
modal("client-card");
toggleClientLoyaltyFields();
};
window.toggleClientLoyaltyFields = () => {
const mode=document.getElementById('clientLoyaltyMode')?.value||'fixed';
const fixed=document.getElementById('clientLoyaltyFixedFields'),cum=document.getElementById('clientLoyaltyCumulativeFields');
if(fixed) fixed.hidden=mode!=='fixed'; if(cum) cum.hidden=mode!=='cumulative';
};
window.generateClientLoyaltyCard = () => {
const input=document.getElementById('clientLoyaltyCard'); if(!input)return;
const rnd=String(Date.now()).slice(-6)+String(Math.floor(Math.random()*100)).padStart(2,'0'); input.value=`SUN-${rnd}`;
};
window.saveClientLoyalty = encodedKey => {
const key=decodeURIComponent(encodedKey),store=readLoyaltyStore();
const val=id=>document.getElementById(id)?.value||'',num=id=>Number(val(id)||0);
const mode=val('clientLoyaltyMode')==='cumulative'?'cumulative':'fixed';
store[key]={enabled:Boolean(document.getElementById('clientLoyaltyEnabled')?.checked),cardNumber:val('clientLoyaltyCard').trim(),mode,fixedDiscount:clampDiscount(num('clientLoyaltyFixed')),baseDiscount:clampDiscount(num('clientLoyaltyBase')),stepAmount:Math.max(1,num('clientLoyaltyStepAmount')||50000),stepPercent:Math.max(0,num('clientLoyaltyStepPercent')),maxDiscount:clampDiscount(num('clientLoyaltyMax'))};
if(store[key].maxDiscount<store[key].baseDiscount) store[key].maxDiscount=store[key].baseDiscount;
writeLoyaltyStore(store);
renderClients(document.getElementById('client-search')?.value||'');
window.openClientCard(encodedKey);
try{window.SunEnterprise?.toast('Правило лояльности сохранено.','success');}catch(_){}
};
window.openClientOrder = id => {
closeModal("client-card");
if (typeof openOrder === "function") openOrder(id);
};
document.getElementById("client-search").addEventListener("input", event => renderClients(event.target.value));
clientButton.onclick = () => {
document.querySelectorAll(".view").forEach(x => x.classList.remove("on"));
clientView.classList.add("on");
[...nav.querySelectorAll("button")].forEach(x => x.classList.remove("on"));
clientButton.classList.add("on");
document.getElementById("client-search").value = "";
renderClients();
window.CateriumDataV1773?.clients?.refreshServer?.({reason:'clients-open'}).catch(()=>{});
};
// --- Money / finance module ---
const FINANCE_KEY = 'sunFinanceRecordsV2';
let financeRecords = (() => {
try { return JSON.parse(localStorage.getItem(FINANCE_KEY) || '[]'); }
catch (_) { return []; }
})();
let moneyTab = 'income';
let editingFinanceId = null;
const moneyButton = document.createElement("button");
moneyButton.textContent = "Деньги";
nav.appendChild(moneyButton);
const moneyView = document.createElement("section");
moneyView.id = "money";
moneyView.className = "view page";
moneyView.innerHTML = `
<div class="catalog-head money-title-row">
<div><h1>Деньги</h1><p class="hint" style="margin:5px 0 0">Доходы по заказам, закупки со склада, расходы, зарплаты и прочие операции.</p></div>
<button class="primary" id="moneyAddRecord">Добавить операцию</button>
</div>
<div class="money-period">
<label>Период<select id="moneyPeriod"><option value="month">Этот месяц</option><option value="all">Всё время</option><option value="custom">Свой период</option></select></label>
<label>С<input id="moneyFrom" type="date"></label>
<label>По<input id="moneyTo" type="date"></label>
</div>
<div class="money-summary" id="moneySummary"></div>
<div class="subnav" id="money-tabs">
<button data-money="income" class="on">Доходы</button>
<button data-money="expenses">Расходы</button>
<button data-money="salary">Зарплаты</button>
<button data-money="other">Прочее</button>
</div>
<div id="money-content"></div>`;
document.body.appendChild(moneyView);
const financeStyle = document.createElement('style');
financeStyle.textContent = `
.money-title-row{gap:12px;align-items:flex-start}.money-period{display:grid;grid-template-columns:180px 160px 160px;gap:10px;margin:16px 0}
.money-summary{display:grid;grid-template-columns:repeat(6,minmax(120px,1fr));gap:9px;margin:0 0 16px}.money-card{border:1px solid var(--l);border-radius:8px;padding:11px;background:#fafafa;min-height:72px}.money-card small{display:block;color:#68717a;margin-bottom:6px}.money-card b{font-size:18px;color:var(--n)}.money-card.profit b{font-size:21px}.money-card.negative b{color:#a33}
.finance-toolbar{display:flex;justify-content:space-between;align-items:center;gap:10px;margin:8px 0 12px;flex-wrap:wrap}.finance-table{width:100%;border-collapse:collapse;min-width:760px}.finance-table th,.finance-table td{padding:9px 8px;border-bottom:1px solid var(--l);text-align:left;vertical-align:top}.finance-table th{font-size:12px;color:#68717a}.finance-table .num{text-align:right;white-space:nowrap}.finance-source{display:block;color:#879198;font-size:11px;margin-top:2px}.finance-pos{color:#287a49}.finance-neg{color:#a33}.finance-actions{display:flex;gap:5px;justify-content:flex-end}.finance-form{display:grid;grid-template-columns:1fr 1fr;gap:10px}.finance-form .wide{grid-column:1/-1}.money-note{padding:10px 12px;background:#fffaf0;border-left:3px solid var(--g);margin:12px 0;color:#68717a;font-size:13px}
@media(max-width:1100px){.money-summary{grid-template-columns:repeat(3,1fr)}}
@media(max-width:700px){.money-title-row{display:block}.money-title-row>button{margin-top:10px;width:100%}.money-period{grid-template-columns:1fr 1fr}.money-period label:first-child{grid-column:1/-1}.money-summary{grid-template-columns:1fr 1fr}.finance-form{grid-template-columns:1fr}.finance-form .wide{grid-column:auto}}
`;
document.head.appendChild(financeStyle);
const financeModal = document.createElement('div');
financeModal.id = 'finance-modal';
financeModal.className = 'modal';
financeModal.innerHTML = `<div class="dialog"><div class="dialog-head"><h2 id="financeModalTitle">Финансовая операция</h2><button type="button" onclick="closeModal('finance-modal')">×</button></div>
<div class="finance-form">
<label>Раздел<select id="financeKind"><option value="expense">Расход</option><option value="salary">Зарплата</option><option value="other_income">Прочий доход</option><option value="other_expense">Прочий расход</option></select></label>
<label>Дата<input id="financeDate" type="date"></label>
<label>Сумма<input id="financeAmount" type="number" min="0" step="0.01"></label>
<label>Категория<input id="financeCategory" placeholder="Продукты, реклама, аренда…"></label>
<label class="wide">Описание<input id="financeDescription" placeholder="Что это за операция"></label>
<label class="wide">Комментарий<textarea id="financeNote" rows="3"></textarea></label>
</div>
<div class="actions"><button class="primary" id="financeSave">Сохранить</button><button class="danger" id="financeDelete" style="display:none">Удалить</button></div></div>`;
document.body.appendChild(financeModal);
const financeModalTitle = document.getElementById('financeModalTitle');
const financeKind = document.getElementById('financeKind');
const financeDate = document.getElementById('financeDate');
const financeAmount = document.getElementById('financeAmount');
const financeCategory = document.getElementById('financeCategory');
const financeDescription = document.getElementById('financeDescription');
const financeNote = document.getElementById('financeNote');
const financeSave = document.getElementById('financeSave');
const financeDelete = document.getElementById('financeDelete');
const moneyEsc = value => String(value ?? '').replace(/[&<>"']/g, ch => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[ch]));
const moneyDateLabel = raw => {
if (!raw) return '—';
const d = new Date(raw + 'T00:00:00');
return Number.isNaN(d.getTime()) ? raw : d.toLocaleDateString('ru-RU');
};
const financeMoney = value => moneyLocal(Number(value || 0));
const saveFinance = () => localStorage.setItem(FINANCE_KEY, JSON.stringify(financeRecords));
const orderFinanceTotal = order => typeof orderTotalValue === 'function' ? Number(orderTotalValue(order) || 0) : Number(order.total || 0);
const orderPaid = order => Math.min(orderFinanceTotal(order), Math.max(0, Number(order.prepayment || 0)));
function stockPurchaseRows() {
let moves = [];
try { moves = JSON.parse(localStorage.getItem('sunStockMoves') || '[]'); } catch (_) {}
return moves.filter(m => m.type === 'in' && Number(m.qty || 0) > 0 && Number(m.cost || 0) > 0).map(m => ({
id: `stock:${m.id}`, source: 'stock', date: m.date || (m.createdAt || '').slice(0,10), amount: Number(m.qty || 0) * Number(m.cost || 0),
category: 'Закупка на склад', description: m.productName || 'Продукт', note: m.note || '', qty: Number(m.qty || 0), unit: m.unit || '', unitCost: Number(m.cost || 0)
}));
}
function currentMoneyRange() {
const mode = document.getElementById('moneyPeriod')?.value || 'month';
if (mode === 'all') return { from:'', to:'' };
if (mode === 'custom') return { from: document.getElementById('moneyFrom')?.value || '', to: document.getElementById('moneyTo')?.value || '' };
const now = new Date();
const y = now.getFullYear(), m = String(now.getMonth()+1).padStart(2,'0');
const last = new Date(y, now.getMonth()+1, 0).getDate();
return { from:`${y}-${m}-01`, to:`${y}-${m}-${String(last).padStart(2,'0')}` };
}
const inRange = (dateValue, range = currentMoneyRange()) => (!range.from || dateValue >= range.from) && (!range.to || dateValue <= range.to);
function financeSnapshot() {
const range = currentMoneyRange();
const validOrders = orders.filter(o => (o.status || 'Новый') !== 'Отменён' && inRange(o.date || '', range));
const sales = validOrders.reduce((s,o)=>s+orderFinanceTotal(o),0);
const received = validOrders.reduce((s,o)=>s+orderPaid(o),0);
const stockPurchases = stockPurchaseRows().filter(r=>inRange(r.date || '', range));
const stockCost = stockPurchases.reduce((s,r)=>s+r.amount,0);
const manual = financeRecords.filter(r=>inRange(r.date || '', range));
const expenses = manual.filter(r=>r.kind==='expense');
const salaries = manual.filter(r=>r.kind==='salary');
const otherIncome = manual.filter(r=>r.kind==='other_income');
const otherExpense = manual.filter(r=>r.kind==='other_expense');
const expenseTotal = stockCost + expenses.reduce((s,r)=>s+Number(r.amount||0),0);
const salaryTotal = salaries.reduce((s,r)=>s+Number(r.amount||0),0);
const otherIncomeTotal = otherIncome.reduce((s,r)=>s+Number(r.amount||0),0);
const otherExpenseTotal = otherExpense.reduce((s,r)=>s+Number(r.amount||0),0);
const accruedProfit = sales + otherIncomeTotal - expenseTotal - salaryTotal - otherExpenseTotal;
const cashBalance = received + otherIncomeTotal - expenseTotal - salaryTotal - otherExpenseTotal;
return { range, validOrders, sales, received, stockPurchases, stockCost, manual, expenses, salaries, otherIncome, otherExpense, expenseTotal, salaryTotal, otherIncomeTotal, otherExpenseTotal, accruedProfit, cashBalance };
}
function renderMoneySummary(data) {
const pending = Math.max(0, data.sales - data.received);
document.getElementById('moneySummary').innerHTML = `
<div class="money-card"><small>Продажи по заказам</small><b>${financeMoney(data.sales)}</b></div>
<div class="money-card"><small>Получено предоплат</small><b>${financeMoney(data.received)}</b></div>
<div class="money-card"><small>К получению</small><b>${financeMoney(pending)}</b></div>
<div class="money-card"><small>Расходы + закупки</small><b>${financeMoney(data.expenseTotal)}</b></div>
<div class="money-card"><small>Зарплаты</small><b>${financeMoney(data.salaryTotal)}</b></div>
<div class="money-card profit ${data.accruedProfit < 0 ? 'negative' : ''}"><small>Расчётная прибыль</small><b>${financeMoney(data.accruedProfit)}</b></div>`;
}
function financeTable(rows, columns, emptyText) {
if (!rows.length) return `<p class="empty">${emptyText}</p>`;
return `<div style="overflow:auto"><table class="finance-table"><thead><tr>${columns.map(c=>`<th class="${c.num?'num':''}">${c.label}</th>`).join('')}<th></th></tr></thead><tbody>${rows.map(row=>`<tr>${columns.map(c=>`<td class="${c.num?'num':''}">${c.render(row)}</td>`).join('')}<td>${row.source==='manual'?`<div class="finance-actions"><button class="mini" onclick="editFinanceRecord('${row.id}')">Изменить</button><button class="mini danger" onclick="deleteFinanceRecord('${row.id}')">Удалить</button></div>`:''}</td></tr>`).join('')}</tbody></table></div>`;
}
function renderIncome(data) {
const rows = [...data.validOrders].sort((a,b)=>(b.date||'').localeCompare(a.date||''));
const columns = [
{label:'Дата',render:o=>moneyDateLabel(o.date)},
{label:'Заказ',render:o=>`<b>№${moneyEsc(o.id)} · ${moneyEsc(o.event||'Без названия')}</b><span class="finance-source">${moneyEsc(o.contact||'Клиент не указан')} · ${moneyEsc(o.status||'Новый')}</span>`},
{label:'Сумма',num:true,render:o=>`<b>${financeMoney(orderFinanceTotal(o))}</b>`},
{label:'Получено',num:true,render:o=>`<span class="finance-pos">${financeMoney(orderPaid(o))}</span>`},
{label:'Остаток',num:true,render:o=>financeMoney(Math.max(0,orderFinanceTotal(o)-orderPaid(o)))}
];
document.getElementById('money-content').innerHTML = `<div class="money-note">Доходы по заказам считаются по полной стоимости всех неотменённых заказов. «Получено» сейчас равно сохранённой предоплате; остаток показан отдельно, чтобы не считать его уже полученными деньгами.</div>${financeTable(rows,columns,'Заказов за выбранный период нет.')}`;
}
function renderExpenses(data) {
const manualRows = data.expenses.map(r=>({...r,source:'manual'}));
const rows = [...data.stockPurchases, ...manualRows].sort((a,b)=>(b.date||'').localeCompare(a.date||''));
const columns = [
{label:'Дата',render:r=>moneyDateLabel(r.date)},
{label:'Категория',render:r=>`<b>${moneyEsc(r.category||'Расход')}</b><span class="finance-source">${r.source==='stock'?'Автоматически со склада':'Ручная запись'}</span>`},
{label:'Описание',render:r=>`${moneyEsc(r.description||'—')}${r.note?`<span class="finance-source">${moneyEsc(r.note)}</span>`:''}`},
{label:'Сумма',num:true,render:r=>`<b class="finance-neg"> ${financeMoney(r.amount)}</b>`}
];
document.getElementById('money-content').innerHTML = `<div class="finance-toolbar"><span class="hint">Приход товара на склад с указанной закупочной ценой автоматически попадает сюда как закупка.</span><button class="outline" onclick="newFinanceRecord('expense')">Добавить расход</button></div>${financeTable(rows,columns,'Расходов за выбранный период нет.')}`;
}
function renderSalary(data) {
const rows = data.salaries.map(r=>({...r,source:'manual'})).sort((a,b)=>(b.date||'').localeCompare(a.date||''));
const columns = [
{label:'Дата',render:r=>moneyDateLabel(r.date)},
{label:'Сотрудник / категория',render:r=>`<b>${moneyEsc(r.category||'Зарплата')}</b>`},
{label:'Описание',render:r=>`${moneyEsc(r.description||'—')}${r.note?`<span class="finance-source">${moneyEsc(r.note)}</span>`:''}`},
{label:'Сумма',num:true,render:r=>`<b class="finance-neg"> ${financeMoney(r.amount)}</b>`}
];
document.getElementById('money-content').innerHTML = `<div class="finance-toolbar"><span class="hint">Здесь учитываются выплаты сотрудникам и подрядчикам.</span><button class="outline" onclick="newFinanceRecord('salary')">Добавить зарплату</button></div>${financeTable(rows,columns,'Выплат зарплат за выбранный период нет.')}`;
}
function renderOther(data) {
const rows = [...data.otherIncome.map(r=>({...r,source:'manual'})), ...data.otherExpense.map(r=>({...r,source:'manual'}))].sort((a,b)=>(b.date||'').localeCompare(a.date||''));
const columns = [
{label:'Дата',render:r=>moneyDateLabel(r.date)},
{label:'Тип',render:r=>r.kind==='other_income'?'<b class="finance-pos">Прочий доход</b>':'<b class="finance-neg">Прочий расход</b>'},
{label:'Категория',render:r=>moneyEsc(r.category||'Прочее')},
{label:'Описание',render:r=>`${moneyEsc(r.description||'—')}${r.note?`<span class="finance-source">${moneyEsc(r.note)}</span>`:''}`},
{label:'Сумма',num:true,render:r=>`<b class="${r.kind==='other_income'?'finance-pos':'finance-neg'}">${r.kind==='other_income'?'+':''} ${financeMoney(r.amount)}</b>`}
];
document.getElementById('money-content').innerHTML = `<div class="finance-toolbar"><span class="hint">Например: возвраты, компенсации, аренда оборудования, разовые поступления.</span><button class="outline" onclick="newFinanceRecord('other_income')">Добавить операцию</button></div>${financeTable(rows,columns,'Прочих операций за выбранный период нет.')}`;
}
function renderMoney() {
const period = document.getElementById('moneyPeriod');
const custom = period?.value === 'custom';
document.getElementById('moneyFrom').disabled = !custom;
document.getElementById('moneyTo').disabled = !custom;
const data = financeSnapshot();
renderMoneySummary(data);
[...document.querySelectorAll('#money-tabs button')].forEach(b=>b.classList.toggle('on',b.dataset.money===moneyTab));
if (moneyTab === 'income') renderIncome(data);
else if (moneyTab === 'expenses') renderExpenses(data);
else if (moneyTab === 'salary') renderSalary(data);
else renderOther(data);
}
window.newFinanceRecord = (kind = 'expense') => {
editingFinanceId = null;
financeModalTitle.textContent = 'Новая финансовая операция';
financeKind.value = kind;
financeDate.value = new Date().toISOString().slice(0,10);
financeAmount.value = '';
financeCategory.value = kind === 'salary' ? 'Сотрудник' : '';
financeDescription.value = '';
financeNote.value = '';
financeDelete.style.display = 'none';
modal('finance-modal');
};
window.editFinanceRecord = id => {
const r = financeRecords.find(x=>x.id===id); if (!r) return;
editingFinanceId = id;
financeModalTitle.textContent = 'Изменить финансовую операцию';
financeKind.value = r.kind; financeDate.value = r.date || ''; financeAmount.value = Number(r.amount||0); financeCategory.value = r.category || ''; financeDescription.value = r.description || ''; financeNote.value = r.note || '';
financeDelete.style.display = 'inline-block';
modal('finance-modal');
};
window.deleteFinanceRecord = id => {
const r = financeRecords.find(x=>x.id===id); if (!r || !confirm('Удалить финансовую операцию?')) return;
financeRecords = financeRecords.filter(x=>x.id!==id); saveFinance(); renderMoney();
};
financeSave.onclick = () => {
const amount = Math.max(0,Number(financeAmount.value||0));
if (!financeDate.value) return alert('Укажите дату.');
if (amount <= 0) return alert('Введите сумму больше нуля.');
const item = { id: editingFinanceId || sunUUID(), kind: financeKind.value, date: financeDate.value, amount, category: financeCategory.value.trim(), description: financeDescription.value.trim(), note: financeNote.value.trim(), createdAt: new Date().toISOString() };
const idx = financeRecords.findIndex(x=>x.id===item.id);
if (idx >= 0) financeRecords[idx] = {...financeRecords[idx],...item}; else financeRecords.push(item);
saveFinance(); closeModal('finance-modal'); renderMoney();
};
financeDelete.onclick = () => editingFinanceId && window.deleteFinanceRecord(editingFinanceId);
document.getElementById('moneyAddRecord').onclick = () => newFinanceRecord(moneyTab==='salary'?'salary':moneyTab==='income'?'other_income':moneyTab==='salary'?'salary':moneyTab==='other'?'other_income':'expense');
document.getElementById('moneyPeriod').onchange = renderMoney;
document.getElementById('moneyFrom').onchange = renderMoney;
document.getElementById('moneyTo').onchange = renderMoney;
[...moneyView.querySelectorAll('#money-tabs button')].forEach(button => button.onclick = () => { moneyTab = button.dataset.money; renderMoney(); });
moneyButton.onclick = () => {
document.querySelectorAll(".view").forEach(x => x.classList.remove("on"));
moneyView.classList.add("on");
[...nav.querySelectorAll("button")].forEach(x => x.classList.remove("on"));
moneyButton.classList.add("on");
renderMoney();
};
// --- Order colors ---
const rowColors = { "Новый": "#f3f8fb", "В работе": "#f1effa", "Собран": "#fff6dc", "Передан курьеру": "#fff9db", "Отдан заказчику": "#eaf6ed", "Отменён": "#fbeeee" };
const savedRenderOrders = window.renderOrders;
window.renderOrders = () => {
savedRenderOrders();
[...document.querySelectorAll("#ordersList .row")].forEach((row, index) => row.style.background = rowColors[orders[index]?.status || "Новый"] || "#fff");
};
const savedStatus = window.status;
window.status = (id, value) => { savedStatus(id, value); window.renderOrders(); renderClients(document.getElementById("client-search")?.value || ""); };
// Refresh clients after saving or deleting an order.
const savedSaveOrder = window.saveOrder;
window.saveOrder = () => { savedSaveOrder(); renderClients(document.getElementById("client-search")?.value || ""); };
const savedDeleteOrderById = window.deleteOrderById;
if (typeof savedDeleteOrderById === "function") window.deleteOrderById = id => { savedDeleteOrderById(id); renderClients(document.getElementById("client-search")?.value || ""); };
// --- Branded order documents / receipt ---
const printStyle = document.createElement("style");
printStyle.textContent = "@media print { #details { display:none !important; } body{background:#fff!important} }";
document.head.appendChild(printStyle);
const documentStyle = document.createElement("style");
documentStyle.textContent = `
.sun-doc{font-family:Arial,sans-serif;color:#172f41;background:#fff;max-width:900px;margin:0 auto}
.sun-doc *{box-sizing:border-box}
.sun-doc-head{display:flex;justify-content:space-between;gap:24px;align-items:flex-start;padding:18px 0 16px;border-bottom:3px solid #c99a32}
.sun-doc-brand{display:flex;align-items:center;gap:12px}
.sun-doc-brand img{width:112px;height:72px;object-fit:contain}
.sun-doc-brand strong{display:block;font-size:22px;color:#15364c}.sun-doc-brand small{display:block;margin-top:4px;color:#6d7981}
.sun-doc-title{text-align:right}.sun-doc-title h2{margin:0;color:#15364c;font-size:26px}.sun-doc-title p{margin:6px 0 0;color:#68717a}
.sun-doc-badge{display:inline-block;margin-top:8px;padding:6px 10px;border:1px solid #d9dde0;border-radius:999px;font-size:12px;font-weight:700}
.sun-doc-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px 28px;margin:18px 0;padding:14px 16px;background:#f8f6f0;border-radius:8px}
.sun-doc-field{padding:5px 0;border-bottom:1px solid #e0ddd4}.sun-doc-field span{display:block;color:#77828a;font-size:11px;text-transform:uppercase;letter-spacing:.04em;margin-bottom:3px}.sun-doc-field b{font-size:14px;color:#203040;font-weight:600}
.sun-doc table{width:100%;border-collapse:collapse;margin-top:14px}.sun-doc th{background:#15364c;color:#fff;font-size:12px;text-align:left;padding:9px}.sun-doc td{border-bottom:1px solid #d9dde0;padding:10px 9px;vertical-align:top}.sun-doc th:nth-child(n+3),.sun-doc td:nth-child(n+3){text-align:right;white-space:nowrap}
.sun-doc .doc-num{width:44px;text-align:center!important}.sun-doc .doc-name{text-align:left!important}.sun-doc .doc-sub{display:block;font-size:11px;color:#78838b;margin-top:3px}
.sun-doc-summary{width:min(360px,100%);margin:18px 0 0 auto}.sun-doc-summary p{display:flex;justify-content:space-between;margin:0;padding:7px 0;border-bottom:1px solid #e0e3e5}.sun-doc-summary .grand{font-size:19px;font-weight:700;color:#15364c;border-top:2px solid #c99a32;border-bottom:0;padding-top:10px}
.sun-doc-note{margin-top:18px;padding:12px 14px;border-left:4px solid #c99a32;background:#fffaf0}.sun-doc-note b{display:block;margin-bottom:5px}
.sun-doc-footer{display:grid;grid-template-columns:1fr 1fr;gap:40px;margin-top:34px;padding-top:14px;border-top:1px solid #d9dde0;color:#68717a;font-size:12px}.sun-doc-sign{margin-top:30px;border-top:1px solid #50606b;padding-top:5px;color:#50606b}
.sun-doc-actions{display:flex;gap:8px;margin-top:20px;justify-content:flex-end}
.sun-receipt{max-width:720px}.sun-receipt .sun-doc-head{padding-top:4px}.sun-receipt .sun-doc-grid{grid-template-columns:1fr;margin-top:14px}.sun-receipt .sun-doc-footer{grid-template-columns:1fr 1fr}
@media(max-width:700px){.sun-doc-head{flex-direction:column}.sun-doc-title{text-align:left}.sun-doc-grid{grid-template-columns:1fr}.sun-doc-footer{grid-template-columns:1fr}.sun-doc-brand img{width:88px;height:58px}}
@media print{
@page{size:A4 portrait;margin:8mm}
.sun-doc-actions{display:none!important}
.sun-doc{max-width:none;font-size:11px}
.sun-doc-head{padding:0 0 8px;gap:12px}
.sun-doc-brand img{width:64px;height:42px}
.sun-doc-brand strong{font-size:15px}.sun-doc-brand small{margin-top:1px;font-size:10px}
.sun-doc-title h2{font-size:17px}.sun-doc-title p{margin-top:2px;font-size:11px}
.sun-doc-grid{display:grid!important;grid-template-columns:none!important;grid-auto-flow:column;grid-auto-columns:1fr;gap:4px 10px;margin:8px 0;padding:6px 8px;background:#fff;border:1px solid #e0ddd4}
.sun-doc-field{padding:0;border-bottom:0}
.sun-doc-field span{font-size:8px;margin-bottom:1px}
.sun-doc-field b{font-size:10px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.sun-doc table{margin-top:6px}
.sun-doc th{background:#15364c!important;color:#fff!important;-webkit-print-color-adjust:exact;print-color-adjust:exact;font-size:9px;padding:4px 5px}
.sun-doc td{padding:3px 5px;font-size:10px}
.sun-doc .doc-sub{font-size:8px;margin-top:1px}
.sun-doc-summary{width:min(260px,100%);margin-top:8px}
.sun-doc-summary p{padding:3px 0;font-size:10px}
.sun-doc-summary .grand{font-size:13px;padding-top:5px}
.sun-doc-note{margin-top:8px;padding:6px 8px;font-size:10px;background:#fffaf0!important;-webkit-print-color-adjust:exact;print-color-adjust:exact}
.sun-doc-footer{grid-template-columns:1fr 1fr;gap:20px;margin-top:14px;padding-top:8px;font-size:9px}
.sun-doc-sign{margin-top:14px;padding-top:3px}
}
`;
document.head.appendChild(documentStyle);
const escDoc = value => String(value ?? "").replace(/[&<>\"']/g, ch => ({'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',"'":'&#39;'}[ch]));
const docMoney = n => Number(n || 0).toLocaleString('ru-RU') + ' ₽';
const docDate = raw => {
if (!raw) return '—';
const d = new Date(raw + 'T00:00:00');
return Number.isNaN(d.getTime()) ? raw : d.toLocaleDateString('ru-RU');
};
const currentRows = () => (draft.lines || []).map((line,index) => {
const item = boxes.find(x => String(x.id) === String(line.id));
const direct = Number(line?.price);
const price = Number.isFinite(direct) && direct >= 0 ? direct : Number(item?.price || 0);
const qty = Math.max(0, Number(line.qty || 0));
const name = item?.name || line?.name || `Позиция ${line?.id ?? ''}`;
const category = item?.category ?? line?.category ?? 'Боксы';
return {index:index+1,name,category,qty,price,sum:price*qty};
}).filter(row => row.qty > 0);
const currentDocData = () => {
const rows=currentRows();
const total=rows.reduce((s,r)=>s+r.sum,0);
const paid=Math.max(0,Number(prepayment.value || draft.prepayment || 0));
return {rows,total,paid,balance:Math.max(0,total-paid)};
};
const docActions = () => `<div class="sun-doc-actions"><button class="primary" onclick="print()">Печать / PDF</button><button class="outline" onclick="closeModal('preview')">Закрыть</button></div>`;
function showOrderDocument(){
const {rows,total,paid,balance}=currentDocData();
const number=draft.id || 'черновик';
const dialog=document.querySelector('#preview .dialog');
dialog.innerHTML=`<div class="sun-doc">
<div class="sun-doc-head"><div class="sun-doc-brand"><img src="${window.CateriumBranding.logoHTML()}" alt="${window.CateriumBranding.nameHTML()}"><div><strong>${window.CateriumBranding.nameHTML()}</strong><small>${window.CateriumBranding.cityHTML()}</small></div></div><div class="sun-doc-title"><h2>Заказ № ${escDoc(number)}</h2><p>${escDoc(document.getElementById('event').value || '')}</p><span class="sun-doc-badge">${escDoc(draft.status || 'Новый')}</span></div></div>
<div class="sun-doc-grid">
<div class="sun-doc-field"><span>Клиент</span><b>${escDoc(contact.value || '—')}</b></div>
<div class="sun-doc-field"><span>Телефон</span><b>${escDoc(phone.value || '—')}</b></div>
<div class="sun-doc-field"><span>Дата и время выдачи</span><b>${escDoc(docDate(date.value))} · ${escDoc(time.value || '—')}</b></div>
<div class="sun-doc-field"><span>Адрес доставки</span><b>${escDoc(address.value || '—')}</b></div>
</div>
<table><thead><tr><th class="doc-num">№</th><th class="doc-name">Позиция</th><th>Цена</th><th>Кол.</th><th>Сумма</th></tr></thead><tbody>${rows.length?rows.map(r=>`<tr><td class="doc-num">${r.index}</td><td class="doc-name"><b>${escDoc(r.name)}</b><span class="doc-sub">${escDoc(r.category)}</span></td><td>${docMoney(r.price)}</td><td>${r.qty}</td><td><b>${docMoney(r.sum)}</b></td></tr>`).join(''):`<tr><td colspan="5">В заказе пока нет позиций.</td></tr>`}</tbody></table>
<div class="sun-doc-summary"><p><span>Стоимость заказа</span><b>${docMoney(total)}</b></p><p><span>Предоплата</span><b>${docMoney(paid)}</b></p><p class="grand"><span>Остаток к оплате</span><span>${docMoney(balance)}</span></p></div>
${note.value?`<div class="sun-doc-note"><b>Комментарий к заказу</b>${escDoc(note.value)}</div>`:''}
<div class="sun-doc-footer"><div><b>${window.CateriumBranding.nameHTML()}</b><br>Спасибо, что выбрали нас.<div class="sun-doc-sign">Ответственный / подпись</div></div><div>Заказчик подтверждает состав заказа, дату, время и адрес выдачи.<div class="sun-doc-sign">Заказчик / подпись</div></div></div>
${docActions()}
</div>`;
modal('preview');
}
function showReceiptDocument(){
const {rows,total,paid,balance}=currentDocData();
const number=draft.id || '—';
const dialog=document.querySelector('#preview .dialog');
dialog.innerHTML=`<div class="sun-doc sun-receipt">
<div class="sun-doc-head"><div class="sun-doc-brand"><img src="${window.CateriumBranding.logoHTML()}" alt="${window.CateriumBranding.nameHTML()}"><div><strong>${window.CateriumBranding.nameHTML()}</strong><small>${window.CateriumBranding.cityHTML()}</small></div></div><div class="sun-doc-title"><h2>Товарный чек № ${escDoc(number)}</h2><p>${escDoc(docDate(date.value))} · ${escDoc(time.value || '')}</p></div></div>
<div class="sun-doc-grid"><div class="sun-doc-field"><span>Покупатель</span><b>${escDoc(contact.value || '—')}${phone.value?' · '+escDoc(phone.value):''}</b></div>${address.value?`<div class="sun-doc-field"><span>Адрес</span><b>${escDoc(address.value)}</b></div>`:''}</div>
<table><thead><tr><th class="doc-num">№</th><th class="doc-name">Наименование</th><th>Цена</th><th>Кол.</th><th>Сумма</th></tr></thead><tbody>${rows.map(r=>`<tr><td class="doc-num">${r.index}</td><td class="doc-name">${escDoc(r.name)}</td><td>${docMoney(r.price)}</td><td>${r.qty}</td><td>${docMoney(r.sum)}</td></tr>`).join('')}</tbody></table>
<div class="sun-doc-summary"><p><span>Итого</span><b>${docMoney(total)}</b></p><p><span>Оплачено</span><b>${docMoney(paid)}</b></p><p class="grand"><span>К оплате</span><span>${docMoney(balance)}</span></p></div>
${note.value?`<div class="sun-doc-note"><b>Примечание</b>${escDoc(note.value)}</div>`:''}
<div class="sun-doc-footer"><div><div class="sun-doc-sign">Продавец / подпись</div></div><div><div class="sun-doc-sign">Покупатель / подпись</div></div></div>
${docActions()}
</div>`;
modal('preview');
}
const orderDocButton=document.createElement('button');
orderDocButton.className='outline'; orderDocButton.textContent='Бланк заказа'; orderDocButton.onclick=showOrderDocument;
// Hidden helper: the order blank remains available to the Print center, but is not shown in the New order action bar.
orderDocButton.hidden=true; orderDocButton.setAttribute('aria-hidden','true'); orderDocButton.tabIndex=-1; orderDocButton.style.setProperty('display','none','important');
const receiptButton=document.createElement('button');
receiptButton.className='outline'; receiptButton.textContent='Товарный чек'; receiptButton.onclick=showReceiptDocument;
const actionBar=[...document.querySelectorAll('#new .actions')].find(a=>[...a.querySelectorAll('button')].some(b=>b.textContent.trim()==='Сохранить'));
actionBar.appendChild(orderDocButton); actionBar.appendChild(receiptButton);
renderCatalog();
})();
/* ===== STOCK MODULE v1 ===== */
(() => {
const stockView = document.getElementById('stock');
if (!stockView || stockView.dataset.upgraded === '1') return;
stockView.dataset.upgraded = '1';
const STOCK_KEY = 'sunStock';
const MOVES_KEY = 'sunStockMoves';
const SUPPLIERS_KEY = 'sunSuppliers';
let stock = JSON.parse(localStorage.getItem(STOCK_KEY) || '[]');
let stockMoves = JSON.parse(localStorage.getItem(MOVES_KEY) || '[]');
let editingStockId = null;
let movementStockId = null;
let stockTab = 'balances';
const stockCss = document.createElement('style');
stockCss.textContent = `
.stock-toolbar{display:flex;gap:8px;align-items:center;justify-content:space-between;flex-wrap:wrap;margin:14px 0}
.stock-toolbar .left,.stock-toolbar .right{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
.stock-toolbar input{min-width:260px}
.stock-summary{display:grid;grid-template-columns:repeat(4,minmax(150px,1fr));gap:10px;margin:14px 0}
.stock-summary .summary-box{background:#fafafa;border:1px solid var(--l);border-radius:8px;padding:12px}
.stock-summary small{display:block;color:#68717a;margin-bottom:5px}.stock-summary b{font-size:20px;color:var(--n)}
.stock-table{width:100%;border-collapse:collapse;background:#fff}.stock-table th,.stock-table td{padding:10px 8px;border-bottom:1px solid var(--l);text-align:left;vertical-align:middle}.stock-table th{font-size:12px;color:#68717a;background:#fafafa;position:sticky;top:0}.stock-table td.num,.stock-table th.num{text-align:right}.stock-table tr.low{background:#fff7ed}.stock-table tr.zero{background:#fff0f0}.stock-table .stock-actions{white-space:nowrap;display:flex;gap:5px;justify-content:flex-end}.stock-table .stock-actions button{padding:5px 8px}
.stock-name b{display:block}.stock-name small{color:#68717a}.stock-badge{display:inline-block;padding:3px 7px;border-radius:999px;background:#eef2f4;font-size:12px}.stock-badge.warn{background:#fff0d5;color:#8a5b00}.stock-badge.danger{background:#fde7e7;color:#9c2d2d}.stock-positive{color:#2b6e43;font-weight:bold}.stock-negative{color:#a33;font-weight:bold}
.stock-form{display:grid;grid-template-columns:1.4fr .65fr .8fr .75fr;gap:10px}.stock-form label{margin-bottom:8px}.movement-form{display:grid;grid-template-columns:1fr 1fr;gap:10px}.movement-form .wide{grid-column:1/-1}
.stock-supplier b{display:block}.stock-supplier small{display:block;color:#68717a;margin-top:2px}.stock-supplier .inactive{color:#a36a00}.stock-balances-table{min-width:980px}.stock-needs-table{min-width:880px}
.need-order{margin:12px 0;border:1px solid var(--l);border-radius:8px;overflow:hidden}.need-order-head{display:flex;justify-content:space-between;gap:12px;padding:10px 12px;background:#fafafa}.need-order table{margin:0}.moves-list{display:grid;gap:7px}.move-row{display:grid;grid-template-columns:120px 1fr 110px 140px 1fr;gap:8px;padding:9px;border-bottom:1px solid var(--l);align-items:center}.move-row small{color:#68717a}.stock-empty{padding:20px 0;color:#879198}
@media(max-width:900px){.stock-summary{grid-template-columns:1fr 1fr}.stock-form,.movement-form{grid-template-columns:1fr}.movement-form .wide{grid-column:auto}.stock-table{font-size:13px}.stock-balances-table th:nth-child(4),.stock-balances-table td:nth-child(4),.stock-balances-table th:nth-child(5),.stock-balances-table td:nth-child(5),.stock-balances-table th:nth-child(6),.stock-balances-table td:nth-child(6){display:none}.move-row{grid-template-columns:1fr 1fr}.move-row span:last-child{grid-column:1/-1}.stock-toolbar input{min-width:0;width:100%}}
`;
document.head.appendChild(stockCss);
const stockEsc = value => String(value ?? '').replace(/[&<>"']/g, ch => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[ch]));
function getSuppliers() {
try {
const list = JSON.parse(localStorage.getItem(SUPPLIERS_KEY) || '[]');
return Array.isArray(list) ? list.map(s => ({ ...s, id: String(s.id || ''), active: s.active !== false })) : [];
} catch (_) { return []; }
}
function supplierById(id) {
return getSuppliers().find(s => String(s.id) === String(id || '')) || null;
}
function supplierOptions(selected = '') {
const current = String(selected || '');
const list = getSuppliers().filter(s => s.active !== false || s.id === current).sort((a, b) => String(a.name || '').localeCompare(String(b.name || ''), 'ru'));
return `<option value="">Без поставщика</option>${list.map(s => `<option value="${stockEsc(s.id)}" ${s.id === current ? 'selected' : ''}>${stockEsc(s.name || 'Без названия')}${s.active === false ? ' · неактивен' : ''}</option>`).join('')}`;
}
function saveStock() {
localStorage.setItem(STOCK_KEY, JSON.stringify(stock));
localStorage.setItem(MOVES_KEY, JSON.stringify(stockMoves));
try { window.dispatchEvent(new CustomEvent('sun:stock-changed')); } catch (_) {}
}
function skuKey(name, unit) {
return `${String(name || '').trim().toLowerCase()}|${String(unit || '').trim().toLowerCase()}`;
}
function syncProductsFromCatalog() {
const existing = new Map(stock.map(p => [skuKey(p.name, p.unit), p]));
let changed = false;
boxes.forEach(item => {
(item.ingredients || []).forEach(part => {
const name = String(part[0] || '').trim();
const unit = String(part[2] || 'шт.').trim() || 'шт.';
if (!name) return;
const key = skuKey(name, unit);
if (!existing.has(key)) {
const product = { id: sunUUID(), name, unit, qty: 0, cost: 0, min: 0, supplierId: '', lastPurchaseDate: '', lastPurchasePrice: 0 };
stock.push(product);
existing.set(key, product);
changed = true;
}
});
});
if (changed) saveStock();
}
// Normalize legacy values.
stock = stock.map(p => ({
id: p.id || sunUUID(), name: String(p.name || 'Без названия'), unit: String(p.unit || 'шт.'),
qty: Number(p.qty || 0), cost: Number(p.cost || 0), min: Number(p.min || 0),
supplierId: String(p.supplierId || ''), lastPurchaseDate: String(p.lastPurchaseDate || ''), lastPurchasePrice: Number(p.lastPurchasePrice || 0)
}));
syncProductsFromCatalog();
saveStock();
stockView.innerHTML = `
<div class="catalog-head"><div><h1>Склад</h1><p class="hint" style="margin:5px 0 0">Остатки, закупочные цены, движения и автоматическая потребность по активным заказам.</p></div></div>
<div class="stock-summary" id="stockSummary"></div>
<div class="subnav" id="stockTabs">
<button class="on" data-stock-tab="balances">Остатки</button>
<button data-stock-tab="needs">Потребность по заказам</button>
<button data-stock-tab="moves">Движения</button>
</div>
<div id="stockContent"></div>`;
const productModal = document.createElement('div');
productModal.id = 'stock-product-modal';
productModal.className = 'modal';
productModal.innerHTML = `<div class="dialog"><div class="dialog-head"><h2 id="stockProductTitle">Продукт</h2><button type="button" onclick="closeModal('stock-product-modal')">×</button></div>
<div class="stock-form">
<label>Продукт<input id="stockName"></label>
<label>Ед. изм.<input id="stockUnit" placeholder="кг / г / шт."></label>
<label>Текущая закупочная цена<input id="stockCost" type="number" min="0" step="0.01"></label>
<label>Минимальный остаток<input id="stockMin" type="number" min="0" step="0.01"></label>
<label>Основной поставщик<select id="stockSupplier"><option value="">Без поставщика</option></select></label>
<label>Последняя закупка<input id="stockLastPurchaseDate" type="date"></label>
<label>Цена последней закупки<input id="stockLastPurchasePrice" type="number" min="0" step="0.01"></label>
</div>
<div class="actions"><button class="primary" id="stockSaveProduct">Сохранить</button><button class="danger" id="stockDeleteProduct">Удалить</button></div></div>`;
document.body.appendChild(productModal);
const movementModal = document.createElement('div');
movementModal.id = 'stock-movement-modal';
movementModal.className = 'modal';
movementModal.innerHTML = `<div class="dialog"><div class="dialog-head"><h2 id="stockMovementTitle">Движение</h2><button type="button" onclick="closeModal('stock-movement-modal')">×</button></div>
<div class="movement-form">
<label>Операция<select id="stockMoveType"><option value="in">Приход</option><option value="out">Списание</option><option value="set">Инвентаризация (установить остаток)</option></select></label>
<label>Количество<input id="stockMoveQty" type="number" min="0" step="0.01" value="1"></label>
<label>Цена за единицу<input id="stockMoveCost" type="number" min="0" step="0.01"></label>
<label>Дата<input id="stockMoveDate" type="date"></label>
<label>Поставщик<select id="stockMoveSupplier"><option value="">Без поставщика</option></select></label>
<label class="wide">Комментарий<input id="stockMoveNote" placeholder="Накладная, партия, причина списания…"></label>
</div>
<div class="actions"><button class="primary" id="stockSaveMove">Провести</button></div></div>`;
document.body.appendChild(movementModal);
const fmt = value => (typeof money === 'function' ? money(value) : `${Number(value || 0).toLocaleString('ru-RU')}`);
const qtyFmt = value => Number(value || 0).toLocaleString('ru-RU', { maximumFractionDigits: 3 });
function productByKey(name, unit) {
return stock.find(p => skuKey(p.name, p.unit) === skuKey(name, unit));
}
function activeOrders() {
return orders.filter(o => !['Отменён', 'Отдан заказчику'].includes(o.status || 'Новый'));
}
function aggregateNeed(orderList = activeOrders()) {
const map = {};
orderList.forEach(order => {
(order.lines || []).forEach(line => {
const item = boxes.find(x => x.id == line.id);
if (!item) return;
(item.ingredients || []).forEach(part => {
const name = String(part[0] || '').trim();
const unit = String(part[2] || 'шт.').trim();
const key = skuKey(name, unit);
if (!map[key]) map[key] = { name, unit, qty: 0 };
map[key].qty += Number(part[1] || 0) * Number(line.qty || 0);
});
});
});
return Object.values(map).sort((a, b) => a.name.localeCompare(b.name, 'ru'));
}
function renderStockSummary() {
const low = stock.filter(p => Number(p.qty) <= Number(p.min) && Number(p.min) > 0).length;
const zero = stock.filter(p => Number(p.qty) <= 0).length;
const value = stock.reduce((sum, p) => sum + Number(p.qty || 0) * Number(p.cost || 0), 0);
const need = aggregateNeed().reduce((sum, n) => {
const p = productByKey(n.name, n.unit);
return sum + Math.max(0, n.qty - Number(p?.qty || 0)) * Number(p?.cost || 0);
}, 0);
document.getElementById('stockSummary').innerHTML = `
<div class="summary-box"><small>Позиций на складе</small><b>${stock.length}</b></div>
<div class="summary-box"><small>Нулевой / минусовой остаток</small><b>${zero}</b></div>
<div class="summary-box"><small>Стоимость остатков</small><b>${fmt(value)}</b></div>
<div class="summary-box"><small>Ориентир закупки под заказы</small><b>${fmt(need)}</b></div>`;
}
function renderBalances(filter = '') {
const q = String(filter || '').trim().toLowerCase();
const supplierMap = new Map(getSuppliers().map(s => [String(s.id), s]));
const list = stock.filter(p => {
const supplier = supplierMap.get(String(p.supplierId || ''));
const haystack = `${p.name} ${p.unit} ${supplier?.name || ''} ${supplier?.phone || ''}`.toLowerCase();
return !q || haystack.includes(q);
}).sort((a, b) => a.name.localeCompare(b.name, 'ru'));
document.getElementById('stockContent').innerHTML = `
<div class="stock-toolbar"><div class="left"><input id="stockSearch" type="search" placeholder="Поиск продукта или поставщика" value="${stockEsc(filter || '')}"><button class="primary" id="stockAddProduct">Добавить продукт</button></div><div class="right"><button class="outline" id="stockSync">Обновить из составов боксов</button></div></div>
${list.length ? `<div style="overflow:auto"><table class="stock-table stock-balances-table"><thead><tr><th>Продукт</th><th>Поставщик</th><th class="num">Остаток</th><th class="num">Мин.</th><th class="num">Закуп. цена</th><th class="num">Стоимость</th><th></th></tr></thead><tbody>${list.map(p => {
const isZero = p.qty <= 0, isLow = !isZero && p.min > 0 && p.qty <= p.min;
const supplier = supplierMap.get(String(p.supplierId || ''));
const supplierMeta = [supplier?.phone || '', p.lastPurchaseDate ? `закупка ${p.lastPurchaseDate}` : ''].filter(Boolean).join(' · ');
return `<tr class="${isZero ? 'zero' : isLow ? 'low' : ''}"><td class="stock-name"><b>${stockEsc(p.name)}</b><small>${stockEsc(p.unit)}</small></td><td class="stock-supplier">${supplier ? `<b>${stockEsc(supplier.name || 'Без названия')}</b><small class="${supplier.active === false ? 'inactive' : ''}">${stockEsc(supplierMeta || (supplier.active === false ? 'Неактивен' : ''))}</small>` : '<span class="hint">Не назначен</span>'}</td><td class="num"><span class="stock-badge ${isZero ? 'danger' : isLow ? 'warn' : ''}">${qtyFmt(p.qty)} ${stockEsc(p.unit)}</span></td><td class="num">${qtyFmt(p.min)} ${stockEsc(p.unit)}</td><td class="num">${fmt(p.cost)}</td><td class="num"><b>${fmt(p.qty * p.cost)}</b></td><td><div class="stock-actions"><button class="mini" onclick="stockMovement('${stockEsc(p.id)}','in')"> Приход</button><button class="mini" onclick="stockMovement('${stockEsc(p.id)}','out')"> Списать</button><button class="mini" onclick="editStockProduct('${stockEsc(p.id)}')">Изменить</button></div></td></tr>`;
}).join('')}</tbody></table></div>` : '<p class="stock-empty">Продукты не найдены.</p>'}`;
document.getElementById('stockSearch').oninput = e => renderBalances(e.target.value);
document.getElementById('stockAddProduct').onclick = () => editStockProduct(null);
document.getElementById('stockSync').onclick = () => { syncProductsFromCatalog(); renderStock(); };
}
function renderNeeds() {
const needs = aggregateNeed();
const orderCount = activeOrders().length;
const estimated = needs.reduce((sum, n) => {
const p = productByKey(n.name, n.unit);
return sum + Math.max(0, n.qty - Number(p?.qty || 0)) * Number(p?.cost || 0);
}, 0);
const supplierMap = new Map(getSuppliers().map(s => [String(s.id), s]));
document.getElementById('stockContent').innerHTML = `
<div class="stock-toolbar"><div class="left"><b>Активных заказов: ${orderCount}</b><span class="hint">Учитываются все, кроме «Отдан заказчику» и «Отменён».</span></div><div class="right"><b>Ориентир закупки: ${fmt(estimated)}</b></div></div>
${needs.length ? `<div style="overflow:auto"><table class="stock-table stock-needs-table"><thead><tr><th>Продукт</th><th>Поставщик</th><th class="num">Нужно</th><th class="num">Есть</th><th class="num">Не хватает</th><th class="num">Цена</th><th class="num">Закупить на</th></tr></thead><tbody>${needs.map(n => {
const p = productByKey(n.name, n.unit);
const supplier = supplierMap.get(String(p?.supplierId || ''));
const have = Number(p?.qty || 0), deficit = Math.max(0, n.qty - have), cost = Number(p?.cost || 0);
return `<tr class="${deficit > 0 ? 'low' : ''}"><td class="stock-name"><b>${stockEsc(n.name)}</b><small>${stockEsc(n.unit)}</small></td><td>${supplier ? `<b>${stockEsc(supplier.name)}</b><small style="display:block;color:#68717a">${stockEsc(supplier.phone || '')}</small>` : '<span class="hint">Не назначен</span>'}</td><td class="num">${qtyFmt(n.qty)} ${stockEsc(n.unit)}</td><td class="num">${qtyFmt(have)} ${stockEsc(n.unit)}</td><td class="num"><span class="${deficit > 0 ? 'stock-negative' : 'stock-positive'}">${qtyFmt(deficit)} ${stockEsc(n.unit)}</span></td><td class="num">${fmt(cost)}</td><td class="num"><b>${fmt(deficit * cost)}</b></td></tr>`;
}).join('')}</tbody></table></div>` : '<p class="stock-empty">По активным заказам сейчас нет потребности в продуктах.</p>'}
<h3 style="margin-top:24px">По заказам</h3>${renderOrderNeeds()}`;
}
function renderOrderNeeds() {
const list = activeOrders().sort((a, b) => `${a.date || ''}T${a.time || ''}`.localeCompare(`${b.date || ''}T${b.time || ''}`));
if (!list.length) return '<p class="stock-empty">Активных заказов нет.</p>';
return list.map(order => {
const needs = aggregateNeed([order]);
return `<div class="need-order"><div class="need-order-head"><div><b>Заказ №${order.id} · ${order.event || 'Без названия'}</b><div class="hint">${order.date || '—'} ${order.time || ''} · ${order.status || 'Новый'}</div></div><button class="outline" onclick="writeOffOrderStock(${order.id})">Списать продукты по заказу</button></div>
${needs.length ? `<div style="overflow:auto"><table class="stock-table"><tbody>${needs.map(n => `<tr><td>${n.name}</td><td class="num">${qtyFmt(n.qty)} ${n.unit}</td></tr>`).join('')}</tbody></table></div>` : '<div class="stock-empty" style="padding:10px 12px">В заказе нет позиций с составом.</div>'}</div>`;
}).join('');
}
function renderMoves() {
const list = [...stockMoves].sort((a, b) => `${b.date || ''}${b.createdAt || ''}`.localeCompare(`${a.date || ''}${a.createdAt || ''}`));
document.getElementById('stockContent').innerHTML = `<div class="stock-toolbar"><div class="left"><b>История движений</b></div><div class="right"><button class="outline" id="clearMoves">Очистить историю</button></div></div>
<div class="moves-list">${list.length ? list.map(m => {
const p = stock.find(x => x.id === m.productId);
const supplier = supplierById(m.supplierId);
const supplierName = m.supplierName || supplier?.name || '';
const label = m.type === 'in' ? 'Приход' : m.type === 'out' ? 'Списание' : 'Инвентаризация';
const cls = m.type === 'in' ? 'stock-positive' : m.type === 'out' ? 'stock-negative' : '';
const note = [supplierName ? `Поставщик: ${supplierName}` : '', m.note || ''].filter(Boolean).join(' · ');
return `<div class="move-row"><span>${stockEsc(m.date || '—')}</span><span><b>${stockEsc(m.productName || p?.name || 'Удалённый продукт')}</b><small>${stockEsc(m.unit || p?.unit || '')}</small></span><span class="${cls}">${label}<br><b>${m.type === 'out' ? '' : m.type === 'in' ? '+' : ''}${qtyFmt(m.qty)}</b></span><span>${m.cost ? fmt(m.cost) + '/ед.' : '—'}</span><span>${stockEsc(note)}</span></div>`;
}).join('') : '<p class="stock-empty">Движений пока нет.</p>'}</div>`;
document.getElementById('clearMoves').onclick = () => { if (confirm('Очистить историю движений склада? Остатки продуктов не изменятся.')) { stockMoves = []; saveStock(); renderStock(); } };
}
function renderStock() {
syncProductsFromCatalog();
renderStockSummary();
document.querySelectorAll('#stockTabs button').forEach(b => b.classList.toggle('on', b.dataset.stockTab === stockTab));
if (stockTab === 'balances') renderBalances();
else if (stockTab === 'needs') renderNeeds();
else renderMoves();
}
document.querySelectorAll('#stockTabs button').forEach(button => button.onclick = () => {
stockTab = button.dataset.stockTab;
renderStock();
});
window.editStockProduct = id => {
editingStockId = id;
const p = id ? stock.find(x => x.id === id) : null;
document.getElementById('stockProductTitle').textContent = p ? 'Изменить продукт' : 'Новый продукт';
stockName.value = p?.name || '';
stockUnit.value = p?.unit || 'шт.';
stockCost.value = Number(p?.cost || 0);
stockMin.value = Number(p?.min || 0);
stockSupplier.innerHTML = supplierOptions(p?.supplierId || '');
stockSupplier.value = String(p?.supplierId || '');
stockLastPurchaseDate.value = p?.lastPurchaseDate || '';
stockLastPurchasePrice.value = Number(p?.lastPurchasePrice || 0);
stockDeleteProduct.style.display = p ? 'inline-block' : 'none';
modal('stock-product-modal');
};
stockSaveProduct.onclick = () => {
const name = stockName.value.trim(), unit = stockUnit.value.trim() || 'шт.';
if (!name) return alert('Введите название продукта.');
const duplicate = stock.find(p => p.id !== editingStockId && skuKey(p.name, p.unit) === skuKey(name, unit));
if (duplicate) return alert('Такой продукт с этой единицей измерения уже есть на складе.');
const lastPrice = Math.max(0, Number(stockLastPurchasePrice.value || 0));
const fields = {
name, unit,
cost: Math.max(0, Number(stockCost.value || lastPrice || 0)),
min: Math.max(0, Number(stockMin.value || 0)),
supplierId: String(stockSupplier.value || ''),
lastPurchaseDate: String(stockLastPurchaseDate.value || ''),
lastPurchasePrice: lastPrice
};
if (editingStockId) {
const p = stock.find(x => x.id === editingStockId);
Object.assign(p, fields);
} else {
stock.push({ id: sunUUID(), qty: 0, ...fields });
}
saveStock(); closeModal('stock-product-modal'); renderStock();
};
stockDeleteProduct.onclick = () => {
const p = stock.find(x => x.id === editingStockId);
if (!p || !confirm(`Удалить «${p.name}» со склада? История движений сохранится.`)) return;
stock = stock.filter(x => x.id !== editingStockId);
saveStock(); closeModal('stock-product-modal'); renderStock();
};
function updateMovementSupplierState() {
const select = document.getElementById('stockMoveSupplier');
if (select) select.disabled = stockMoveType.value !== 'in';
}
stockMoveType.onchange = updateMovementSupplierState;
window.stockMovement = (id, type = 'in') => {
movementStockId = id;
const p = stock.find(x => x.id === id);
if (!p) return;
stockMovementTitle.textContent = `${p.name} · ${qtyFmt(p.qty)} ${p.unit}`;
stockMoveType.value = type;
stockMoveQty.value = 1;
stockMoveCost.value = Number(p.cost || 0);
stockMoveDate.value = new Date().toISOString().slice(0, 10);
stockMoveSupplier.innerHTML = supplierOptions(p.supplierId || '');
stockMoveSupplier.value = String(p.supplierId || '');
stockMoveNote.value = '';
updateMovementSupplierState();
modal('stock-movement-modal');
};
stockSaveMove.onclick = () => {
const p = stock.find(x => x.id === movementStockId);
if (!p) return;
const type = stockMoveType.value, amount = Math.max(0, Number(stockMoveQty.value || 0));
if (type !== 'set' && amount <= 0) return alert('Введите количество больше нуля.');
const oldQty = Number(p.qty || 0);
if (type === 'in') p.qty = oldQty + amount;
else if (type === 'out') p.qty = oldQty - amount;
else p.qty = amount;
const moveCost = Math.max(0, Number(stockMoveCost.value || 0));
const supplierId = type === 'in' ? String(stockMoveSupplier.value || '') : '';
const supplier = supplierById(supplierId);
if (type === 'in') {
if (moveCost > 0) p.cost = moveCost;
p.supplierId = supplierId || p.supplierId || '';
p.lastPurchaseDate = String(stockMoveDate.value || new Date().toISOString().slice(0, 10));
p.lastPurchasePrice = moveCost > 0 ? moveCost : Number(p.cost || 0);
}
stockMoves.push({ id: sunUUID(), productId: p.id, productName: p.name, unit: p.unit, type, qty: type === 'set' ? amount - oldQty : amount, setTo: type === 'set' ? amount : undefined, cost: moveCost, date: stockMoveDate.value, supplierId, supplierName: supplier?.name || '', note: stockMoveNote.value.trim(), createdAt: new Date().toISOString() });
saveStock(); closeModal('stock-movement-modal'); renderStock();
};
window.writeOffOrderStock = orderId => {
const order = orders.find(o => o.id == orderId);
if (!order) return;
if (order.stockWrittenOff) return alert(`По заказу №${orderId} склад уже был списан.`);
const needs = aggregateNeed([order]);
if (!needs.length) return alert('В заказе нет продуктов для списания.');
if (!confirm(`Списать продукты со склада по заказу №${orderId}? Остатки могут уйти в минус.`)) return;
const today = new Date().toISOString().slice(0, 10);
needs.forEach(n => {
let p = productByKey(n.name, n.unit);
if (!p) {
p = { id: sunUUID(), name: n.name, unit: n.unit, qty: 0, cost: 0, min: 0, supplierId: '', lastPurchaseDate: '', lastPurchasePrice: 0 };
stock.push(p);
}
p.qty -= n.qty;
stockMoves.push({ id: sunUUID(), productId: p.id, productName: p.name, unit: p.unit, type: 'out', qty: n.qty, cost: p.cost, date: today, note: `Автосписание по заказу №${order.id} · ${order.event || ''}`.trim(), createdAt: new Date().toISOString() });
});
order.stockWrittenOff = true;
persist();
saveStock();
renderStock();
alert(`Продукты по заказу №${orderId} списаны.`);
};
// Keep the need list current when catalog/order data changes.
const priorSaveBoxStock = window.saveBox;
window.saveBox = () => { priorSaveBoxStock(); syncProductsFromCatalog(); if (stockView.classList.contains('on')) renderStock(); window.sunClientOfferCatalogChanged?.(edited?.id); };
const priorSaveOrderStock = window.saveOrder;
window.saveOrder = () => { priorSaveOrderStock(); if (stockView.classList.contains('on')) renderStock(); };
const priorStatusStock = window.status;
window.status = (id, value) => { priorStatusStock(id, value); if (stockView.classList.contains('on')) renderStock(); };
// The original navigation calls show(); make sure stock is refreshed when opened.
const stockNavButton = [...document.querySelectorAll('nav button')].find(b => b.textContent.trim() === 'Склад');
if (stockNavButton) {
const previousClick = stockNavButton.onclick;
stockNavButton.onclick = function(event) {
if (previousClick) previousClick.call(this, event);
setTimeout(renderStock, 0);
};
}
window.sunRefreshStock = renderStock;
// Cloud hydration replaces localStorage after this module has booted.
// Refresh the closure before edits/receipts can write an older inventory back.
window.addEventListener('sun:cloud-state-applied', () => {
try { stock = JSON.parse(localStorage.getItem(STOCK_KEY) || '[]'); stockMoves = JSON.parse(localStorage.getItem(MOVES_KEY) || '[]'); } catch (_) { return; }
if (!Array.isArray(stock)) stock = [];
if (!Array.isArray(stockMoves)) stockMoves = [];
if (stockView.classList.contains('on')) renderStock();
});
window.addEventListener('sun:cloud-tenant-changing', () => { stock = []; stockMoves = []; });
window.sunUnlinkSupplier = supplierId => {
const target = String(supplierId || '');
let changed = false;
stock.forEach(product => {
if (String(product.supplierId || '') === target) { product.supplierId = ''; changed = true; }
});
if (changed) saveStock();
if (stockView.classList.contains('on')) renderStock();
return changed;
};
window.addEventListener('sun:suppliers-changed', () => {
const productSelect = document.getElementById('stockSupplier');
const moveSelect = document.getElementById('stockMoveSupplier');
if (productSelect) { const selected = productSelect.value; productSelect.innerHTML = supplierOptions(selected); productSelect.value = selected; }
if (moveSelect) { const selected = moveSelect.value; moveSelect.innerHTML = supplierOptions(selected); moveSelect.value = selected; }
if (stockView.classList.contains('on')) renderStock();
});
renderStock();
})();
/* ===== DASHBOARD MODULE v1 ===== */
(() => {
const nav = document.querySelector('nav');
if (!nav || document.getElementById('dashboard')) return;
const css = document.createElement('style');
css.textContent = `
.dash-head{display:flex;justify-content:space-between;gap:16px;align-items:flex-start;flex-wrap:wrap;margin-bottom:16px}
.dash-head h1{margin:0;color:var(--n)}.dash-head p{margin:5px 0 0;color:#68717a}
.dash-actions{display:flex;gap:8px;flex-wrap:wrap}
.dash-metrics{display:grid;grid-template-columns:repeat(4,minmax(150px,1fr));gap:10px;margin:14px 0 18px}
.dash-metric{border:1px solid var(--l);border-radius:10px;padding:14px;background:#fafafa;min-height:92px}
.dash-metric small{display:block;color:#68717a;margin-bottom:7px}.dash-metric b{display:block;color:var(--n);font-size:24px}.dash-metric span{display:block;color:#879198;font-size:12px;margin-top:4px}
.dash-grid{display:grid;grid-template-columns:minmax(0,1.35fr) minmax(320px,.85fr);gap:14px}
.dash-panel{border:1px solid var(--l);border-radius:10px;background:#fff;overflow:hidden}
.dash-panel-head{display:flex;justify-content:space-between;gap:10px;align-items:center;padding:13px 14px;border-bottom:1px solid var(--l);background:#fafafa}
.dash-panel-head h2{font-size:17px;margin:0;color:var(--n)}.dash-panel-head small{color:#68717a}
.dash-list{padding:0 14px}.dash-order{display:grid;grid-template-columns:78px 1fr 115px 120px;gap:9px;align-items:center;padding:11px 0;border-bottom:1px solid #e7e9ea}
.dash-order:last-child{border-bottom:0}.dash-order-time{font-weight:700;color:var(--n)}.dash-order-main b{display:block}.dash-order-main small{display:block;color:#68717a;margin-top:3px}.dash-order-money{text-align:right;font-weight:700}.dash-order button{justify-self:end}
.dash-badge{display:inline-block;padding:4px 8px;border-radius:999px;background:#eef2f4;font-size:11px;color:#50606b;margin-top:4px}
.dash-empty{padding:20px 14px;color:#879198}
.dash-week{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:8px;padding:14px}
.dash-day{border:1px solid #e6e9ec;background:#f8f9fb;border-radius:10px;padding:10px 8px;min-height:94px;display:flex;flex-direction:column;justify-content:space-between}
.dash-day.today{border-color:#c9a45c;box-shadow:inset 0 0 0 1px #c9a45c;background:#fffaf0}
.dash-day small{color:#7a848c;font-size:11px;text-transform:uppercase;letter-spacing:.03em}.dash-day b{color:var(--n);font-size:20px;line-height:1}.dash-day span{color:#68717a;font-size:11px;line-height:1.25}
.dash-attention{padding:2px 14px}.dash-attn{display:grid;grid-template-columns:34px 1fr auto;gap:10px;align-items:center;padding:10px 0;border-bottom:1px solid #e7e9ea}.dash-attn:last-child{border-bottom:0}
.dash-attn-icon{width:30px;height:30px;border-radius:9px;background:#fff4e8;display:grid;place-items:center;font-size:15px}.dash-attn b{display:block;color:var(--n);font-size:13px}.dash-attn small{display:block;color:#68717a;margin-top:2px}.dash-attn .mini{white-space:nowrap}
.dash-alert{margin:0 14px 14px;padding:10px 12px;border-radius:8px;background:#fff7ed;border:1px solid #f0dcc1;color:#7a5521}
.dash-good{background:#eff8f1;border-color:#cee4d2;color:#366144}
.dash-mini-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px;padding:14px}.dash-mini{border-radius:8px;background:#f7f8f8;padding:11px}.dash-mini small{display:block;color:#68717a}.dash-mini b{display:block;margin-top:4px;color:var(--n);font-size:17px}
@media(max-width:1050px){.dash-metrics{grid-template-columns:1fr 1fr}.dash-grid{grid-template-columns:1fr}.dash-order{grid-template-columns:70px 1fr 105px}.dash-order button{display:none}}
@media(max-width:650px){.dash-metrics{grid-template-columns:1fr 1fr}.dash-metric b{font-size:19px}.dash-order{grid-template-columns:58px 1fr}.dash-order-money{display:none}.dash-week{grid-template-columns:repeat(2,minmax(0,1fr))}.dash-day:last-child{grid-column:1/-1}.dash-attn{grid-template-columns:34px 1fr}.dash-attn .mini{display:none}}
`;
document.head.appendChild(css);
const button = document.createElement('button');
button.textContent = 'Главная';
nav.appendChild(button);
const view = document.createElement('section');
view.id = 'dashboard';
view.className = 'view page';
document.body.appendChild(view);
const fmt = n => (typeof money === 'function' ? money(n) : `${Number(n || 0).toLocaleString('ru-RU')}`);
const orderTotal = o => (typeof orderTotalValue === 'function' ? Number(orderTotalValue(o) || 0) : Number(o.total || 0));
const todayIso = () => {
const d = new Date();
const y = d.getFullYear(), m = String(d.getMonth() + 1).padStart(2, '0'), day = String(d.getDate()).padStart(2, '0');
return `${y}-${m}-${day}`;
};
const dateLabel = raw => {
if (!raw) return '—';
const d = new Date(raw + 'T00:00:00');
return Number.isNaN(d.getTime()) ? raw : d.toLocaleDateString('ru-RU', { day:'2-digit', month:'2-digit' });
};
const active = o => (o.status || 'Новый') !== 'Отменён';
const working = o => !['Отменён','Отдан заказчику'].includes(o.status || 'Новый');
const orderStamp = o => `${o.date || '9999-99-99'}T${o.time || '23:59'}`;
function go(label) {
const target = [...nav.querySelectorAll('button')].find(b => b.textContent.trim() === label);
if (target) target.click();
}
window.dashboardGo = go;
window.dashboardOpenOrder = id => {
if (typeof openOrder === 'function') openOrder(id);
const newBtn = [...nav.querySelectorAll('button')].find(b => b.textContent.trim() === 'Новый заказ');
[...nav.querySelectorAll('button')].forEach(b => b.classList.remove('on'));
if (newBtn) newBtn.classList.add('on');
};
function stockData() {
try { return JSON.parse(localStorage.getItem('sunStock') || '[]'); } catch { return []; }
}
function skuKey(name, unit) { return `${String(name || '').trim().toLowerCase()}|${String(unit || '').trim().toLowerCase()}`; }
function aggregateNeeds(orderList) {
const map = {};
orderList.forEach(order => (order.lines || []).forEach(line => {
const item = boxes.find(x => x.id == line.id);
(item?.ingredients || []).forEach(part => {
const name = String(part[0] || '').trim(), unit = String(part[2] || 'шт.').trim() || 'шт.';
if (!name) return;
const key = skuKey(name, unit);
if (!map[key]) map[key] = { name, unit, qty:0 };
map[key].qty += Number(part[1] || 0) * Number(line.qty || 0);
});
}));
const stock = stockData();
const stockMap = new Map(stock.map(p => [skuKey(p.name,p.unit), p]));
return Object.values(map).map(n => {
const p = stockMap.get(skuKey(n.name,n.unit));
const have = Number(p?.qty || 0), deficit = Math.max(0, n.qty - have), cost = Number(p?.cost || 0);
return {...n, have, deficit, cost, buy:deficit*cost};
}).filter(n => n.deficit > 0).sort((a,b) => b.buy - a.buy || b.deficit - a.deficit);
}
function render() {
const today = todayIso();
const valid = orders.filter(active);
const todayOrders = valid.filter(o => o.date === today).sort((a,b) => orderStamp(a).localeCompare(orderStamp(b)));
const upcoming = orders.filter(working).filter(o => (o.date || '') >= today).sort((a,b) => orderStamp(a).localeCompare(orderStamp(b)));
const nextOrders = upcoming.slice(0, 7);
const totalRevenue = valid.reduce((s,o) => s + orderTotal(o), 0);
const totalPaid = valid.reduce((s,o) => s + Math.min(orderTotal(o), Math.max(0, Number(o.prepayment || 0))), 0);
const totalDebt = valid.reduce((s,o) => s + Math.max(0, orderTotal(o) - Math.max(0, Number(o.prepayment || 0))), 0);
const startDay = new Date(today + 'T00:00:00');
const weekDays = Array.from({length:7}, (_,i) => {
const d = new Date(startDay); d.setDate(startDay.getDate()+i);
const iso = `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;
const list = valid.filter(o => o.date === iso);
return {date:iso, dateObj:d, list, total:list.reduce((sum,o)=>sum+orderTotal(o),0)};
});
const weekOrders = weekDays.reduce((sum,d)=>sum+d.list.length,0);
const weekRevenue = weekDays.reduce((sum,d)=>sum+d.total,0);
const attention = [];
upcoming.slice(0,30).forEach(o => {
const debt = Math.max(0, orderTotal(o)-Math.max(0,Number(o.prepayment||0)));
const checklist = o.checklist && typeof o.checklist === 'object' ? Object.values(o.checklist) : [];
const checklistDone = checklist.length ? checklist.filter(Boolean).length : 0;
if (!String(o.address||'').trim()) attention.push({icon:'⌖',title:`Заказ №${o.id}: нет адреса`,sub:o.event||'Мероприятие',id:o.id});
else if (!String(o.responsible||'').trim()) attention.push({icon:'👤',title:`Заказ №${o.id}: нет ответственного`,sub:`${dateLabel(o.date)} · ${o.time||'—'}`,id:o.id});
else if (debt>0) attention.push({icon:'₽',title:`Заказ №${o.id}: остаток ${fmt(debt)}`,sub:o.contact||o.event||'Клиент',id:o.id});
else if (checklist.length && checklistDone < checklist.length) attention.push({icon:'✓',title:`Заказ №${o.id}: готовность ${checklistDone}/${checklist.length}`,sub:`${dateLabel(o.date)} · ${o.time||'—'}`,id:o.id});
});
const todayHtml = todayOrders.length ? todayOrders.map(o => `<div class="dash-order">
<div class="dash-order-time">${o.time || '—'}</div>
<div class="dash-order-main"><b>№${o.id} · ${o.event || 'Заказ'}</b><small>${o.contact || 'Клиент не указан'} · ${o.address || 'Без адреса'}</small><span class="dash-badge">${o.status || 'Новый'}</span></div>
<div class="dash-order-money">${fmt(orderTotal(o))}</div>
<button class="outline" type="button" onclick="dashboardOpenOrder(${o.id})">Открыть</button>
</div>`).join('') : '<div class="dash-empty">На сегодня сохранённых заказов нет.</div>';
const nextHtml = nextOrders.length ? nextOrders.map(o => `<div class="dash-order">
<div class="dash-order-time">${dateLabel(o.date)}<small style="display:block;font-weight:400;color:#68717a">${o.time || ''}</small></div>
<div class="dash-order-main"><b>№${o.id} · ${o.event || 'Заказ'}</b><small>${o.contact || 'Клиент не указан'} · ${o.address || 'Без адреса'}</small><span class="dash-badge">${o.status || 'Новый'}</span></div>
<div class="dash-order-money">${fmt(orderTotal(o))}</div>
<button class="outline" type="button" onclick="dashboardOpenOrder(${o.id})">Открыть</button>
</div>`).join('') : '<div class="dash-empty">Ближайших активных заказов нет.</div>';
const weekHtml = weekDays.map((d,i) => `<div class="dash-day ${i===0?'today':''}"><small>${d.dateObj.toLocaleDateString('ru-RU',{weekday:'short'})} · ${d.dateObj.toLocaleDateString('ru-RU',{day:'2-digit',month:'2-digit'})}</small><b>${d.list.length}</b><span>${d.list.length===1?'заказ':(d.list.length>=2&&d.list.length<=4?'заказа':'заказов')}<br>${fmt(d.total)}</span></div>`).join('');
const attentionHtml = attention.length ? attention.slice(0,6).map(a => `<div class="dash-attn"><div class="dash-attn-icon">${a.icon}</div><div><b>${a.title}</b><small>${a.sub}</small></div><button class="mini" type="button" onclick="dashboardOpenOrder(${a.id})">Открыть</button></div>`).join('') : '<div class="dash-empty">По ближайшим заказам критичных моментов нет.</div>';
view.innerHTML = `<div class="dash-head"><div><h1>Главная</h1><p>Оперативная картина по заказам, оплатам и ближайшим мероприятиям.</p></div><div class="dash-actions"><button class="primary" onclick="dashboardGo('Новый заказ')"> Новый заказ</button><button class="outline" onclick="dashboardGo('Заказы')">Все заказы</button><button class="outline" onclick="dashboardGo('Календарь')">Календарь</button></div></div>
<div class="dash-metrics">
<div class="dash-metric"><small>Заказов сегодня</small><b>${todayOrders.length}</b><span>${todayOrders.filter(o=>working(o)).length} ещё в работе</span></div>
<div class="dash-metric"><small>Выручка по заказам</small><b>${fmt(totalRevenue)}</b><span>без отменённых</span></div>
<div class="dash-metric"><small>Получено предоплат</small><b>${fmt(totalPaid)}</b><span>${totalRevenue ? Math.round(totalPaid/totalRevenue*100) : 0}% от суммы заказов</span></div>
<div class="dash-metric"><small>Осталось получить</small><b>${fmt(totalDebt)}</b><span>по действующим заказам</span></div>
</div>
<div class="dash-grid">
<div>
<section class="dash-panel"><div class="dash-panel-head"><h2>Сегодня</h2><small>${new Date().toLocaleDateString('ru-RU',{day:'numeric',month:'long',weekday:'long'})}</small></div><div class="dash-list">${todayHtml}</div></section>
<section class="dash-panel" style="margin-top:14px"><div class="dash-panel-head"><h2>Ближайшие мероприятия</h2><button class="mini" onclick="dashboardGo('Заказы')">Все заказы</button></div><div class="dash-list">${nextHtml}</div></section>
</div>
<div>
<section class="dash-panel"><div class="dash-panel-head"><h2>План на 7 дней</h2><button class="mini" onclick="dashboardGo('Календарь')">Календарь</button></div>
<div class="dash-mini-grid"><div class="dash-mini"><small>Заказов за 7 дней</small><b>${weekOrders}</b></div><div class="dash-mini"><small>Сумма заказов</small><b>${fmt(weekRevenue)}</b></div></div>
<div class="dash-week">${weekHtml}</div>
</section>
<section class="dash-panel" style="margin-top:14px"><div class="dash-panel-head"><h2>Требуют внимания</h2><small>${attention.length ? `${attention.length} ${attention.length===1?'пункт':'пунктов'}` : 'всё спокойно'}</small></div><div class="dash-attention">${attentionHtml}</div></section>
</div>
</div>`;
}
button.onclick = () => {
document.querySelectorAll('.view').forEach(x => x.classList.remove('on'));
view.classList.add('on');
[...nav.querySelectorAll('button')].forEach(x => x.classList.remove('on'));
button.classList.add('on');
render();
};
// Make the dashboard the start screen without changing the old navigation indexes.
document.querySelectorAll('.view').forEach(x => x.classList.remove('on'));
[...nav.querySelectorAll('button')].forEach(x => x.classList.remove('on'));
view.classList.add('on');
button.classList.add('on');
render();
})();
/* ===== ORDER CALENDAR MODULE v1 ===== */
(() => {
const nav = document.querySelector('nav');
if (!nav || document.getElementById('calendar')) return;
const css = document.createElement('style');
css.textContent = `
.cal-head{display:flex;justify-content:space-between;gap:14px;align-items:flex-start;flex-wrap:wrap;margin-bottom:14px}
.cal-head h1{margin:0;color:var(--n)}.cal-head p{margin:5px 0 0;color:#68717a}
.cal-controls{display:flex;gap:7px;align-items:center;flex-wrap:wrap}.cal-controls .outline,.cal-controls .primary{padding:8px 11px}
.cal-mode{display:flex;border:1px solid var(--l);border-radius:7px;overflow:hidden;background:#fff}.cal-mode button{border:0;border-right:1px solid var(--l);background:#fff;padding:8px 11px;color:#68717a}.cal-mode button:last-child{border-right:0}.cal-mode button.on{background:var(--n);color:#fff}
.cal-period-title{font-size:18px;font-weight:700;color:var(--n);min-width:180px;text-align:center}
.cal-metrics{display:grid;grid-template-columns:repeat(4,minmax(130px,1fr));gap:9px;margin:0 0 14px}.cal-metric{border:1px solid var(--l);border-radius:9px;padding:11px;background:#fafafa}.cal-metric small{display:block;color:#68717a;margin-bottom:5px}.cal-metric b{font-size:19px;color:var(--n)}
.cal-month{display:grid;grid-template-columns:repeat(7,minmax(120px,1fr));border:1px solid var(--l);border-radius:10px;overflow:hidden;background:#fff;min-width:850px}
.cal-weekday{padding:9px;text-align:center;font-weight:700;color:#68717a;background:#f7f8f8;border-right:1px solid var(--l);border-bottom:1px solid var(--l)}.cal-weekday:nth-child(7){border-right:0}
.cal-day{min-height:142px;padding:8px;border-right:1px solid var(--l);border-bottom:1px solid var(--l);background:#fff;overflow:hidden}.cal-day:nth-child(7n){border-right:0}.cal-day.outside{background:#fafafa;color:#a7adb1}.cal-day.today{box-shadow:inset 0 0 0 2px var(--g)}
.cal-date{display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;font-weight:700}.cal-count{font-size:10px;color:#68717a;background:#eef1f2;border-radius:999px;padding:2px 6px}
.cal-event{display:block;width:100%;border:0;border-left:4px solid #c9d0d8;border-radius:5px;background:#f7f8f8;padding:6px 7px;margin:5px 0;text-align:left;font-size:11px;line-height:1.2;color:#203040}.cal-event:hover{filter:brightness(.98)}.cal-event b{display:block;font-size:11px}.cal-event small{display:block;color:#68717a;margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.cal-event-overflow{display:none!important}
.cal-day--compact .cal-event{display:flex;align-items:center;gap:5px;min-width:0;padding:3px 5px;margin:3px 0;white-space:nowrap}
.cal-day--compact .cal-event b{flex:none;white-space:nowrap}
.cal-day--compact .cal-event-name{flex:1;min-width:0;margin:0;line-height:1.2}
.cal-day--compact .cal-event-finance{display:none!important}
.cal-more{font-size:11px;color:#68717a;padding-top:3px;cursor:pointer}
.cal-scroll{overflow-x:auto;-webkit-overflow-scrolling:touch;padding-bottom:3px}
.cal-agenda{display:grid;gap:9px}.cal-agenda-day{border:1px solid var(--l);border-radius:9px;background:#fff;overflow:hidden}.cal-agenda-date{padding:10px 12px;background:#fafafa;border-bottom:1px solid var(--l);font-weight:700;color:var(--n)}
.cal-agenda-order{display:grid;grid-template-columns:75px minmax(0,1fr) 120px 115px;gap:8px;align-items:center;padding:10px 12px;border-bottom:1px solid #e7e9ea}.cal-agenda-order:last-child{border-bottom:0}.cal-agenda-order .time{font-weight:700;color:var(--n)}.cal-agenda-order small{display:block;color:#68717a;margin-top:2px}.cal-agenda-order .amount{text-align:right;font-weight:700}.cal-status{display:inline-block;padding:3px 7px;border-radius:999px;font-size:10px;margin-top:4px;background:#eef2f4}
.cal-empty{padding:24px;text-align:center;color:#879198;border:1px dashed var(--l);border-radius:9px;background:#fff}
@media(max-width:900px){.cal-metrics{grid-template-columns:1fr 1fr}.cal-period-title{order:-1;width:100%;text-align:left}.cal-agenda-order{grid-template-columns:60px minmax(0,1fr) 95px}.cal-agenda-order button{display:none}}
@media(max-width:650px){.cal-controls{display:grid;grid-template-columns:auto auto 1fr;width:100%}.cal-controls .cal-mode{grid-column:1/-1}.cal-period-title{grid-column:1/-1}.cal-metrics{grid-template-columns:1fr 1fr}.cal-agenda-order{grid-template-columns:52px minmax(0,1fr)}.cal-agenda-order .amount,.cal-agenda-order button{display:none}.cal-month{min-width:760px}.cal-day{min-height:125px}.cal-print-btn{grid-column:1/-1}}
@media print{
body.sun-print-calendar *{visibility:hidden!important}
body.sun-print-calendar #calendar,body.sun-print-calendar #calendar *{visibility:visible!important}
body.sun-print-calendar #calendar{display:block!important;position:absolute!important;left:0!important;top:0!important;width:100%!important;margin:0!important;padding:8mm!important;min-height:0!important;background:#fff!important;box-shadow:none!important}
body.sun-print-calendar #calendar .cal-print-btn,body.sun-print-calendar #calendar #calPrev,body.sun-print-calendar #calendar #calToday,body.sun-print-calendar #calendar #calNext{display:none!important}
body.sun-print-calendar #calendar .cal-scroll{overflow:visible!important}
body.sun-print-calendar #calendar .cal-month{min-width:0!important;width:100%!important;grid-template-columns:repeat(7,minmax(0,1fr))!important}
body.sun-print-calendar #calendar .cal-day{min-height:32mm!important;padding:4px!important}
body.sun-print-calendar #calendar .cal-event{font-size:8pt!important;padding:3px 4px!important;margin:2px 0!important}
body.sun-print-calendar #calendar .cal-event b,body.sun-print-calendar #calendar .cal-event small{font-size:7.5pt!important}
body.sun-print-calendar #calendar .cal-agenda-order{break-inside:avoid}
@page{size:A4 landscape;margin:8mm}
}
`;
document.head.appendChild(css);
const button = document.createElement('button');
button.textContent = 'Календарь';
nav.appendChild(button);
const view = document.createElement('section');
view.id = 'calendar';
view.className = 'view page';
document.body.appendChild(view);
let mode = localStorage.getItem('sunCalendarMode') || 'month';
let cursor = new Date();
cursor.setHours(0,0,0,0);
const fmt = n => typeof money === 'function' ? money(n) : `${Number(n || 0).toLocaleString('ru-RU')}`;
const calEsc = value => String(value??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
const total = o => typeof orderTotalValue === 'function' ? Number(orderTotalValue(o) || 0) : Number(o.total || 0);
const statusColors = { 'Новый':'#8aa7c2','В работе':'#b1a7cf','Собран':'#c99a32','Передан курьеру':'#d7b24c','Отдан заказчику':'#65a978','Отменён':'#c88787' };
const paymentDefaults={unpaid:'#d84a4a',prepaid:'#35a46b',paid:'#3b82f6',cancelled:'#9aa0aa'};
const paymentLabels={unpaid:'Без предоплаты',prepaid:'Предоплата внесена',paid:'Оплачен полностью',cancelled:'Отменён'};
const paymentState=o=>{
try{if(typeof window.sunOrderPaymentState==='function')return window.sunOrderPaymentState(o)}catch(_){}
if(String(o?.status||'').trim()==='Отменён')return'cancelled';
const paid=Math.max(0,Number(o?.prepayment||0)),sum=Math.max(0,total(o));
if(sum>0&&paid>=sum-0.5)return'paid';
if(paid>0)return'prepaid';
return'unpaid';
};
const paymentColors=()=>{try{return Object.assign({},paymentDefaults,typeof window.sunOrderPaymentColors==='function'?window.sunOrderPaymentColors():{})}catch(_){return {...paymentDefaults}}};
const paymentColor=o=>paymentColors()[paymentState(o)]||paymentDefaults.unpaid;
const tint=(color,alpha=.12)=>{const h=String(color||'').replace('#','');if(!/^[0-9a-f]{6}$/i.test(h))return'#f7f8f8';const r=parseInt(h.slice(0,2),16),g=parseInt(h.slice(2,4),16),b=parseInt(h.slice(4,6),16);return `rgba(${r},${g},${b},${alpha})`};
const pad = n => String(n).padStart(2,'0');
const iso = d => `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}`;
const cloneDate = d => new Date(d.getFullYear(), d.getMonth(), d.getDate());
const startMonday = d => { const x=cloneDate(d), day=(x.getDay()+6)%7; x.setDate(x.getDate()-day); return x; };
const orderStamp = o => `${o.date || '9999-99-99'}T${o.time || '23:59'}`;
const liveOrders = () => orders.filter(o => (o.status || 'Новый') !== 'Отменён');
function openCalendarOrder(id){
if (typeof openOrder === 'function') openOrder(id);
const newBtn = [...nav.querySelectorAll('button')].find(b => b.textContent.trim() === 'Новый заказ');
[...nav.querySelectorAll('button')].forEach(b => b.classList.remove('on'));
if (newBtn) newBtn.classList.add('on');
}
window.calendarOpenOrder = openCalendarOrder;
function rangeForMode(){
if(mode==='day') { const s=cloneDate(cursor); return {start:s,end:s}; }
if(mode==='week') { const s=startMonday(cursor), e=cloneDate(s); e.setDate(e.getDate()+6); return {start:s,end:e}; }
const s=new Date(cursor.getFullYear(),cursor.getMonth(),1), e=new Date(cursor.getFullYear(),cursor.getMonth()+1,0); return {start:s,end:e};
}
function titleForMode(){
if(mode==='day') return cursor.toLocaleDateString('ru-RU',{day:'numeric',month:'long',year:'numeric'});
if(mode==='week') { const {start,end}=rangeForMode(); return `${start.toLocaleDateString('ru-RU',{day:'numeric',month:'short'})} — ${end.toLocaleDateString('ru-RU',{day:'numeric',month:'short',year:'numeric'})}`; }
return cursor.toLocaleDateString('ru-RU',{month:'long',year:'numeric'}).replace(/^./,c=>c.toUpperCase());
}
function inRange(o,start,end){ return (o.date || '') >= iso(start) && (o.date || '') <= iso(end); }
function ordersByDate(list){
return list.reduce((m,o)=>{ (m[o.date] ||= []).push(o); return m; },{});
}
function periodStats(list){
const revenue=list.reduce((s,o)=>s+total(o),0);
const paid=list.reduce((s,o)=>s+Math.min(total(o),Math.max(0,Number(o.prepayment||0))),0);
const debt=list.reduce((s,o)=>s+Math.max(0,total(o)-Math.max(0,Number(o.prepayment||0))),0);
return {revenue,paid,debt};
}
function eventHtml(o,overflow=false){
const state=paymentState(o),color=paymentColor(o),bg=tint(color,.13),label=paymentLabels[state]||'';
return `<button class="cal-event${overflow?' cal-event-overflow':''}" data-order-id="${calEsc(o.id)}" data-payment-state="${state}" style="border-left-color:${color};background:${bg}" onclick="calendarOpenOrder(${Number(o.id)})" title="${calEsc(`${o.time||'—'} · №${o.id} · ${o.event||'Заказ'} · ${fmt(total(o))} · ${label}`)}"><b>${calEsc(o.time || '—')} · №${calEsc(o.id)}</b><small class="cal-event-name">${calEsc(o.event || 'Заказ')}</small><small class="cal-event-finance">${calEsc(fmt(total(o)))} · ${calEsc(o.status || 'Новый')}</small></button>`;
}
function renderMonth(list){
const first=new Date(cursor.getFullYear(),cursor.getMonth(),1), gridStart=startMonday(first), today=iso(new Date());
const byDate=ordersByDate(liveOrders());
let cells='';
for(let i=0;i<42;i++){
const d=cloneDate(gridStart); d.setDate(d.getDate()+i); const key=iso(d); const dayOrders=(byDate[key]||[]).sort((a,b)=>orderStamp(a).localeCompare(orderStamp(b)));
const outside=d.getMonth()!==cursor.getMonth(),compact=dayOrders.length>3;
cells += `<div class="cal-day ${outside?'outside':''} ${key===today?'today':''} ${compact?'cal-day--compact':''}" data-date="${key}"><div class="cal-date"><span>${d.getDate()}</span>${dayOrders.length?`<span class="cal-count">${dayOrders.length}</span>`:''}</div>${dayOrders.map((o,index)=>eventHtml(o,index>=9)).join('')}${dayOrders.length>9?`<div class="cal-more">+ ещё ${dayOrders.length-9}</div>`:''}</div>`;
}
return `<div class="cal-scroll"><div class="cal-month">${['Пн','Вт','Ср','Чт','Пт','Сб','Вс'].map(x=>`<div class="cal-weekday">${x}</div>`).join('')}${cells}</div></div>`;
}
function renderAgenda(list){
if(!list.length) return '<div class="cal-empty">В выбранном периоде заказов нет.</div>';
const by=ordersByDate(list);
return `<div class="cal-agenda">${Object.keys(by).sort().map(dateKey=>{
const d=new Date(dateKey+'T00:00:00');
const label=d.toLocaleDateString('ru-RU',{weekday:'long',day:'numeric',month:'long'});
const rows=by[dateKey].sort((a,b)=>orderStamp(a).localeCompare(orderStamp(b))).map(o=>{const state=paymentState(o),color=paymentColor(o),bg=tint(color,.08);return `<div class="cal-agenda-order" data-payment-state="${state}" style="border-left:5px solid ${color};background:${bg}"><div class="time">${o.time||'—'}</div><div><b>${o.id} · ${o.event||'Заказ'}</b><small>${o.contact||'Клиент не указан'} · ${o.address||'Без адреса'}</small><span class="cal-status" style="border-left:4px solid ${statusColors[o.status||'Новый']||'#c9d0d8'}">${o.status||'Новый'}</span></div><div class="amount">${fmt(total(o))}</div><button class="outline" onclick="calendarOpenOrder(${o.id})">Открыть</button></div>`}).join('');
return `<section class="cal-agenda-day"><div class="cal-agenda-date">${label}</div>${rows}</section>`;
}).join('')}</div>`;
}
function render(){
const {start,end}=rangeForMode();
const period=liveOrders().filter(o=>inRange(o,start,end)).sort((a,b)=>orderStamp(a).localeCompare(orderStamp(b)));
const stats=periodStats(period);
const today=iso(new Date());
const todayCount=liveOrders().filter(o=>o.date===today).length;
view.innerHTML=`<div class="cal-head"><div><h1>Календарь заказов</h1><p>Все мероприятия, суммы, статусы и загрузка по датам.</p></div><div class="cal-controls"><button class="outline" id="calPrev"></button><button class="outline" id="calToday">Сегодня</button><button class="outline" id="calNext"></button><div class="cal-period-title">${titleForMode()}</div><div class="cal-mode"><button data-cal-mode="day" class="${mode==='day'?'on':''}">День</button><button data-cal-mode="week" class="${mode==='week'?'on':''}">Неделя</button><button data-cal-mode="month" class="${mode==='month'?'on':''}">Месяц</button></div><button class="outline cal-print-btn" id="calPrint" type="button">Печать</button></div></div>
<div class="cal-metrics"><div class="cal-metric"><small>Заказов в периоде</small><b>${period.length}</b></div><div class="cal-metric"><small>Сумма заказов</small><b>${fmt(stats.revenue)}</b></div><div class="cal-metric"><small>Предоплаты</small><b>${fmt(stats.paid)}</b></div><div class="cal-metric"><small>К получению</small><b>${fmt(stats.debt)}</b></div></div>
${mode==='month'?renderMonth(period):renderAgenda(period)}`;
document.getElementById('calPrev').onclick=()=>{ if(mode==='day')cursor.setDate(cursor.getDate()-1); else if(mode==='week')cursor.setDate(cursor.getDate()-7); else cursor.setMonth(cursor.getMonth()-1); render(); };
document.getElementById('calNext').onclick=()=>{ if(mode==='day')cursor.setDate(cursor.getDate()+1); else if(mode==='week')cursor.setDate(cursor.getDate()+7); else cursor.setMonth(cursor.getMonth()+1); render(); };
document.getElementById('calToday').onclick=()=>{ cursor=new Date(); cursor.setHours(0,0,0,0); render(); };
document.getElementById('calPrint').onclick=()=>{ document.body.classList.add('sun-print-calendar'); window.print(); setTimeout(()=>document.body.classList.remove('sun-print-calendar'),120); };
[...view.querySelectorAll('[data-cal-mode]')].forEach(b=>b.onclick=()=>{ mode=b.dataset.calMode; localStorage.setItem('sunCalendarMode',mode); render(); });
}
button.onclick=()=>{ document.querySelectorAll('.view').forEach(x=>x.classList.remove('on')); view.classList.add('on'); [...nav.querySelectorAll('button')].forEach(x=>x.classList.remove('on')); button.classList.add('on'); render(); };
const prevSave=window.saveOrder; window.saveOrder=()=>{ prevSave(); if(view.classList.contains('on')) render(); };
const prevStatus=window.status; window.status=(id,value)=>{ prevStatus(id,value); if(view.classList.contains('on')) render(); };
const prevDelete=window.deleteOrder; if(typeof prevDelete==='function') window.deleteOrder=(...args)=>{ const r=prevDelete(...args); if(view.classList.contains('on')) render(); return r; };
})();
/* ===== RESPONSIVE / MOBILE MODULE v1 ===== */
(() => {
if (document.getElementById('sun-responsive-style')) return;
const style = document.createElement('style');
style.id = 'sun-responsive-style';
style.textContent = `
html{scroll-behavior:smooth}
body{min-width:0;overflow-x:hidden}
header{position:sticky;top:0;z-index:80}
nav{min-width:0}
nav button{white-space:nowrap}
.page,.card,.dialog{min-width:0}
.tiles{align-items:stretch}
.tile{width:100%}
input,select,textarea{max-width:100%}
.dialog{overscroll-behavior:contain}
.mobile-only{display:none!important}
/* Tablet: keep the catalog useful without squeezing the order form. */
@media (min-width:901px) and (max-width:1180px){
.work{grid-template-columns:minmax(0,1fr) 170px}
.work>section.card{grid-column:1/-1}
.tiles{grid-template-columns:repeat(auto-fill,minmax(130px,1fr))}
header{gap:20px;padding-inline:16px}
nav button{padding-inline:10px}
.page{margin:12px}
}
@media (max-width:900px){
body{padding-bottom:74px}
header{position:sticky;top:0;z-index:90;display:flex;flex-direction:row;align-items:center;justify-content:space-between;height:62px;padding:8px 12px;gap:10px;box-shadow:0 2px 10px #102c3d22}
.brand{display:flex;align-items:center;font-size:18px;white-space:nowrap;min-width:0}
.brand .real-logo{width:38px!important;height:38px!important;margin-right:7px!important}
header nav{position:fixed;left:0;right:0;bottom:0;z-index:100;display:flex;align-items:stretch;overflow-x:auto;overflow-y:hidden;background:var(--n);height:68px;padding:0 max(6px,env(safe-area-inset-right)) env(safe-area-inset-bottom) max(6px,env(safe-area-inset-left));box-shadow:0 -2px 14px #102c3d2b;scrollbar-width:none;-webkit-overflow-scrolling:touch}
header nav::-webkit-scrollbar{display:none}
header nav button{flex:1 0 92px;height:62px;padding:5px 8px;border-bottom:0;border-top:3px solid transparent;font-size:12px;line-height:1.15;color:#dce8ec}
header nav button.on{border-top-color:var(--g);border-bottom-color:transparent;color:#fff;background:#ffffff0d}
.view.page{margin:10px;min-height:calc(100vh - 150px);padding:14px}
.work{display:flex!important;flex-direction:column;padding:10px;gap:10px;min-height:0}
.work>.cats{order:1;position:sticky;top:62px;z-index:50;display:flex;gap:7px;overflow-x:auto;padding:8px;background:#e9efe8;box-shadow:0 2px 6px #00000012;scrollbar-width:none;-webkit-overflow-scrolling:touch}
.work>.cats::-webkit-scrollbar{display:none}
.work>.cats button{flex:0 0 auto;width:auto;min-width:max-content;margin:0;padding:10px 13px}
.work>.cats p{display:none}
.work>.catalog{order:2}
.work>section.card{order:3}
.catalog-head{gap:10px;flex-wrap:wrap}
.catalog-head .outline{margin-left:auto}
.tiles{grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;margin-top:10px}
.tile{min-height:132px;padding:6px;font-size:12px}
.tile img,.tile .ph{height:72px}
.catalog .tile.add{min-height:132px}
.form{grid-template-columns:1fr 1fr!important}
.form label:first-child{grid-column:1/-1}
.line{grid-template-columns:minmax(0,1fr) 34px 52px 34px;gap:4px}
.line button{height:34px}
.actions{flex-wrap:wrap}
.actions>button{min-height:42px}
.subnav{overflow-x:auto;white-space:nowrap;scrollbar-width:none}
.subnav::-webkit-scrollbar{display:none}
.modal{padding:10px;z-index:150}
.dialog{width:100%;max-height:calc(100dvh - 20px);border-radius:12px;padding:16px}
.manager{grid-template-columns:repeat(2,minmax(0,1fr))}
.ingredient{grid-template-columns:minmax(0,1fr) 72px 66px 34px}
.prep{grid-template-columns:1fr}
.clients-toolbar{align-items:stretch;flex-direction:column}
.clients-toolbar input{width:100%}
.client-grid{grid-template-columns:repeat(2,minmax(0,1fr))}
.stock-toolbar{align-items:stretch!important;flex-direction:column!important}
.stock-toolbar input{width:100%!important}
.dash-actions{width:100%}
.dash-actions button{flex:1 1 130px}
button,.primary,.outline,.danger{touch-action:manipulation}
}
@media (max-width:640px){
body{font-size:14px;padding-bottom:70px}
header{height:58px;padding:7px 10px}
.brand{font-size:16px}
.brand .real-logo{width:34px!important;height:34px!important}
header nav{height:66px}
header nav button{flex-basis:86px;height:60px;font-size:11px;padding-inline:5px}
.view.page{margin:7px;padding:11px;border-radius:8px}
.work{padding:7px}
.work>.cats{top:58px;margin-inline:0;border-radius:7px}
.catalog h1,.page h1{font-size:20px}
.tiles{grid-template-columns:repeat(2,minmax(0,1fr))}
.tile{min-height:142px;font-size:13px}
.tile img,.tile .ph{height:82px}
.catalog .tile.add{min-height:142px}
.form{grid-template-columns:1fr!important}
.form label:first-child{grid-column:auto}
.line{grid-template-columns:minmax(0,1fr) 32px 48px 32px;padding:9px 0}
.line span{font-size:13px;overflow-wrap:anywhere}
.line input{min-width:0}
.actions{display:grid;grid-template-columns:1fr 1fr;width:100%}
.actions>button{width:100%;padding:10px 8px}
.actions>button:only-child{grid-column:1/-1}
.manager{grid-template-columns:1fr}
.dialog{padding:14px;border-radius:10px}
.dialog-head{position:sticky;top:-14px;z-index:2;background:#fff;padding:10px 0 8px;margin-top:-10px}
.dialog-head h2{font-size:19px}
.ingredient{grid-template-columns:minmax(0,1fr) 66px 58px 32px;font-size:12px}
.ingredient input{padding:7px 5px}
.client-grid{grid-template-columns:1fr}
.client-summary,.stock-summary,.dash-metrics{grid-template-columns:1fr 1fr!important;gap:7px}
.client-summary .summary-box,.stock-summary>*,.dash-metric{padding:10px;min-height:0}
.client-summary b,.dash-metric b{font-size:18px!important}
.client-card .client-metrics{grid-template-columns:1fr 1fr}
.client-detail-grid{grid-template-columns:1fr}
.client-order{grid-template-columns:52px minmax(0,1fr) 82px!important}
.client-order .client-order-date,.client-order .client-order-status{display:none!important}
#orders .head{display:none}
#ordersList{display:grid;gap:8px}
#ordersList .row{display:grid!important;grid-template-columns:72px minmax(0,1fr)!important;gap:4px 8px;padding:11px;border:1px solid var(--l);border-radius:8px}
#ordersList .row>span:nth-child(1){grid-row:1/3;font-weight:700;color:var(--n)}
#ordersList .row>span:nth-child(2){font-weight:700}
#ordersList .row>span:nth-child(3){font-size:12px;color:#68717a}
#ordersList .row>span:nth-child(4){display:none!important}
#ordersList .row>select{grid-column:1/-1!important;width:100%;margin-top:5px}
.stock-table{display:block;overflow-x:auto;white-space:nowrap;-webkit-overflow-scrolling:touch}
.stock-table table,.stock-table{min-width:680px}
.move-row{grid-template-columns:1fr!important;gap:3px!important}
.move-row span:last-child{grid-column:auto!important}
.dash-head{margin-bottom:10px}
.dash-head p{font-size:13px}
.dash-actions{display:grid;grid-template-columns:1fr 1fr}
.dash-actions button:first-child{grid-column:1/-1}
.dash-metrics{margin:10px 0 12px}
.dash-grid{gap:10px}
.dash-panel-head{align-items:flex-start;padding:11px}
.dash-panel-head small{font-size:11px;text-align:right}
.dash-list{padding:0 10px}
.dash-order{grid-template-columns:52px minmax(0,1fr)!important;padding:10px 0}
.dash-order-money,.dash-order button{display:none!important}
.dash-mini-grid{gap:7px;padding:10px}
.dash-needs{padding-inline:10px}
.dash-need{grid-template-columns:minmax(0,1fr) 82px!important}
.dash-need .cost{display:none!important}
.dash-alert{margin:0 10px 10px}
.sun-doc{font-size:12px}
.sun-doc-table-wrap{overflow-x:auto;-webkit-overflow-scrolling:touch}
.sun-doc table{min-width:600px}
.sun-doc-actions{display:grid!important;grid-template-columns:1fr 1fr}
.sun-doc-actions button{width:100%}
}
@media (max-width:390px){
.tiles{grid-template-columns:1fr 1fr}
.tile{min-height:130px}
.tile img,.tile .ph{height:68px}
.client-summary,.stock-summary,.dash-metrics{grid-template-columns:1fr!important}
.actions{grid-template-columns:1fr}
.actions>button{grid-column:auto!important}
header nav button{flex-basis:78px}
}
@media print{
body{padding-bottom:0!important;overflow:visible!important}
header nav{position:static!important}
}
`;
document.head.appendChild(style);
// Improve document tables on narrow screens without changing print output.
const wrapDocTables = () => {
document.querySelectorAll('.sun-doc table').forEach(table => {
if (table.parentElement?.classList.contains('sun-doc-table-wrap')) return;
const wrap = document.createElement('div');
wrap.className = 'sun-doc-table-wrap';
window.SunSafe.insertBefore(table.parentNode,wrap,table);
wrap.appendChild(table);
});
};
// Re-run only when the print/preview area changes; observing the whole app caused needless rescans.
let wrapQueued=false;
const observer = new MutationObserver(() => {if(wrapQueued)return;wrapQueued=true;requestAnimationFrame(()=>{wrapQueued=false;wrapDocTables();});});
const previewRoot=document.getElementById('preview');if(previewRoot)observer.observe(previewRoot,{childList:true,subtree:true});
wrapDocTables();
})();
/* ===== EVENT / PRODUCTION CARD MODULE v1 ===== */
(() => {
if (document.getElementById('sun-event-card-style')) return;
const detailsBox = document.getElementById('details');
if (!detailsBox) return;
const style = document.createElement('style');
style.id = 'sun-event-card-style';
style.textContent = `
.event-card-section{margin-top:18px;padding-top:16px;border-top:1px solid var(--l)}
.event-card-section h3{margin:0 0 12px;color:var(--n);font-size:17px}
.event-card-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px 12px}
.event-card-grid .wide{grid-column:1/-1}
.event-card-times{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px 12px;margin-top:10px}
.ops-notes{display:grid;grid-template-columns:1fr 1fr;gap:10px 12px;margin-top:10px}
.checklist-card{margin-top:16px;border:1px solid var(--l);border-radius:9px;background:#fafbfb;overflow:hidden}
.checklist-head{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:12px 14px;background:#f4f1e8;border-bottom:1px solid #e4dfd3}
.checklist-head h4{margin:0;color:var(--n);font-size:15px}.checklist-head b{color:var(--n)}
.checklist-progress{height:6px;background:#e5e7e8}.checklist-progress span{display:block;height:100%;background:var(--g);transition:width .2s ease}
.event-checklist{display:grid;grid-template-columns:1fr 1fr;gap:0;padding:4px 12px 10px}
.event-check{display:flex;flex-direction:row;align-items:center;gap:9px;padding:9px 6px;color:#203040;border-bottom:1px solid #eceeef;font-size:13px;cursor:pointer}
.event-check:nth-last-child(-n+2){border-bottom:0}.event-check input{width:18px;height:18px;accent-color:#c99a32;padding:0;flex:0 0 auto}
.event-check.done span{text-decoration:line-through;color:#879198}
.ops-status-pill{display:inline-flex;align-items:center;padding:3px 7px;border-radius:999px;font-size:10px;font-weight:700;margin-top:5px;background:#eef2f4;color:#50606b}
.ops-status-pill.complete{background:#eaf6ed;color:#39744a}
.ops-summary{margin-top:12px;display:grid;grid-template-columns:repeat(3,1fr);gap:8px}.ops-summary div{padding:9px;border-radius:7px;background:#f7f8f8}.ops-summary small{display:block;color:#7c858b}.ops-summary b{display:block;margin-top:3px;color:var(--n)}
@media(max-width:700px){.event-card-grid,.event-card-times,.ops-notes,.event-checklist{grid-template-columns:1fr}.event-check:nth-last-child(-n+2){border-bottom:1px solid #eceeef}.event-check:last-child{border-bottom:0}.ops-summary{grid-template-columns:1fr}}
`;
document.head.appendChild(style);
const section = document.createElement('div');
section.className = 'event-card-section';
section.innerHTML = `
<h3>Карточка мероприятия</h3>
<div class="event-card-grid">
<label>Количество гостей<input id="eventGuests" type="number" min="0" step="1" placeholder="Например, 25"></label>
<label>Ответственный<input id="eventResponsible" list="responsiblePeople" placeholder="Имя сотрудника"><datalist id="responsiblePeople"></datalist></label>
</div>
<div class="event-card-times">
<label>Начало приготовления<input id="eventPrepTime" type="time"></label>
<label>Время выезда<input id="eventDepartureTime" type="time"></label>
<label>Время подачи<input id="eventServingTime" type="time"></label>
</div>
<div class="ops-notes">
<label>Комментарий кухне<textarea id="eventKitchenNote" rows="4" placeholder="Особые требования, аллергии, подача, упаковка..."></textarea></label>
<label>Комментарий курьеру<textarea id="eventCourierNote" rows="4" placeholder="Контакт на месте, парковка, вход, этаж, ориентиры..."></textarea></label>
</div>
<div class="checklist-card">
<div class="checklist-head"><h4>Чек-лист готовности</h4><b id="eventChecklistText">0 / 7</b></div>
<div class="checklist-progress"><span id="eventChecklistBar" style="width:0%"></span></div>
<div class="event-checklist" id="eventChecklist">
<label class="event-check"><input type="checkbox" data-check="client"><span>Клиент, дата и адрес подтверждены</span></label>
<label class="event-check"><input type="checkbox" data-check="purchase"><span>Продукты и закупка готовы</span></label>
<label class="event-check"><input type="checkbox" data-check="kitchen"><span>Кухня завершила приготовление</span></label>
<label class="event-check"><input type="checkbox" data-check="packing"><span>Заказ упакован и промаркирован</span></label>
<label class="event-check"><input type="checkbox" data-check="equipment"><span>Посуда и дополнительный инвентарь собраны</span></label>
<label class="event-check"><input type="checkbox" data-check="courier"><span>Курьер / транспорт подтверждён</span></label>
<label class="event-check"><input type="checkbox" data-check="final"><span>Финальная проверка заказа выполнена</span></label>
</div>
</div>
<div class="ops-summary" id="eventOpsSummary"></div>
`;
detailsBox.appendChild(section);
const checklistKeys = ['client','purchase','kitchen','packing','equipment','courier','final'];
const $ = id => document.getElementById(id);
const getChecklist = source => {
const obj = source && typeof source === 'object' ? source : {};
return Object.fromEntries(checklistKeys.map(k => [k, Boolean(obj[k])]));
};
const countChecklist = obj => checklistKeys.filter(k => obj?.[k]).length;
function timeText(v){ return v || '—'; }
function refreshResponsibleList(){
const names = [...new Set(orders.map(o => (o.responsible || '').trim()).filter(Boolean))].sort((a,b)=>a.localeCompare(b,'ru'));
$('responsiblePeople').innerHTML = names.map(name => `<option value="${String(name).replace(/"/g,'&quot;')}"></option>`).join('');
}
function readFormIntoDraft(){
draft.guestsCount = Math.max(0, Number($('eventGuests').value || 0));
draft.responsible = $('eventResponsible').value.trim();
draft.prepTime = $('eventPrepTime').value || '';
draft.departureTime = $('eventDepartureTime').value || '';
draft.servingTime = $('eventServingTime').value || '';
draft.kitchenNote = $('eventKitchenNote').value.trim();
draft.courierNote = $('eventCourierNote').value.trim();
draft.checklist = getChecklist(Object.fromEntries([...document.querySelectorAll('#eventChecklist [data-check]')].map(input => [input.dataset.check, input.checked])));
}
function fillFormFromDraft(){
$('eventGuests').value = Number(draft.guestsCount || 0) || '';
$('eventResponsible').value = draft.responsible || '';
$('eventPrepTime').value = draft.prepTime || '';
$('eventDepartureTime').value = draft.departureTime || '';
$('eventServingTime').value = draft.servingTime || '';
$('eventKitchenNote').value = draft.kitchenNote || '';
$('eventCourierNote').value = draft.courierNote || '';
const state = getChecklist(draft.checklist);
document.querySelectorAll('#eventChecklist [data-check]').forEach(input => input.checked = Boolean(state[input.dataset.check]));
refreshResponsibleList();
renderChecklistProgress();
}
function renderChecklistProgress(){
const checks = [...document.querySelectorAll('#eventChecklist [data-check]')];
const done = checks.filter(x => x.checked).length;
const pct = Math.round(done / checklistKeys.length * 100);
checks.forEach(input => input.closest('.event-check')?.classList.toggle('done', input.checked));
$('eventChecklistText').textContent = `${done} / ${checklistKeys.length}`;
$('eventChecklistBar').style.width = `${pct}%`;
$('eventOpsSummary').innerHTML = `
<div><small>Гостей</small><b>${Math.max(0,Number($('eventGuests').value||0)) || '—'}</b></div>
<div><small>Ответственный</small><b>${$('eventResponsible').value.trim() || '—'}</b></div>
<div><small>Готовность</small><b>${pct}%</b></div>`;
}
document.querySelectorAll('#eventChecklist [data-check]').forEach(input => input.addEventListener('change', () => { readFormIntoDraft(); renderChecklistProgress(); }));
['eventGuests','eventResponsible','eventPrepTime','eventDepartureTime','eventServingTime','eventKitchenNote','eventCourierNote'].forEach(id => $(id).addEventListener('input', () => { readFormIntoDraft(); renderChecklistProgress(); }));
// Save operational fields together with the order before all existing save hooks run.
const previousSaveOrder = window.saveOrder;
window.saveOrder = () => {
readFormIntoDraft();
previousSaveOrder();
refreshResponsibleList();
decorateOrderRows();
};
// Existing openOrder populates the standard order fields first; then we populate the event card.
const previousOpenOrder = window.openOrder;
window.openOrder = id => {
previousOpenOrder(id);
fillFormFromDraft();
};
if (typeof window.resetDraft === 'function') {
const previousResetDraft = window.resetDraft;
window.resetDraft = () => {
previousResetDraft();
fillFormFromDraft();
};
}
function readiness(order){
const done = countChecklist(getChecklist(order?.checklist));
return {done,total:checklistKeys.length,pct:Math.round(done/checklistKeys.length*100)};
}
function decorateOrderRows(){
const rows = [...document.querySelectorAll('#ordersList .row')];
rows.forEach((row,index) => {
const order = orders[index];
if (!order) return;
row.querySelectorAll('.ops-status-pill').forEach(x=>x.remove());
const eventCell = row.children[1];
if (!eventCell) return;
const r = readiness(order);
const extra = document.createElement('small');
extra.className = `ops-status-pill ${r.pct === 100 ? 'complete' : ''}`;
const parts = [`готовность ${r.pct}%`];
if (order.guestsCount) parts.push(`${order.guestsCount} гостей`);
if (order.responsible) parts.push(order.responsible);
extra.textContent = parts.join(' · ');
eventCell.appendChild(extra);
});
}
const previousRenderOrders = window.renderOrders;
window.renderOrders = () => { previousRenderOrders(); decorateOrderRows(); };
// Add compact operational information to printed order documents without changing receipt logic.
const actions = [...document.querySelectorAll('#new .actions')].find(a=>[...a.querySelectorAll('button')].some(b=>b.textContent.trim()==='Сохранить'));
if (actions && !document.getElementById('openEventCardBtn')) {
const btn = document.createElement('button');
btn.id = 'openEventCardBtn';
btn.className = 'outline';
btn.type = 'button';
btn.textContent = 'Карточка мероприятия';
btn.onclick = () => { if (typeof details === 'function') details(true); section.scrollIntoView({behavior:'smooth',block:'start'}); };
actions.insertBefore(btn, actions.lastElementChild || null);
}
fillFormFromDraft();
decorateOrderRows();
})();
/* ===== TEAM / STAFF ASSIGNMENTS MODULE v1 ===== */
(() => {
if (document.getElementById('sun-team-style')) return;
const nav = document.querySelector('nav');
const detailsBox = document.getElementById('details');
if (!nav || !detailsBox) return;
const STORAGE_KEY = 'sunEmployees';
const DEFAULT_ROLES = ['Шеф-повар','Повар','Помощник повара','Комплектовщик','Курьер','Официант','Менеджер'];
let employees = [];
try { employees = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]'); } catch(e) { employees = []; }
if (!Array.isArray(employees)) employees = [];
employees = employees.map(e => ({
id: String(e.id || (crypto.randomUUID ? sunUUID() : Date.now()+Math.random())),
name: String(e.name || '').trim(),
role: String(e.role || 'Сотрудник').trim(),
phone: String(e.phone || '').trim(),
active: e.active !== false,
note: String(e.note || '').trim()
})).filter(e => e.name);
const persistEmployees = () => localStorage.setItem(STORAGE_KEY, JSON.stringify(employees));
const esc=window.SunSafe.escapeHTML;
const fmtMoney = v => (typeof money === 'function' ? money(v) : `${Math.round(Number(v||0)).toLocaleString('ru-RU')}`);
const orderTotalForTeam = o => Number(o?.total || 0) || (o?.lines || []).reduce((s,l)=>{
const b = boxes.find(x=>String(x.id)===String(l.id));
return s + Number(b?.price||0) * Number(l.qty||0);
},0);
const orderStamp = o => `${o?.date || ''}T${o?.time || '00:00'}`;
const activeOrder = o => !['Отменён','Отдан заказчику'].includes(o?.status || 'Новый');
const style = document.createElement('style');
style.id = 'sun-team-style';
style.textContent = `
.team-toolbar{display:flex;justify-content:space-between;align-items:center;gap:10px;margin:14px 0;flex-wrap:wrap}
.team-summary{display:grid;grid-template-columns:repeat(4,minmax(120px,1fr));gap:10px;margin:12px 0 18px}
.team-summary>div{background:#f7f8f8;border-radius:9px;padding:12px}.team-summary small{display:block;color:#7c858b}.team-summary b{display:block;color:var(--n);font-size:20px;margin-top:4px}
.team-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:12px}
.team-card{border:1px solid var(--l);border-radius:10px;background:#fff;padding:14px}.team-card.inactive{opacity:.58}.team-card h3{margin:0;color:var(--n);font-size:17px}.team-card p{margin:5px 0;color:#68717a}.team-card .role{display:inline-flex;padding:4px 8px;border-radius:999px;background:#f4f1e8;color:#7a632c;font-size:11px;font-weight:700;margin-top:6px}.team-card .load{margin:10px 0;padding:8px;border-radius:7px;background:#f7f8f8}.team-card .load b{color:var(--n)}
.team-card .actions{display:flex;flex-wrap:wrap}.team-schedule{margin-top:22px}.team-schedule h2{font-size:19px;color:var(--n);margin:0 0 10px}.team-schedule-row{display:grid;grid-template-columns:150px 1fr 100px;gap:10px;padding:10px 0;border-bottom:1px solid #e6e8e9;align-items:start}.team-schedule-row .events{display:flex;gap:6px;flex-wrap:wrap}.team-shift{border:1px solid #d9dde0;border-radius:7px;background:#fafbfb;padding:6px 8px;font-size:12px;text-align:left}.team-shift b{display:block;color:var(--n)}.team-conflict{display:inline-flex;margin-top:3px;padding:2px 6px;border-radius:999px;background:#fbeeee;color:#9c3d3d;font-size:10px;font-weight:700}
.staff-assignment{margin-top:16px;border:1px solid var(--l);border-radius:9px;overflow:hidden;background:#fff}.staff-assignment-head{padding:11px 13px;background:#eef2f4;display:flex;justify-content:space-between;gap:10px;align-items:center}.staff-assignment-head h4{margin:0;color:var(--n);font-size:15px}.staff-assignment-list{padding:4px 12px 10px;display:grid;grid-template-columns:1fr 1fr;gap:0 14px}.staff-choice{display:flex;flex-direction:row;align-items:center;gap:9px;padding:9px 5px;border-bottom:1px solid #edf0f1;color:#203040;cursor:pointer}.staff-choice input{width:18px;height:18px;accent-color:#c99a32;flex:0 0 auto}.staff-choice span{min-width:0}.staff-choice small{display:block;color:#7b858b;margin-top:2px}.staff-empty{padding:14px;color:#879198}.staff-assignment-meta{font-size:12px;color:#68717a}.staff-assignment-meta.warn{color:#9c3d3d;font-weight:700}
.team-modal-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px}.team-modal-grid .wide{grid-column:1/-1}.team-active-label{display:flex;flex-direction:row;align-items:center;gap:8px;margin-top:12px}.team-active-label input{width:18px;height:18px}
@media(max-width:760px){.team-summary{grid-template-columns:1fr 1fr}.team-schedule-row{grid-template-columns:1fr}.team-schedule-row>div:last-child{text-align:left!important}.staff-assignment-list{grid-template-columns:1fr}.team-modal-grid{grid-template-columns:1fr}.team-modal-grid .wide{grid-column:auto}}
`;
document.head.appendChild(style);
// Navigation and team page.
const teamBtn = document.createElement('button');
teamBtn.type = 'button';
teamBtn.textContent = 'Команда';
nav.appendChild(teamBtn);
const teamView = document.createElement('section');
teamView.id = 'team';
teamView.className = 'view page';
teamView.innerHTML = `
<h1>Команда</h1>
<div class="team-toolbar">
<div><b>Сотрудники и загрузка</b><div class="hint">Назначайте сотрудников на мероприятия и контролируйте пересечения.</div></div>
<button class="primary" type="button" id="teamAddBtn">Добавить сотрудника</button>
</div>
<div class="team-summary" id="teamSummary"></div>
<div class="team-grid" id="teamGrid"></div>
<div class="team-schedule"><h2>Загрузка на ближайшие 14 дней</h2><div id="teamSchedule"></div></div>`;
document.body.appendChild(teamView);
teamBtn.onclick = () => {
document.querySelectorAll('.view').forEach(v=>v.classList.remove('on'));
teamView.classList.add('on');
[...nav.querySelectorAll('button')].forEach(b=>b.classList.remove('on'));
teamBtn.classList.add('on');
renderTeam();
};
// Employee editor modal.
const modalEl = document.createElement('div');
modalEl.id = 'teamEditor';
modalEl.className = 'modal';
modalEl.innerHTML = `<div class="dialog" style="width:min(620px,100%)">
<div class="dialog-head"><h2 id="teamEditorTitle">Сотрудник</h2><button type="button" id="teamEditorClose">×</button></div>
<div class="team-modal-grid">
<label>Имя<input id="teamName" placeholder="Имя сотрудника"></label>
<label>Роль<input id="teamRole" list="teamRoles" placeholder="Например, повар"><datalist id="teamRoles">${DEFAULT_ROLES.map(r=>`<option value="${esc(r)}"></option>`).join('')}</datalist></label>
<label>Телефон<input id="teamPhone" placeholder="+7 ..."></label>
<label class="wide">Комментарий<textarea id="teamNote" rows="3" placeholder="Навыки, график, особенности..."></textarea></label>
</div>
<label class="team-active-label"><input id="teamActive" type="checkbox" checked> Активный сотрудник</label>
<div class="actions"><button class="primary" type="button" id="teamSave">Сохранить</button><button class="danger" type="button" id="teamDelete" style="display:none">Удалить</button></div>
</div>`;
document.body.appendChild(modalEl);
let editingEmployeeId = null;
const el = id => document.getElementById(id);
el('teamEditorClose').onclick = () => modalEl.classList.remove('on');
modalEl.addEventListener('click', e => { if (e.target === modalEl) modalEl.classList.remove('on'); });
function openEmployee(id=null){
const emp = id ? employees.find(x=>x.id===String(id)) : null;
editingEmployeeId = emp?.id || null;
el('teamEditorTitle').textContent = emp ? 'Изменить сотрудника' : 'Новый сотрудник';
el('teamName').value = emp?.name || '';
el('teamRole').value = emp?.role || '';
el('teamPhone').value = emp?.phone || '';
el('teamNote').value = emp?.note || '';
el('teamActive').checked = emp ? emp.active !== false : true;
el('teamDelete').style.display = emp ? 'inline-block' : 'none';
modalEl.classList.add('on');
}
window.sunEditEmployee = openEmployee;
el('teamAddBtn').onclick = () => openEmployee();
el('teamSave').onclick = () => {
const name = el('teamName').value.trim();
if (!name) return alert('Введите имя сотрудника.');
const record = {
id: editingEmployeeId || (crypto.randomUUID ? sunUUID() : String(Date.now())),
name,
role: el('teamRole').value.trim() || 'Сотрудник',
phone: el('teamPhone').value.trim(),
note: el('teamNote').value.trim(),
active: el('teamActive').checked
};
const idx = employees.findIndex(x=>x.id===record.id);
if (idx >= 0) employees[idx] = record; else employees.push(record);
employees.sort((a,b)=>a.name.localeCompare(b.name,'ru'));
persistEmployees();
modalEl.classList.remove('on');
renderTeam();
renderStaffAssignments();
refreshResponsibleDatalistFromTeam();
};
el('teamDelete').onclick = () => {
const emp = employees.find(x=>x.id===editingEmployeeId);
if (!emp || !confirm(`Удалить сотрудника «${emp.name}»? Назначения в старых заказах будут сняты.`)) return;
employees = employees.filter(x=>x.id!==editingEmployeeId);
orders.forEach(o => { if (Array.isArray(o.staffIds)) o.staffIds = o.staffIds.filter(id=>String(id)!==editingEmployeeId); });
if (Array.isArray(draft.staffIds)) draft.staffIds = draft.staffIds.filter(id=>String(id)!==editingEmployeeId);
persistEmployees();
if (typeof persist === 'function') persist();
modalEl.classList.remove('on');
renderTeam();
renderStaffAssignments();
refreshResponsibleDatalistFromTeam();
};
function assignedOrders(empId){
return orders.filter(o => activeOrder(o) && Array.isArray(o.staffIds) && o.staffIds.map(String).includes(String(empId)));
}
function conflictCount(empId){
const list = assignedOrders(empId);
const groups = {};
list.forEach(o => { const key = `${o.date||''}|${o.time||''}`; groups[key]=(groups[key]||0)+1; });
return Object.values(groups).filter(n=>n>1).reduce((s,n)=>s+(n-1),0);
}
function openTeamOrder(id){ if (typeof window.openOrder === 'function') window.openOrder(id); }
window.openTeamOrder = openTeamOrder;
function renderTeam(){
const now = new Date(); now.setHours(0,0,0,0);
const until = new Date(now); until.setDate(until.getDate()+14);
const upcoming = orders.filter(o=>{
if (!activeOrder(o) || !o.date) return false;
const d = new Date(`${o.date}T00:00:00`);
return d >= now && d < until;
});
const activeCount = employees.filter(e=>e.active).length;
const assignedSet = new Set(upcoming.flatMap(o=>Array.isArray(o.staffIds)?o.staffIds.map(String):[]));
const conflicts = employees.reduce((s,e)=>s+conflictCount(e.id),0);
el('teamSummary').innerHTML = `
<div><small>Сотрудников</small><b>${employees.length}</b></div>
<div><small>Активных</small><b>${activeCount}</b></div>
<div><small>Задействованы 14 дней</small><b>${assignedSet.size}</b></div>
<div><small>Пересечений</small><b>${conflicts}</b></div>`;
el('teamGrid').innerHTML = employees.length ? employees.map(emp=>{
const list = assignedOrders(emp.id).sort((a,b)=>orderStamp(a).localeCompare(orderStamp(b)));
const future = list.filter(o=>o.date && new Date(`${o.date}T23:59:59`) >= now).slice(0,3);
const conflicts = conflictCount(emp.id);
return `<div class="team-card ${emp.active?'':'inactive'}">
<h3>${esc(emp.name)}</h3><span class="role">${esc(emp.role)}</span>
<p>${emp.phone ? esc(emp.phone) : 'Телефон не указан'}</p>
<div class="load"><b>${future.length}</b> ближайших назначений${conflicts?`<div class="team-conflict">⚠ пересечений: ${conflicts}</div>`:''}</div>
${future.map(o=>`<button class="team-shift" type="button" onclick="openTeamOrder(${Number(o.id)})"><b>${esc(o.date)} · ${esc(o.time||'—')}</b>${esc(o.event||'Заказ')}</button>`).join('') || '<p class="hint">Нет ближайших назначений.</p>'}
<div class="actions"><button class="outline" type="button" onclick="sunEditEmployee('${esc(emp.id)}')">Изменить</button></div>
</div>`;
}).join('') : '<p class="empty">Сотрудников пока нет. Добавьте первого сотрудника.</p>';
const byDate = {};
upcoming.sort((a,b)=>orderStamp(a).localeCompare(orderStamp(b))).forEach(o=>{ (byDate[o.date] ||= []).push(o); });
el('teamSchedule').innerHTML = Object.keys(byDate).length ? Object.entries(byDate).map(([dateKey,list])=>{
const shifts = list.map(o=>{
const staff = (Array.isArray(o.staffIds)?o.staffIds:[]).map(id=>employees.find(e=>e.id===String(id))).filter(Boolean);
return `<button class="team-shift" type="button" onclick="openTeamOrder(${Number(o.id)})"><b>${esc(o.time||'—')} · №${esc(o.id)}</b>${esc(o.event||'Заказ')}<br><small>${staff.length ? staff.map(s=>esc(s.name)).join(', ') : 'Команда не назначена'}</small></button>`;
}).join('');
return `<div class="team-schedule-row"><div><b>${esc(dateKey)}</b><div class="hint">${list.length} мероприятий</div></div><div class="events">${shifts}</div><div style="text-align:right"><b>${fmtMoney(list.reduce((s,o)=>s+orderTotalForTeam(o),0))}</b></div></div>`;
}).join('') : '<p class="empty">На ближайшие 14 дней активных мероприятий нет.</p>';
}
// Assignment block inside event card.
const eventSection = detailsBox.querySelector('.event-card-section');
const assignment = document.createElement('div');
assignment.className = 'staff-assignment';
assignment.innerHTML = `<div class="staff-assignment-head"><h4>Команда на мероприятие</h4><span class="staff-assignment-meta" id="staffAssignmentMeta">0 назначено</span></div><div id="staffAssignmentList" class="staff-assignment-list"></div>`;
if (eventSection) {
const notes = eventSection.querySelector('.ops-notes');
if (notes) notes.insertAdjacentElement('afterend', assignment); else eventSection.appendChild(assignment);
} else detailsBox.appendChild(assignment);
function currentSelectedIds(){
return [...document.querySelectorAll('#staffAssignmentList [data-staff-id]:checked')].map(x=>String(x.dataset.staffId));
}
function readStaffIntoDraft(){ draft.staffIds = currentSelectedIds(); }
function currentDraftStamp(){ return `${date?.value || draft.date || ''}|${time?.value || draft.time || ''}`; }
function employeeHasConflict(empId){
const stamp = currentDraftStamp();
if (!stamp || stamp.startsWith('|')) return false;
return orders.some(o => o.id != draft.id && activeOrder(o) && `${o.date||''}|${o.time||''}` === stamp && Array.isArray(o.staffIds) && o.staffIds.map(String).includes(String(empId)));
}
function renderStaffAssignments(){
const list = el('staffAssignmentList');
if (!list) return;
const selected = new Set((Array.isArray(draft.staffIds)?draft.staffIds:[]).map(String));
const active = employees.filter(e=>e.active || selected.has(e.id));
list.innerHTML = active.length ? active.map(emp=>{
const conflict = employeeHasConflict(emp.id);
return `<label class="staff-choice"><input type="checkbox" data-staff-id="${esc(emp.id)}" ${selected.has(emp.id)?'checked':''}><span><b>${esc(emp.name)}</b><small>${esc(emp.role)}${conflict?' · ⚠ уже назначен на это время':''}</small></span></label>`;
}).join('') : '<div class="staff-empty">Сначала добавьте сотрудников в разделе «Команда».</div>';
list.querySelectorAll('[data-staff-id]').forEach(input=>input.addEventListener('change',()=>{ readStaffIntoDraft(); updateStaffMeta(); }));
updateStaffMeta();
}
function updateStaffMeta(){
const meta = el('staffAssignmentMeta'); if (!meta) return;
const ids = currentSelectedIds();
const conflicts = ids.filter(employeeHasConflict).length;
meta.textContent = `${ids.length} назначено${conflicts ? ` · ⚠ конфликтов ${conflicts}` : ''}`;
meta.classList.toggle('warn', conflicts>0);
}
function refreshResponsibleDatalistFromTeam(){
const dl = document.getElementById('responsiblePeople');
if (!dl) return;
const names = new Set(employees.filter(e=>e.active).map(e=>e.name));
orders.map(o=>(o.responsible||'').trim()).filter(Boolean).forEach(n=>names.add(n));
dl.innerHTML = [...names].sort((a,b)=>a.localeCompare(b,'ru')).map(n=>`<option value="${esc(n)}"></option>`).join('');
}
// Save/open/reset hooks after the event-card module.
const priorSave = window.saveOrder;
window.saveOrder = () => {
readStaffIntoDraft();
priorSave();
renderTeam();
};
const priorOpen = window.openOrder;
window.openOrder = id => {
priorOpen(id);
renderStaffAssignments();
refreshResponsibleDatalistFromTeam();
};
if (typeof window.resetDraft === 'function') {
const priorReset = window.resetDraft;
window.resetDraft = () => { priorReset(); renderStaffAssignments(); };
}
const priorRenderOrders = window.renderOrders;
window.renderOrders = () => { priorRenderOrders(); if (teamView.classList.contains('on')) renderTeam(); };
// Refresh conflict warnings when the order date/time changes.
['date','time'].forEach(id => document.getElementById(id)?.addEventListener('change', renderStaffAssignments));
persistEmployees();
refreshResponsibleDatalistFromTeam();
renderStaffAssignments();
renderTeam();
})();
/* ===== DAILY PRODUCTION SHEET + STABILITY PATCH v1 ===== */
(() => {
if (document.getElementById('sun-production-style')) return;
const nav = document.querySelector('nav');
if (!nav) return;
const esc=window.SunSafe.escapeHTML;
const fmt = v => typeof money === 'function' ? money(v) : `${Math.round(Number(v||0)).toLocaleString('ru-RU')}`;
const itemById = id => boxes.find(x => String(x.id) === String(id));
const orderTotal = o => Number(o?.total || 0) || (o?.lines || []).reduce((s,l)=>s+Number(itemById(l.id)?.price||0)*Number(l.qty||0),0);
const isWork = o => !['Отменён','Отдан заказчику'].includes(o?.status || 'Новый');
const getEmployees = () => { try { const x=JSON.parse(localStorage.getItem('sunEmployees')||'[]'); return Array.isArray(x)?x:[]; } catch(e){ return []; } };
const getStock = () => { try { const x=JSON.parse(localStorage.getItem('sunStock')||'[]'); return Array.isArray(x)?x:[]; } catch(e){ return []; } };
const stockKey = (name,unit) => `${String(name||'').trim().toLowerCase()}|${String(unit||'').trim().toLowerCase()}`;
const style = document.createElement('style');
style.id = 'sun-production-style';
style.textContent = `
.prod-head{display:flex;justify-content:space-between;gap:12px;align-items:flex-start;flex-wrap:wrap}.prod-head .actions{margin:0}.prod-datebar{display:flex;align-items:end;gap:8px;flex-wrap:wrap;margin:14px 0}.prod-datebar label{min-width:180px}.prod-summary{display:grid;grid-template-columns:repeat(5,minmax(130px,1fr));gap:9px;margin:12px 0 18px}.prod-summary>div{border:1px solid var(--l);background:#fafafa;border-radius:8px;padding:11px}.prod-summary small{display:block;color:#68717a}.prod-summary b{display:block;color:var(--n);font-size:19px;margin-top:4px}
.prod-section{border:1px solid var(--l);border-radius:10px;background:#fff;margin:12px 0;overflow:hidden}.prod-section-head{display:flex;justify-content:space-between;gap:10px;align-items:center;background:#f6f7f7;padding:11px 13px}.prod-section-head h2{font-size:17px;margin:0;color:var(--n)}.prod-body{padding:0 13px 12px}.prod-order{display:grid;grid-template-columns:100px minmax(180px,1.3fr) minmax(160px,1fr) 130px 110px;gap:10px;padding:12px 0;border-bottom:1px solid #e7eaeb;align-items:start}.prod-order:last-child{border-bottom:0}.prod-time b{font-size:17px;color:var(--n)}.prod-order small{display:block;color:#68717a;margin-top:3px}.prod-order .staff{display:flex;flex-wrap:wrap;gap:4px}.prod-chip{font-size:11px;background:#eef2f4;border-radius:999px;padding:3px 7px}.prod-status{font-size:11px;font-weight:700;border-radius:999px;padding:4px 8px;background:#f4f1e8;display:inline-block}.prod-actions{display:flex;flex-direction:column;gap:5px}.prod-actions button{padding:6px 8px}
.prod-table-wrap{overflow:auto}.prod-table{width:100%;border-collapse:collapse;min-width:680px}.prod-table th,.prod-table td{padding:9px 7px;border-bottom:1px solid #e7eaeb;text-align:left}.prod-table th{font-size:12px;color:#68717a;background:#fafafa}.prod-table td.num,.prod-table th.num{text-align:right;white-space:nowrap}.prod-deficit{color:#a33;font-weight:700}.prod-ok{color:#287a49}.prod-empty{padding:18px 0;color:#879198}.prod-print-title{display:none}
@media(max-width:900px){.prod-summary{grid-template-columns:1fr 1fr}.prod-order{grid-template-columns:72px 1fr}.prod-order>div:nth-child(3),.prod-order>div:nth-child(4){grid-column:2}.prod-actions{grid-column:1/-1;flex-direction:row}.prod-actions button{flex:1}}
@media print{body *{visibility:hidden!important}#production,#production *{visibility:visible!important}#production{position:absolute;inset:0;margin:0!important;padding:10mm!important;box-shadow:none!important}.prod-head .actions,.prod-datebar,.prod-actions,header{display:none!important}.prod-print-title{display:block!important}.prod-section{break-inside:avoid}.prod-order{grid-template-columns:80px 1.2fr 1fr 110px}.prod-actions{display:none!important}.prod-summary{grid-template-columns:repeat(5,1fr)}}
`;
document.head.appendChild(style);
const btn = document.createElement('button');
btn.type = 'button'; btn.textContent = 'Производство';
nav.appendChild(btn);
const view = document.createElement('section');
view.id = 'production'; view.className = 'view page';
view.innerHTML = `<div class="prod-head"><div><h1>Производственный лист</h1><p class="hint" style="margin:5px 0 0">Все заказы выбранного дня: кухня, комплектация, выезд, команда и потребность.</p></div><div class="actions"><button class="outline" type="button" id="prodPrint">Печать / PDF</button></div></div><div class="prod-print-title"><h1>${window.CateriumBranding.nameHTML()} · Производственный лист</h1></div><div class="prod-datebar"><button class="outline" type="button" id="prodPrev">←</button><label>Дата<input type="date" id="prodDate"></label><button class="outline" type="button" id="prodNext">→</button><button class="outline" type="button" id="prodToday">Сегодня</button></div><div id="prodContent"></div>`;
document.body.appendChild(view);
const dateInput = view.querySelector('#prodDate');
const today = () => new Date().toISOString().slice(0,10);
dateInput.value = today();
const moveDate = d => { const x=new Date(`${dateInput.value||today()}T12:00:00`); x.setDate(x.getDate()+d); dateInput.value=x.toISOString().slice(0,10); renderProduction(); };
view.querySelector('#prodPrev').onclick=()=>moveDate(-1);
view.querySelector('#prodNext').onclick=()=>moveDate(1);
view.querySelector('#prodToday').onclick=()=>{dateInput.value=today();renderProduction();};
view.querySelector('#prodPrint').onclick=()=>window.print();
dateInput.onchange=renderProduction;
function staffFor(o){
const emps=getEmployees();
return (Array.isArray(o.staffIds)?o.staffIds:[]).map(id=>emps.find(e=>String(e.id)===String(id))).filter(Boolean);
}
function aggregateLines(dayOrders){
const map=new Map();
dayOrders.forEach(o=>(o.lines||[]).forEach(l=>{
const b=itemById(l.id); if(!b) return;
const key=String(b.id); const prev=map.get(key)||{name:b.name,qty:0,price:Number(b.price||0)}; prev.qty+=Number(l.qty||0); map.set(key,prev);
}));
return [...map.values()].sort((a,b)=>a.name.localeCompare(b.name,'ru'));
}
function aggregatePrep(dayOrders){
const map=new Map();
dayOrders.forEach(o=>(o.lines||[]).forEach(l=>{
const b=itemById(l.id); if(!b || !Array.isArray(b.ingredients)) return;
b.ingredients.forEach(x=>{
if(!Array.isArray(x) || !x[0]) return;
const name=x[0], qty=Number(x[1]||0)*Number(l.qty||0), unit=x[2]||'шт.';
const key=stockKey(name,unit); const p=map.get(key)||{name,unit,qty:0}; p.qty+=qty; map.set(key,p);
});
}));
return [...map.values()].sort((a,b)=>a.name.localeCompare(b.name,'ru'));
}
function renderProduction(){
const key=dateInput.value||today();
const dayOrders=orders.filter(o=>o.date===key && isWork(o)).sort((a,b)=>String(a.time||'').localeCompare(String(b.time||'')));
const total=dayOrders.reduce((s,o)=>s+orderTotal(o),0);
const guests=dayOrders.reduce((s,o)=>s+Number(o.guestsCount||0),0);
const staffSet=new Set(dayOrders.flatMap(o=>(Array.isArray(o.staffIds)?o.staffIds.map(String):[])));
const boxesList=aggregateLines(dayOrders);
const prep=aggregatePrep(dayOrders);
const stock=getStock(); const stockMap=new Map(stock.map(p=>[stockKey(p.name,p.unit),p]));
const deficits=prep.filter(p=>p.qty>Number(stockMap.get(stockKey(p.name,p.unit))?.qty||stockMap.get(stockKey(p.name,p.unit))?.balance||0));
const orderHtml=dayOrders.map(o=>{
const staff=staffFor(o); const ready = o.checklist && typeof o.checklist==='object' ? Object.values(o.checklist).filter(Boolean).length : 0;
const prepTime=o.prepTime||'—', depart=o.departureTime||'—', serve=o.serveTime||o.time||'—';
return `<div class="prod-order"><div class="prod-time"><b>${esc(o.time||'—')}</b><small>Подача ${esc(serve)}</small></div><div><b>№${esc(o.id)} · ${esc(o.event||'Заказ')}</b><small>${esc(o.contact||'Клиент не указан')} · ${esc(o.address||'Адрес не указан')}</small><small>${o.guestsCount?`${esc(o.guestsCount)} гостей · `:''}${fmt(orderTotal(o))}</small>${o.kitchenNote?`<small><b>Кухня:</b> ${esc(o.kitchenNote)}</small>`:''}</div><div><small>Начать готовить: <b>${esc(prepTime)}</b></small><small>Выезд: <b>${esc(depart)}</b></small><div class="staff">${staff.length?staff.map(s=>`<span class="prod-chip">${esc(s.role||'')} · ${esc(s.name)}</span>`).join(''):'<span class="prod-chip">Команда не назначена</span>'}</div>${o.courierNote?`<small><b>Курьеру:</b> ${esc(o.courierNote)}</small>`:''}</div><div><span class="prod-status">${esc(o.status||'Новый')}</span><small>Чек-лист: ${ready}/7</small></div><div class="prod-actions"><button class="outline" type="button" onclick="productionOpenOrder(${Number(o.id)})">Открыть</button>${typeof window.writeOffOrderStock==='function'?`<button class="outline" type="button" onclick="writeOffOrderStock(${Number(o.id)})">Списать склад</button>`:''}</div></div>`;
}).join('');
const boxesHtml=boxesList.length?`<div class="prod-table-wrap"><table class="prod-table"><thead><tr><th>Позиция</th><th class="num">Кол-во</th><th class="num">Цена</th><th class="num">Сумма</th></tr></thead><tbody>${boxesList.map(x=>`<tr><td>${esc(x.name)}</td><td class="num"><b>${x.qty}</b></td><td class="num">${fmt(x.price)}</td><td class="num">${fmt(x.price*x.qty)}</td></tr>`).join('')}</tbody></table></div>`:'<div class="prod-empty">Позиций на эту дату нет.</div>';
const prepHtml=prep.length?`<div class="prod-table-wrap"><table class="prod-table"><thead><tr><th>Продукт / заготовка</th><th class="num">Нужно</th><th class="num">На складе</th><th class="num">Не хватает</th></tr></thead><tbody>${prep.map(p=>{const sp=stockMap.get(stockKey(p.name,p.unit));const have=Number(sp?.qty||sp?.balance||0);const lack=Math.max(0,p.qty-have);return `<tr><td>${esc(p.name)}</td><td class="num"><b>${Number(p.qty.toFixed(2))} ${esc(p.unit)}</b></td><td class="num">${Number(have.toFixed(2))} ${esc(p.unit)}</td><td class="num ${lack?'prod-deficit':'prod-ok'}">${lack?`${Number(lack.toFixed(2))} ${esc(p.unit)}`:'✓'}</td></tr>`;}).join('')}</tbody></table></div>`:'<div class="prod-empty">Составы боксов не требуют расчёта или заказов нет.</div>';
view.querySelector('#prodContent').innerHTML=`<div class="prod-summary"><div><small>Заказов</small><b>${dayOrders.length}</b></div><div><small>Гостей</small><b>${guests}</b></div><div><small>Боксов / позиций</small><b>${boxesList.reduce((s,x)=>s+x.qty,0)}</b></div><div><small>Сотрудников</small><b>${staffSet.size}</b></div><div><small>Сумма дня</small><b>${fmt(total)}</b></div></div><section class="prod-section"><div class="prod-section-head"><h2>План дня · ${esc(key)}</h2><span class="hint">${dayOrders.length} заказов</span></div><div class="prod-body">${orderHtml||'<div class="prod-empty">На выбранную дату активных заказов нет.</div>'}</div></section><section class="prod-section"><div class="prod-section-head"><h2>Сводка по продукции</h2><span class="hint">Что приготовить и собрать</span></div><div class="prod-body">${boxesHtml}</div></section><section class="prod-section"><div class="prod-section-head"><h2>Продукты и заготовки</h2><span class="hint">Дефицит: ${deficits.length} поз.</span></div><div class="prod-body">${prepHtml}</div></section>`;
}
window.productionOpenOrder=id=>{ if(typeof window.openOrder==='function') window.openOrder(id); };
btn.onclick=()=>{document.querySelectorAll('.view').forEach(v=>v.classList.remove('on'));view.classList.add('on');[...nav.querySelectorAll('button')].forEach(b=>b.classList.remove('on'));btn.classList.add('on');renderProduction();};
// Keep the production sheet synchronized with mutations in other modules.
if (typeof window.saveOrder === 'function') { const p=window.saveOrder; window.saveOrder=()=>{p(); if(view.classList.contains('on')) renderProduction();}; }
if (typeof window.renderOrders === 'function') { const p=window.renderOrders; window.renderOrders=()=>{p(); if(view.classList.contains('on')) renderProduction();}; }
renderProduction();
})();
/* ===== SHOPPING LIST MODULE v2 · SUPPLIER GROUPING ===== */
(() => {
if (document.getElementById('sun-shopping-style')) return;
const nav = document.querySelector('nav');
if (!nav) return;
const esc=window.SunSafe.escapeHTML;
const fmt = v => typeof money === 'function' ? money(v) : `${Math.round(Number(v||0)).toLocaleString('ru-RU')}`;
const qtyFmt = v => Number(v || 0).toLocaleString('ru-RU', { maximumFractionDigits: 3 });
const itemById = id => boxes.find(x => String(x.id) === String(id));
const getStock = () => { try { const x=JSON.parse(localStorage.getItem('sunStock')||'[]'); return Array.isArray(x)?x:[]; } catch { return []; } };
const getSuppliers = () => { try { const x=JSON.parse(localStorage.getItem('sunSuppliers')||'[]'); return Array.isArray(x)?x.map(s=>({...s,id:String(s.id||''),active:s.active!==false})):[]; } catch { return []; } };
const keyOf = (name,unit) => `${String(name||'').trim().toLowerCase()}|${String(unit||'').trim().toLowerCase()}`;
const activeOrder = o => !['Отменён','Отдан заказчику'].includes(o?.status || 'Новый');
function categoryFor(name){
const s=String(name||'').toLowerCase();
if (/(лосос|форел|тунец|кревет|икр|селед|рыб|морепродукт)/.test(s)) return 'Рыба и морепродукты';
if (/(говядин|свин|куриц|цыпл|индей|ветчин|салям|мортадел|прошут|коппа|пастрами|мяс|печен|паштет)/.test(s)) return 'Мясо и птица';
if (/(сыр|моцарел|пармез|чеддер|бри|камамбер|дорблю|крем.?чиз|сливоч|масл|фета)/.test(s)) return 'Молочные продукты и сыры';
if (/(томат|черри|огур|салат|ромэн|фрилис|баклаж|перец|картоф|капуст|груш|виноград|клубник|голубик|малин|апельс|лимон|фрукт|зелень|артишок|олив|маслин|гриб)/.test(s)) return 'Овощи, фрукты и зелень';
if (/(хлеб|крост|брускет|тарталет|эклер|профитрол|круассан|булоч|пирож|блин|тесто|бисквит|мафин|вафл)/.test(s)) return 'Хлеб и выпечка';
if (/(соус|песто|чили|горчиц|кетчуп|барбекю|трюфел|мед|карамел|орех|фисташ|миндал|пекан|юдзу)/.test(s)) return 'Соусы, специи и бакалея';
return 'Прочее';
}
const style=document.createElement('style');
style.id='sun-shopping-style';
style.textContent=`
.shop-head{display:flex;justify-content:space-between;align-items:flex-start;gap:12px;flex-wrap:wrap}.shop-controls{display:flex;gap:9px;align-items:end;flex-wrap:wrap;margin:14px 0}.shop-controls label{min-width:155px}.shop-controls .grow{flex:1;min-width:210px}.shop-summary{display:grid;grid-template-columns:repeat(auto-fit,minmax(125px,1fr));gap:10px;margin:14px 0}.shop-summary>div{background:#fafafa;border:1px solid var(--l);border-radius:8px;padding:12px}.shop-summary small{display:block;color:#68717a;margin-bottom:5px}.shop-summary b{font-size:20px;color:var(--n)}
.shop-section{border:1px solid var(--l);border-radius:9px;overflow:hidden;margin:13px 0;background:#fff}.shop-section h3{margin:0;padding:11px 13px;background:#f7f8f8;color:var(--n);display:flex;justify-content:space-between;align-items:center;gap:12px}.shop-title-wrap{display:flex;flex-direction:column;gap:3px}.shop-title-wrap small{font-weight:400;color:#68717a}.shop-group-total{text-align:right;white-space:nowrap}.shop-group-total small{display:block;color:#68717a;font-weight:400}.shop-supplier-link{border:0;background:transparent;padding:0;color:var(--n);text-decoration:underline;font:inherit;font-size:12px}
.shop-table-wrap{overflow:auto}.shop-table{width:100%;border-collapse:collapse;min-width:760px}.shop-table th,.shop-table td{padding:9px 8px;border-bottom:1px solid #e7eaeb;text-align:left}.shop-table th{font-size:12px;color:#68717a;background:#fff}.shop-table .num{text-align:right;white-space:nowrap}.shop-table tr.done{opacity:.48;text-decoration:line-through}.shop-buy{font-weight:700;color:#a33}.shop-ok{color:#287a49;font-weight:700}.shop-check{width:18px;height:18px}.shop-price-btn{border:0;background:transparent;color:var(--n);text-decoration:underline;padding:0;cursor:pointer}.shop-orders{font-size:11px;color:#68717a;max-width:330px}.shop-empty{padding:22px;color:#879198;text-align:center}.shop-note{margin:8px 0;color:#68717a;font-size:12px}.shop-filter{display:flex;align-items:center;gap:7px;color:#203040;font-size:13px}.shop-copy-status{font-size:12px;color:#287a49;min-height:18px}
@media(max-width:900px){.shop-summary{grid-template-columns:1fr 1fr}.shop-controls{align-items:stretch}.shop-controls label,.shop-controls .grow{min-width:0;width:100%}.shop-head .actions{width:100%}.shop-head .actions button{flex:1}.shop-section h3{align-items:flex-start}.shop-group-total{font-size:12px}}
@media print{body *{visibility:hidden!important}#shopping,#shopping *{visibility:visible!important}#shopping{position:absolute;inset:0;margin:0!important;padding:8mm!important;box-shadow:none!important}.shop-head .actions,.shop-controls,.shop-check,.shop-copy-status,header,.shop-supplier-link{display:none!important}.shop-section{break-inside:avoid}.shop-table{min-width:0;font-size:11px}.shop-summary{grid-template-columns:repeat(7,1fr)}}`;
document.head.appendChild(style);
const btn=document.createElement('button');
btn.type='button'; btn.textContent='Закупки'; nav.appendChild(btn);
const view=document.createElement('section'); view.id='shopping'; view.className='view page';
view.innerHTML=`<div class="shop-head"><div><h1>Закупочный лист</h1><p class="hint" style="margin:5px 0 0">Считает закупку по заказам, вычитает остатки и распределяет продукты по поставщикам.</p></div><div class="actions"><button class="outline" id="shopCopy" type="button">Копировать список</button><button class="outline" id="shopPrint" type="button">Печать / PDF</button></div></div>
<div class="shop-controls"><label>С даты<input type="date" id="shopFrom"></label><label>По дату<input type="date" id="shopTo"></label><label>Группировка<select id="shopGroup"><option value="supplier">По поставщикам</option><option value="category">По категориям</option></select></label><label class="grow">Поиск<input type="search" id="shopSearch" placeholder="Продукт, категория или поставщик"></label><label class="shop-filter"><input type="checkbox" id="shopOnlyDeficit" checked> Только то, что нужно купить</label><button class="outline" id="shopToday" type="button">Сегодня</button><button class="outline" id="shopWeek" type="button">7 дней</button></div>
<div id="shopCopyStatus" class="shop-copy-status"></div><div id="shopContent"></div>`;
document.body.appendChild(view);
const el=id=>document.getElementById(id);
const iso=d=>d.toISOString().slice(0,10);
const today=()=>iso(new Date());
const plusDays=(dateStr,n)=>{const d=new Date(`${dateStr}T12:00:00`);d.setDate(d.getDate()+n);return iso(d)};
el('shopFrom').value=today(); el('shopTo').value=plusDays(today(),6);
function ordersInRange(){
const from=el('shopFrom').value, to=el('shopTo').value;
return orders.filter(o=>activeOrder(o) && o.date && (!from || o.date>=from) && (!to || o.date<=to)).sort((a,b)=>`${a.date}T${a.time||''}`.localeCompare(`${b.date}T${b.time||''}`));
}
function aggregate(orderList){
const map=new Map();
orderList.forEach(o=>(o.lines||[]).forEach(l=>{
const item=itemById(l.id); if(!item || !Array.isArray(item.ingredients)) return;
item.ingredients.forEach(x=>{
if(!Array.isArray(x) || !x[0]) return;
const name=String(x[0]).trim(), unit=String(x[2]||'шт.').trim()||'шт.', qty=Number(x[1]||0)*Number(l.qty||0), key=keyOf(name,unit);
const p=map.get(key)||{name,unit,need:0,orders:new Set()}; p.need+=qty; p.orders.add(`#${o.id}`); map.set(key,p);
});
}));
const stock=getStock(), stockMap=new Map(stock.map(p=>[keyOf(p.name,p.unit),p]));
const supplierMap=new Map(getSuppliers().map(s=>[String(s.id),s]));
return [...map.values()].map(p=>{
const sp=stockMap.get(keyOf(p.name,p.unit)); const have=Number(sp?.qty ?? sp?.balance ?? 0), cost=Number(sp?.cost||0), buy=Math.max(0,p.need-have);
const rawSupplierId=String(sp?.supplierId||''), supplier=supplierMap.get(rawSupplierId)||null, supplierId=supplier?rawSupplierId:'';
return {...p,have,cost,buy,sum:buy*cost,stockId:sp?.id||'',category:categoryFor(p.name),supplierId,supplier,supplierName:supplier?.name||'Без поставщика',lastPurchaseDate:sp?.lastPurchaseDate||'',lastPurchasePrice:Number(sp?.lastPurchasePrice||0)};
});
}
function doneKey(row){return `sunShopDone|${el('shopFrom').value}|${el('shopTo').value}|${keyOf(row.name,row.unit)}`;}
function isDone(row){return localStorage.getItem(doneKey(row))==='1';}
function setDone(row,on){if(on)localStorage.setItem(doneKey(row),'1');else localStorage.removeItem(doneKey(row));}
function currentRows(){
const q=(el('shopSearch').value||'').trim().toLowerCase(), only=el('shopOnlyDeficit').checked;
return aggregate(ordersInRange()).filter(r=>(!only||r.buy>0) && (!q||`${r.name} ${r.category} ${r.supplierName} ${r.supplier?.phone||''}`.toLowerCase().includes(q))).sort((a,b)=>a.supplierName.localeCompare(b.supplierName,'ru')||a.category.localeCompare(b.category,'ru')||a.name.localeCompare(b.name,'ru'));
}
function groupRows(rows, mode){
const groups=new Map();
rows.forEach(row=>{
const key=mode==='supplier'?(row.supplierId||'__unassigned'):row.category;
if(!groups.has(key)) groups.set(key,{key,rows:[],supplier:mode==='supplier'?row.supplier:null,title:mode==='supplier'?row.supplierName:row.category});
groups.get(key).rows.push(row);
});
return [...groups.values()].sort((a,b)=>{
if(a.key==='__unassigned') return 1;
if(b.key==='__unassigned') return -1;
return String(a.title||'').localeCompare(String(b.title||''),'ru');
});
}
function groupHeader(group, mode){
const total=group.rows.reduce((s,r)=>s+r.sum,0);
if(mode==='category') return `<h3><span class="shop-title-wrap"><b>${esc(group.title)}</b></span><span class="shop-group-total">${group.rows.length} поз.<small>${fmt(total)}</small></span></h3>`;
const s=group.supplier;
const meta=s?[s.contact||'',s.phone||'',s.deliveryDays?`доставка: ${s.deliveryDays}`:'',Number(s.minOrder||0)>0?`мин. заказ ${fmt(s.minOrder)}`:''].filter(Boolean).join(' · '):'Назначьте поставщика в карточке продукта на складе';
return `<h3><span class="shop-title-wrap"><b>${esc(group.title)}</b><small>${esc(meta)}</small>${s?`<button class="shop-supplier-link" type="button" onclick="sunEditSupplier('${esc(s.id)}')">Открыть карточку поставщика</button>`:''}</span><span class="shop-group-total">${group.rows.length} поз.<small>${fmt(total)}</small></span></h3>`;
}
function renderShopping(){
const orderList=ordersInRange(), all=aggregate(orderList), rows=currentRows(), mode=el('shopGroup').value||'supplier';
const deficitAll=all.filter(r=>r.buy>0), estimate=deficitAll.reduce((s,r)=>s+r.sum,0), noPrice=deficitAll.filter(r=>r.cost<=0).length;
const checked=rows.filter(isDone).length, supplierCount=new Set(deficitAll.filter(r=>r.supplierId).map(r=>r.supplierId)).size, unassigned=deficitAll.filter(r=>!r.supplierId).length;
const sections=groupRows(rows,mode).map(group=>`<section class="shop-section">${groupHeader(group,mode)}<div class="shop-table-wrap"><table class="shop-table"><thead><tr><th></th><th>Продукт</th><th class="num">Нужно</th><th class="num">Склад</th><th class="num">Купить</th><th class="num">Цена / ед.</th><th class="num">Сумма</th></tr></thead><tbody>${group.rows.map(r=>`<tr data-shop-key="${esc(keyOf(r.name,r.unit))}" class="${isDone(r)?'done':''}"><td><input class="shop-check" type="checkbox" ${isDone(r)?'checked':''} data-shop-done="${esc(keyOf(r.name,r.unit))}"></td><td><b>${esc(r.name)}</b><div class="shop-orders">Поставщик: ${esc(r.supplierName)}${r.lastPurchaseDate?` · последняя закупка ${esc(r.lastPurchaseDate)}`:''}<br>Заказы: ${esc([...r.orders].join(', '))}</div></td><td class="num">${qtyFmt(r.need)} ${esc(r.unit)}</td><td class="num">${qtyFmt(r.have)} ${esc(r.unit)}</td><td class="num ${r.buy>0?'shop-buy':'shop-ok'}">${r.buy>0?`${qtyFmt(r.buy)} ${esc(r.unit)}`:'✓'}</td><td class="num">${r.stockId?`<button class="shop-price-btn" type="button" onclick="shoppingEditStock('${esc(r.stockId)}')">${fmt(r.cost)}</button>`:fmt(r.cost)}</td><td class="num"><b>${r.cost>0?fmt(r.sum):'—'}</b></td></tr>`).join('')}</tbody></table></div></section>`).join('');
el('shopContent').innerHTML=`<div class="shop-summary"><div><small>Заказов в периоде</small><b>${orderList.length}</b></div><div><small>Нужно купить</small><b>${deficitAll.length} поз.</b></div><div><small>Поставщиков</small><b>${supplierCount}</b></div><div><small>Без поставщика</small><b>${unassigned}</b></div><div><small>Отмечено купленным</small><b>${checked}</b></div><div><small>Без закупочной цены</small><b>${noPrice}</b></div><div><small>Ориентир закупки</small><b>${fmt(estimate)}</b></div></div>${orderList.length?sections||'<div class="shop-empty">По выбранному фильтру ничего не найдено.</div>':'<div class="shop-empty">На выбранный период активных заказов нет.</div>'}<p class="shop-note">Поставщик и закупочная цена берутся из карточки продукта на складе. При проведении прихода дата и цена последней закупки обновляются автоматически.</p>`;
view.querySelectorAll('[data-shop-done]').forEach(input=>input.addEventListener('change',()=>{
const row=rows.find(r=>keyOf(r.name,r.unit)===input.dataset.shopDone); if(row)setDone(row,input.checked); renderShopping();
}));
}
window.shoppingEditStock=id=>{ if(typeof window.editStockProduct==='function') window.editStockProduct(id); else alert('Откройте раздел «Склад» и укажите закупочную цену продукта.'); };
function makeText(){
const orderList=ordersInRange(), rows=aggregate(orderList).filter(r=>r.buy>0), mode=el('shopGroup').value||'supplier';
const lines=[`${window.CateriumBranding.nameHTML().toUpperCase()} — ЗАКУПОЧНЫЙ ЛИСТ`,`${el('shopFrom').value}${el('shopTo').value}`,`Заказов: ${orderList.length}`,''];
groupRows(rows,mode).forEach(group=>{
lines.push(String(group.title||'').toUpperCase());
if(mode==='supplier' && group.supplier){
const s=group.supplier;
if(s.contact) lines.push(`Контакт: ${s.contact}`);
if(s.phone) lines.push(`Телефон: ${s.phone}`);
if(s.address) lines.push(`Адрес: ${s.address}`);
if(s.deliveryDays) lines.push(`Доставка: ${s.deliveryDays}`);
if(Number(s.minOrder||0)>0) lines.push(`Минимальный заказ: ${fmt(s.minOrder)}`);
}
group.rows.forEach(r=>lines.push(`${r.name}: ${qtyFmt(r.buy)} ${r.unit}${r.cost>0?` — ~${fmt(r.sum)}`:''}`));
lines.push(`Подытог: ${fmt(group.rows.reduce((s,r)=>s+r.sum,0))}`,'');
});
const estimate=rows.reduce((s,r)=>s+r.sum,0); lines.push(`Ориентировочно: ${fmt(estimate)}`); return lines.join('\n');
}
el('shopCopy').onclick=async()=>{try{await navigator.clipboard.writeText(makeText());el('shopCopyStatus').textContent='Список скопирован.';}catch{const t=document.createElement('textarea');t.value=makeText();document.body.appendChild(t);t.select();document.execCommand('copy');t.remove();el('shopCopyStatus').textContent='Список скопирован.';}setTimeout(()=>el('shopCopyStatus').textContent='',1800);};
el('shopPrint').onclick=()=>window.print();
el('shopToday').onclick=()=>{el('shopFrom').value=today();el('shopTo').value=today();renderShopping();};
el('shopWeek').onclick=()=>{el('shopFrom').value=today();el('shopTo').value=plusDays(today(),6);renderShopping();};
['shopFrom','shopTo','shopOnlyDeficit','shopGroup'].forEach(id=>el(id).addEventListener('change',renderShopping)); el('shopSearch').addEventListener('input',renderShopping);
btn.onclick=()=>{document.querySelectorAll('.view').forEach(v=>v.classList.remove('on'));view.classList.add('on');[...nav.querySelectorAll('button')].forEach(b=>b.classList.remove('on'));btn.classList.add('on');renderShopping();};
if(typeof window.saveOrder==='function'){const p=window.saveOrder;window.saveOrder=()=>{p();if(view.classList.contains('on'))renderShopping();};}
if(typeof window.renderOrders==='function'){const p=window.renderOrders;window.renderOrders=()=>{p();if(view.classList.contains('on'))renderShopping();};}
window.addEventListener('sun:suppliers-changed',()=>{if(view.classList.contains('on'))renderShopping();});
window.addEventListener('sun:stock-changed',()=>{if(view.classList.contains('on'))renderShopping();});
window.sunRenderShopping=renderShopping;
renderShopping();
})();
/* ===== SUPPLIERS MODULE v1 ===== */
(() => {
if (document.getElementById('sun-suppliers-style')) return;
const nav=document.querySelector('nav');
if(!nav) return;
const KEY='sunSuppliers', STOCK_KEY='sunStock', MOVES_KEY='sunStockMoves';
const esc=window.SunSafe.escapeHTML;
const fmt=v=>typeof money==='function'?money(v):`${Math.round(Number(v||0)).toLocaleString('ru-RU')}`;
const dateLabel=v=>v?new Date(`${v}T12:00:00`).toLocaleDateString('ru-RU'):'—';
const readArray=(key)=>{try{const x=JSON.parse(localStorage.getItem(key)||'[]');return Array.isArray(x)?x:[];}catch{return[];}};
let suppliers=readArray(KEY).map(s=>({id:String(s.id||sunUUID()),name:String(s.name||'Без названия'),category:String(s.category||''),contact:String(s.contact||''),phone:String(s.phone||''),email:String(s.email||''),address:String(s.address||''),deliveryDays:String(s.deliveryDays||''),minOrder:Number(s.minOrder||0),notes:String(s.notes||''),active:s.active!==false,createdAt:s.createdAt||new Date().toISOString()}));
let editingId=null;
function saveSuppliers(){
localStorage.setItem(KEY,JSON.stringify(suppliers));
try{window.dispatchEvent(new CustomEvent('sun:suppliers-changed'));}catch(_){ }
}
saveSuppliers();
const getStock=()=>readArray(STOCK_KEY);
const getMoves=()=>readArray(MOVES_KEY);
const supplierById=id=>suppliers.find(s=>String(s.id)===String(id||''))||null;
const productsFor=id=>getStock().filter(p=>String(p.supplierId||'')===String(id||''));
const purchaseStats=id=>{
const moves=getMoves().filter(m=>m.type==='in'&&String(m.supplierId||'')===String(id||''));
return {count:moves.length,total:moves.reduce((s,m)=>s+Math.abs(Number(m.qty||0))*Number(m.cost||0),0),last:moves.map(m=>m.date||'').sort().pop()||''};
};
const style=document.createElement('style');
style.id='sun-suppliers-style';
style.textContent=`
.sup-head{display:flex;justify-content:space-between;gap:12px;align-items:flex-start;flex-wrap:wrap}.sup-summary{display:grid;grid-template-columns:repeat(5,minmax(130px,1fr));gap:10px;margin:14px 0}.sup-summary>div{background:#fafafa;border:1px solid var(--l);border-radius:8px;padding:12px}.sup-summary small{display:block;color:#68717a;margin-bottom:5px}.sup-summary b{font-size:20px;color:var(--n)}
.sup-toolbar{display:flex;gap:9px;align-items:end;flex-wrap:wrap;margin:14px 0}.sup-toolbar label{min-width:180px}.sup-toolbar .grow{flex:1;min-width:240px}.sup-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:12px}.sup-card{border:1px solid var(--l);background:#fff;border-radius:10px;padding:14px;display:flex;flex-direction:column;gap:10px}.sup-card.inactive{opacity:.64}.sup-card-head{display:flex;justify-content:space-between;gap:10px}.sup-card h3{margin:0;color:var(--n);font-size:18px}.sup-badge{font-size:11px;border-radius:999px;padding:4px 8px;background:#e8f4eb;color:#287a49;white-space:nowrap}.sup-badge.off{background:#f1eeee;color:#8a5555}.sup-meta{display:grid;gap:5px;color:#50606b;font-size:13px}.sup-meta a{color:var(--n)}.sup-stats{display:grid;grid-template-columns:repeat(3,1fr);gap:7px}.sup-stats>div{background:#f7f8f8;border-radius:7px;padding:8px}.sup-stats small{display:block;color:#68717a}.sup-stats b{display:block;margin-top:3px}.sup-actions{display:flex;gap:7px;margin-top:auto}.sup-actions button{flex:1}.sup-notes{font-size:12px;color:#68717a;border-left:3px solid var(--g);padding-left:8px}.sup-empty{padding:25px;text-align:center;color:#879198}
.sup-form{display:grid;grid-template-columns:1fr 1fr;gap:10px}.sup-form .wide{grid-column:1/-1}.sup-active{display:flex;flex-direction:row;align-items:center;gap:8px;color:#203040}.sup-active input{width:18px;height:18px}.sup-products-table{width:100%;border-collapse:collapse;min-width:650px}.sup-products-table th,.sup-products-table td{padding:9px 8px;border-bottom:1px solid var(--l);text-align:left}.sup-products-table th{font-size:12px;color:#68717a}.sup-products-table .num{text-align:right;white-space:nowrap}
@media(max-width:900px){.sup-summary{grid-template-columns:1fr 1fr}.sup-toolbar label,.sup-toolbar .grow{min-width:0;width:100%}.sup-form{grid-template-columns:1fr}.sup-form .wide{grid-column:auto}.sup-grid{grid-template-columns:1fr}}
`;
document.head.appendChild(style);
const btn=document.createElement('button');btn.type='button';btn.textContent='Поставщики';nav.appendChild(btn);
const view=document.createElement('section');view.id='suppliers';view.className='view page';
view.innerHTML=`<div class="sup-head"><div><h1>Поставщики</h1><p class="hint" style="margin:5px 0 0">Контакты, условия, связанные продукты и история закупок.</p></div><button class="primary" id="supAdd" type="button">Добавить поставщика</button></div><div class="sup-summary" id="supSummary"></div><div class="sup-toolbar"><label class="grow">Поиск<input id="supSearch" type="search" placeholder="Название, контакт, телефон, категория"></label><label>Статус<select id="supFilter"><option value="all">Все</option><option value="active">Активные</option><option value="inactive">Неактивные</option></select></label></div><div id="supList"></div>`;
document.body.appendChild(view);
const modalEl=document.createElement('div');modalEl.id='supplier-modal';modalEl.className='modal';
modalEl.innerHTML=`<div class="dialog"><div class="dialog-head"><h2 id="supplierModalTitle">Поставщик</h2><button type="button" onclick="closeModal('supplier-modal')">×</button></div><div class="sup-form"><label>Название<input id="supplierName"></label><label>Категория / тип<input id="supplierCategory" list="supplierCategoryList" placeholder="Овощи, Metro, рыба…"><datalist id="supplierCategoryList"><option value="Оптовый магазин"><option value="Овощи и фрукты"><option value="Мясо и птица"><option value="Рыба и морепродукты"><option value="Молочные продукты"><option value="Выпечка"><option value="Посуда и упаковка"></datalist></label><label>Контактное лицо<input id="supplierContact"></label><label>Телефон<input id="supplierPhone" type="tel"></label><label>Email<input id="supplierEmail" type="email"></label><label>Минимальный заказ, ₽<input id="supplierMinOrder" type="number" min="0" step="1"></label><label class="wide">Адрес / точка выдачи<input id="supplierAddress"></label><label class="wide">Дни и условия доставки<input id="supplierDeliveryDays" placeholder="Пн–Сб, заказ до 15:00, доставка на следующий день"></label><label class="wide">Комментарий<textarea id="supplierNotes" rows="3"></textarea></label><label class="sup-active wide"><input id="supplierActive" type="checkbox" checked> Поставщик активен</label></div><div class="actions"><button class="primary" id="supplierSave" type="button">Сохранить</button><button class="danger" id="supplierDelete" type="button">Удалить</button></div></div>`;
document.body.appendChild(modalEl);
const productsModal=document.createElement('div');productsModal.id='supplier-products-modal';productsModal.className='modal';
productsModal.innerHTML=`<div class="dialog"><div class="dialog-head"><h2 id="supplierProductsTitle">Продукты поставщика</h2><button type="button" onclick="closeModal('supplier-products-modal')">×</button></div><div id="supplierProductsContent"></div><div class="actions"><button class="outline" type="button" onclick="closeModal('supplier-products-modal')">Закрыть</button></div></div>`;
document.body.appendChild(productsModal);
const el=id=>document.getElementById(id);
function render(){
const stock=getStock(), moves=getMoves(), linked=stock.filter(p=>p.supplierId).length, unassigned=stock.filter(p=>!p.supplierId).length, active=suppliers.filter(s=>s.active!==false).length;
const purchaseTotal=moves.filter(m=>m.type==='in'&&m.supplierId).reduce((s,m)=>s+Math.abs(Number(m.qty||0))*Number(m.cost||0),0);
el('supSummary').innerHTML=`<div><small>Всего поставщиков</small><b>${suppliers.length}</b></div><div><small>Активных</small><b>${active}</b></div><div><small>Продуктов привязано</small><b>${linked}</b></div><div><small>Без поставщика</small><b>${unassigned}</b></div><div><small>Закупок по истории</small><b>${fmt(purchaseTotal)}</b></div>`;
const q=(el('supSearch').value||'').trim().toLowerCase(), filter=el('supFilter').value;
const list=suppliers.filter(s=>(filter==='all'||(filter==='active'&&s.active!==false)||(filter==='inactive'&&s.active===false))&&(!q||`${s.name} ${s.category} ${s.contact} ${s.phone} ${s.email} ${s.address}`.toLowerCase().includes(q))).sort((a,b)=>(a.active===b.active?String(a.name).localeCompare(String(b.name),'ru'):(a.active===false?1:-1)));
const cards=list.map(s=>{
const products=stock.filter(p=>String(p.supplierId||'')===String(s.id)), stats=purchaseStats(s.id), phoneHref=String(s.phone||'').replace(/[^\d+]/g,'');
return `<article class="sup-card ${s.active===false?'inactive':''}"><div class="sup-card-head"><div><h3>${esc(s.name)}</h3><small class="hint">${esc(s.category||'Категория не указана')}</small></div><span class="sup-badge ${s.active===false?'off':''}">${s.active===false?'Неактивен':'Активен'}</span></div><div class="sup-meta">${s.contact?`<span>Контакт: <b>${esc(s.contact)}</b></span>`:''}${s.phone?`<span>Телефон: <a href="tel:${esc(phoneHref)}">${esc(s.phone)}</a></span>`:''}${s.email?`<span>Email: <a href="mailto:${esc(s.email)}">${esc(s.email)}</a></span>`:''}${s.address?`<span>Адрес: ${esc(s.address)}</span>`:''}${s.deliveryDays?`<span>Доставка: ${esc(s.deliveryDays)}</span>`:''}${Number(s.minOrder||0)>0?`<span>Минимальный заказ: <b>${fmt(s.minOrder)}</b></span>`:''}</div><div class="sup-stats"><div><small>Продуктов</small><b>${products.length}</b></div><div><small>Закупок</small><b>${stats.count}</b></div><div><small>Последняя</small><b>${dateLabel(stats.last)}</b></div></div>${s.notes?`<div class="sup-notes">${esc(s.notes)}</div>`:''}<div class="sup-actions"><button class="outline" type="button" onclick="sunSupplierProducts('${esc(s.id)}')">Продукты</button><button class="primary" type="button" onclick="sunEditSupplier('${esc(s.id)}')">Изменить</button></div></article>`;
}).join('');
const unassignedCard=unassigned?`<article class="sup-card"><div class="sup-card-head"><div><h3>Без поставщика</h3><small class="hint">Требуется распределение</small></div><span class="sup-badge off">${unassigned} поз.</span></div><p class="hint">Эти продукты попадут в отдельный раздел закупочного листа.</p><div class="sup-actions"><button class="outline" type="button" onclick="sunSupplierProducts('')">Показать продукты</button></div></article>`:'';
el('supList').innerHTML=list.length||unassigned?`<div class="sup-grid">${cards}${unassignedCard}</div>`:'<div class="sup-empty">Поставщиков пока нет. Добавьте первого поставщика и назначьте его продуктам на складе.</div>';
}
function openSupplier(id){
editingId=id||null;const s=editingId?supplierById(editingId):null;
el('supplierModalTitle').textContent=s?'Изменить поставщика':'Новый поставщик';
el('supplierName').value=s?.name||'';el('supplierCategory').value=s?.category||'';el('supplierContact').value=s?.contact||'';el('supplierPhone').value=s?.phone||'';el('supplierEmail').value=s?.email||'';el('supplierMinOrder').value=Number(s?.minOrder||0);el('supplierAddress').value=s?.address||'';el('supplierDeliveryDays').value=s?.deliveryDays||'';el('supplierNotes').value=s?.notes||'';el('supplierActive').checked=s?s.active!==false:true;el('supplierDelete').style.display=s?'inline-block':'none';modal('supplier-modal');
}
window.sunEditSupplier=openSupplier;
el('supAdd').onclick=()=>openSupplier(null);
el('supplierSave').onclick=()=>{
const name=el('supplierName').value.trim();if(!name)return alert('Введите название поставщика.');
const data={name,category:el('supplierCategory').value.trim(),contact:el('supplierContact').value.trim(),phone:el('supplierPhone').value.trim(),email:el('supplierEmail').value.trim(),minOrder:Math.max(0,Number(el('supplierMinOrder').value||0)),address:el('supplierAddress').value.trim(),deliveryDays:el('supplierDeliveryDays').value.trim(),notes:el('supplierNotes').value.trim(),active:el('supplierActive').checked};
if(editingId){const s=supplierById(editingId);Object.assign(s,data);}else{suppliers.push({id:sunUUID(),createdAt:new Date().toISOString(),...data});}
saveSuppliers();closeModal('supplier-modal');render();
};
el('supplierDelete').onclick=()=>{
const s=supplierById(editingId);if(!s)return;const linked=productsFor(s.id).length;
if(!confirm(`Удалить поставщика «${s.name}»?${linked?` У ${linked} продуктов поставщик будет сброшен.`:''}`))return;
if(typeof window.sunUnlinkSupplier==='function') window.sunUnlinkSupplier(s.id);
else { const stock=getStock().map(p=>String(p.supplierId||'')===String(s.id)?{...p,supplierId:''}:p);localStorage.setItem(STOCK_KEY,JSON.stringify(stock));try{window.dispatchEvent(new CustomEvent('sun:stock-changed'));}catch(_){ } }
suppliers=suppliers.filter(x=>x.id!==s.id);saveSuppliers();closeModal('supplier-modal');render();
};
window.sunSupplierProducts=id=>{
const s=id?supplierById(id):null, list=productsFor(id).sort((a,b)=>String(a.name||'').localeCompare(String(b.name||''),'ru'));
el('supplierProductsTitle').textContent=s?`Продукты · ${s.name}`:'Продукты без поставщика';
el('supplierProductsContent').innerHTML=list.length?`<div style="overflow:auto"><table class="sup-products-table"><thead><tr><th>Продукт</th><th>Ед.</th><th class="num">Цена</th><th>Последняя закупка</th><th></th></tr></thead><tbody>${list.map(p=>`<tr><td><b>${esc(p.name||'Без названия')}</b></td><td>${esc(p.unit||'')}</td><td class="num">${fmt(p.cost||0)}</td><td>${esc(p.lastPurchaseDate||'—')}${Number(p.lastPurchasePrice||0)>0?` · ${fmt(p.lastPurchasePrice)}`:''}</td><td><button class="outline" type="button" onclick="sunSupplierEditStock('${esc(p.id)}')">Изменить</button></td></tr>`).join('')}</tbody></table></div>`:'<div class="sup-empty">Продуктов нет.</div>';
modal('supplier-products-modal');
};
window.sunSupplierEditStock=id=>{closeModal('supplier-products-modal');if(typeof window.editStockProduct==='function')window.editStockProduct(id);};
el('supSearch').oninput=render;el('supFilter').onchange=render;
btn.onclick=()=>{document.querySelectorAll('.view').forEach(v=>v.classList.remove('on'));view.classList.add('on');[...nav.querySelectorAll('button')].forEach(b=>b.classList.remove('on'));btn.classList.add('on');render();};
window.addEventListener('sun:stock-changed',()=>{if(view.classList.contains('on'))render();});
window.sunRenderSuppliers=render;
render();
})();
// Polished application navigation: clearer groups, equal spacing and better responsive behavior.
(() => {
const nav = document.querySelector('header nav');
const header = document.querySelector('header');
if (!nav || !header || document.getElementById('sun-nav-polish-style')) return;
const preferredOrder = [
'Главная','Новый заказ','Заказы','Календарь',
'Производство','Закупки','Склад',
'Клиенты','Деньги','Команда','Поставщики','Статистика'
];
const groupMap = {
work: ['Новый заказ','Заказы','Календарь'],
operations: ['Производство','Закупки','Склад'],
management: ['Клиенты','Деньги','Команда','Поставщики','Статистика']
};
const classMap = {
'Главная':'nav-home','Новый заказ':'nav-new','Заказы':'nav-orders','Календарь':'nav-calendar',
'Производство':'nav-production','Закупки':'nav-shopping','Склад':'nav-stock',
'Клиенты':'nav-clients','Деньги':'nav-money','Команда':'nav-team','Поставщики':'nav-suppliers','Статистика':'nav-stats'
};
const allButtons = [...nav.querySelectorAll('button')];
const byName = new Map(allButtons.map(button => [button.textContent.trim(), button]));
// Keep any future/unknown buttons too, after the known navigation items.
preferredOrder.forEach(name => {
const button = byName.get(name);
if (button) nav.appendChild(button);
});
allButtons.filter(button => !preferredOrder.includes(button.textContent.trim())).forEach(button => nav.appendChild(button));
const buildGroup = (key, label) => {
const group = document.createElement('div');
group.className = `sun-nav-group sun-nav-${key}`;
group.dataset.label = label;
const items = groupMap[key] || [];
items.forEach(name => {
const button = byName.get(name);
if (!button) return;
button.classList.add('sun-nav-button', classMap[name] || '');
button.dataset.navLabel = name;
group.appendChild(button);
});
return group;
};
const groups = [
buildGroup('work','Заказы'),
buildGroup('operations','Операции'),
buildGroup('management','Управление')
];
nav.innerHTML = '';
const homeButton = byName.get('Главная');
if (homeButton) {
homeButton.classList.add('sun-nav-button','nav-home','sun-nav-home-standalone');
homeButton.dataset.navLabel = 'Главная';
nav.appendChild(homeButton);
}
groups.forEach(group => nav.appendChild(group));
// Any future buttons that were not in our map go into Management.
allButtons.filter(button => !preferredOrder.includes(button.textContent.trim())).forEach(button => {
button.classList.add('sun-nav-button');
groups[2].appendChild(button);
});
// Safe registration point for modules that add navigation after the sidebar has been grouped.
// Never insert a button relative to a node owned by another parent: register it in the target group instead.
const groupByKey = {work:groups[0], operations:groups[1], management:groups[2]};
window.sunRegisterNavButton = function(options={}){
const button=options.button;
if(!button) return null;
const targetGroup=groupByKey[options.group] || groupByKey.management || nav;
button.classList.add('sun-nav-button');
if(options.className) String(options.className).split(/\s+/).filter(Boolean).forEach(c=>button.classList.add(c));
if(options.navLabel) button.dataset.navLabel=options.navLabel;
const beforeLabel=String(options.beforeLabel||'').trim();
const before=beforeLabel?[...targetGroup.children].find(el=>el.tagName==='BUTTON'&&String(el.dataset.navLabel||el.textContent||'').trim()===beforeLabel):null;
if(before) before.before(button); else targetGroup.appendChild(button);
return button;
};
const style = document.createElement('style');
style.id = 'sun-nav-polish-style';
style.textContent = `
header{
min-height:92px;height:auto;padding:12px 20px;gap:22px;align-items:center;
box-shadow:0 5px 18px #102c3d1c;position:relative;z-index:90
}
header .brand{display:flex;align-items:center;flex:0 0 auto;white-space:nowrap;letter-spacing:.1px}
header nav{
flex:1;min-width:0;display:grid;grid-template-columns:minmax(360px,1.35fr) minmax(270px,.9fr) minmax(430px,1.5fr);
gap:8px;align-items:stretch;overflow:visible;white-space:normal
}
.sun-nav-group{
position:relative;display:grid;grid-auto-flow:column;grid-auto-columns:1fr;gap:4px;
padding:19px 5px 5px;border-radius:12px;background:#ffffff0b;border:1px solid #ffffff13;min-width:0
}
.sun-nav-group::before{
content:attr(data-label);position:absolute;left:10px;top:4px;color:#9fb4c2;font-size:9px;font-weight:700;
letter-spacing:.09em;text-transform:uppercase;line-height:1
}
header nav .sun-nav-button{
min-width:0;height:43px;padding:0 9px;border:0;border-radius:8px;background:transparent;color:#dce8ec;
border-bottom:0;font-size:12.5px;line-height:1.1;font-weight:700;position:relative;transition:.18s ease;
display:flex;align-items:center;justify-content:center;text-align:center;white-space:nowrap
}
header nav .sun-nav-button::before{
content:'';width:6px;height:6px;border-radius:50%;margin-right:7px;background:#7892a3;flex:0 0 auto;opacity:.9
}
header nav .sun-nav-button:hover{background:#ffffff12;color:#fff;transform:translateY(-1px)}
header nav .sun-nav-button.on{
color:#15364c;background:#fff;border:0;box-shadow:0 4px 13px #071b2730;transform:translateY(-1px)
}
header nav .sun-nav-button.on::after{
content:'';position:absolute;left:12px;right:12px;bottom:3px;height:2px;border-radius:2px;background:var(--g)
}
header nav .nav-home::before{background:#d9ad4b}
header nav .nav-new::before{background:#6fb48b}
header nav .nav-orders::before{background:#7da4c4}
header nav .nav-calendar::before{background:#a28bc3}
header nav .nav-production::before{background:#dc8e63}
header nav .nav-shopping::before{background:#72aa97}
header nav .nav-stock::before{background:#9aab68}
header nav .nav-clients::before{background:#6f9fc1}
header nav .nav-money::before{background:#d0a343}
header nav .nav-team::before{background:#a486b6}
header nav .nav-suppliers::before{background:#c77c69}
header nav .nav-stats::before{background:#7c98a8}
@media(max-width:1320px) and (min-width:901px){
header{align-items:flex-start}
header nav{grid-template-columns:1fr 1fr;grid-template-areas:'work work' 'operations management';gap:7px}
.sun-nav-work{grid-area:work}.sun-nav-operations{grid-area:operations}.sun-nav-management{grid-area:management}
.sun-nav-group{padding-top:18px}
}
@media(max-width:1050px) and (min-width:901px){
header{padding-inline:14px;gap:14px}.brand{font-size:18px!important}
header nav .sun-nav-button{font-size:11.5px;padding-inline:7px}
header nav .sun-nav-button::before{margin-right:5px;width:5px;height:5px}
}
@media(max-width:900px){
header{min-height:64px;padding:9px 14px 10px;display:flex;box-shadow:0 2px 10px #102c3d20}
header .brand{font-size:20px}
header nav{
position:fixed;left:0;right:0;bottom:0;z-index:100;display:flex!important;align-items:stretch;gap:0;
overflow-x:auto;overflow-y:hidden;background:var(--n);height:70px;padding:0 max(6px,env(safe-area-inset-right)) env(safe-area-inset-bottom) max(6px,env(safe-area-inset-left));
box-shadow:0 -3px 16px #102c3d35;scrollbar-width:none;-webkit-overflow-scrolling:touch;white-space:nowrap
}
header nav::-webkit-scrollbar{display:none}
.sun-nav-group{display:contents}
.sun-nav-group::before{display:none}
header nav .sun-nav-button{
flex:1 0 92px;height:64px;border-radius:0;padding:7px 6px 5px;font-size:11px;line-height:1.12;
display:flex;flex-direction:column;gap:5px;border-top:3px solid transparent;background:transparent;transform:none!important
}
header nav .sun-nav-button::before{width:7px;height:7px;margin:0}
header nav .sun-nav-button.on{color:#fff;background:#ffffff0d;box-shadow:none;border-top-color:var(--g)}
header nav .sun-nav-button.on::after{display:none}
}
@media(max-width:460px){header nav .sun-nav-button{flex-basis:84px;font-size:10.5px}}
@media print{header nav{display:none!important}}
`;
document.head.appendChild(style);
})();
/* ===== MODAL BACKDROP + ESC CLOSE v1 ===== */
(() => {
if (document.getElementById('sun-modal-backdrop-style')) return;
const style = document.createElement('style');
style.id = 'sun-modal-backdrop-style';
style.textContent = `
.modal{z-index:1000}
.modal.on{cursor:pointer}
.modal.on>.dialog{cursor:default;box-shadow:0 18px 55px #102c3d4f}
`;
document.head.appendChild(style);
document.addEventListener('click', event => {
const target = event.target;
if (!(target instanceof HTMLElement)) return;
if (!target.classList.contains('modal') || !target.classList.contains('on') || !target.id) return;
if (typeof window.closeModal === 'function') window.closeModal(target.id);
});
document.addEventListener('keydown', event => {
if (event.key !== 'Escape') return;
const opened = [...document.querySelectorAll('.modal.on')].pop();
if (opened?.id && typeof window.closeModal === 'function') window.closeModal(opened.id);
});
})();
/* ===== VISUAL STATISTICS DASHBOARD v1 ===== */
(() => {
const statsView = document.getElementById('stats');
const statsList = document.getElementById('statsList');
if (!statsView || !statsList || document.getElementById('sun-stats-visual-style')) return;
const style = document.createElement('style');
style.id = 'sun-stats-visual-style';
style.textContent = `
.stats-head{display:flex;justify-content:space-between;align-items:flex-start;gap:14px;flex-wrap:wrap}
.stats-controls{display:flex;align-items:flex-end;gap:9px;flex-wrap:wrap}.stats-controls label{min-width:190px}
.stats-kpi{display:grid;grid-template-columns:repeat(6,minmax(130px,1fr));gap:10px;margin:16px 0}
.stats-kpi>div{border:1px solid var(--l);border-radius:10px;background:#fafafa;padding:12px;min-height:78px}
.stats-kpi small{display:block;color:#68717a;margin-bottom:7px}.stats-kpi b{display:block;color:var(--n);font-size:20px;line-height:1.1}
.stats-grid{display:grid;grid-template-columns:minmax(0,1.5fr) minmax(280px,.8fr);gap:12px;margin-top:12px}
.stats-panel{border:1px solid var(--l);border-radius:11px;background:#fff;padding:15px;min-width:0}
.stats-panel.wide{grid-column:1/-1}.stats-panel h2{font-size:18px;color:var(--n);margin:0}.stats-panel-head{display:flex;justify-content:space-between;gap:10px;align-items:center;margin-bottom:14px}.stats-panel-head small{color:#7b858c}
.stats-chart-scroll{overflow-x:auto;padding:8px 2px 2px}.stats-bars{height:250px;min-width:620px;display:flex;align-items:flex-end;gap:8px;padding:24px 8px 30px;border-left:1px solid #e0e4e6;border-bottom:1px solid #e0e4e6;background:repeating-linear-gradient(to top,#fff 0,#fff 49px,#edf0f1 50px)}
.stats-bar-item{height:100%;min-width:28px;flex:1;display:flex;flex-direction:column;justify-content:flex-end;align-items:center;position:relative}
.stats-bar-item .value{position:absolute;top:-20px;font-size:10px;color:#53636e;white-space:nowrap}.stats-bar-item i{display:block;width:min(34px,78%);min-height:3px;border-radius:7px 7px 2px 2px;background:linear-gradient(180deg,#d5ad52,#c38e25);box-shadow:0 4px 8px #c99a3222;transition:.18s}
.stats-bar-item:hover i{filter:brightness(1.08);transform:translateY(-2px)}.stats-bar-item label{position:absolute;bottom:-24px;font-size:10px;color:#68717a;white-space:nowrap;transform:rotate(-22deg);transform-origin:center}
.stats-donut-wrap{display:grid;grid-template-columns:160px 1fr;gap:16px;align-items:center}.stats-donut{width:156px;height:156px;border-radius:50%;display:grid;place-items:center;position:relative}.stats-donut::after{content:'';width:92px;height:92px;border-radius:50%;background:#fff;position:absolute;box-shadow:inset 0 0 0 1px #edf0f1}.stats-donut-center{position:relative;z-index:1;text-align:center}.stats-donut-center b{display:block;font-size:27px;color:var(--n)}.stats-donut-center small{color:#68717a}
.stats-legend{display:grid;gap:7px}.stats-legend-row{display:grid;grid-template-columns:10px 1fr auto;gap:8px;align-items:center;font-size:12px}.stats-legend-row i{width:9px;height:9px;border-radius:50%}.stats-legend-row b{color:var(--n)}
.stats-hbars{display:grid;gap:10px}.stats-hbar{display:grid;grid-template-columns:minmax(150px,1fr) minmax(140px,1.4fr) 92px;gap:10px;align-items:center}.stats-hbar>span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px}.stats-hbar-track{height:9px;border-radius:999px;background:#eef1f2;overflow:hidden}.stats-hbar-track i{display:block;height:100%;border-radius:inherit;background:linear-gradient(90deg,#5d90b0,#7baac5)}.stats-hbar strong{text-align:right;color:var(--n);font-size:12px;white-space:nowrap}
.stats-week{display:grid;grid-template-columns:repeat(7,1fr);gap:7px;align-items:end;min-height:160px}.stats-week-day{display:flex;flex-direction:column;align-items:center;justify-content:flex-end;height:160px}.stats-week-day i{display:block;width:70%;min-height:3px;border-radius:6px 6px 2px 2px;background:#7ba88d}.stats-week-day b{font-size:11px;color:var(--n);margin:5px 0}.stats-week-day small{font-size:10px;color:#68717a}
.stats-empty{padding:28px;text-align:center;color:#879198;border:1px dashed #d5dadd;border-radius:9px;background:#fafbfb}
@media(max-width:1180px){.stats-kpi{grid-template-columns:repeat(3,1fr)}.stats-grid{grid-template-columns:1fr}.stats-panel.wide{grid-column:auto}}
@media(max-width:720px){.stats-kpi{grid-template-columns:1fr 1fr}.stats-controls,.stats-controls label{width:100%;min-width:0}.stats-donut-wrap{grid-template-columns:1fr;justify-items:center}.stats-hbar{grid-template-columns:minmax(120px,1fr) 1fr}.stats-hbar strong{grid-column:1/-1;text-align:left}.stats-week{gap:3px}.stats-week-day small{font-size:9px}}
@media(max-width:430px){.stats-kpi{grid-template-columns:1fr}.stats-panel{padding:12px}}
@media print{#stats .stats-controls{display:none!important}#stats{margin:0!important}.stats-grid{display:block}.stats-panel{break-inside:avoid;margin-bottom:12px}.stats-chart-scroll{overflow:visible}.stats-bars{min-width:0}}
`;
document.head.appendChild(style);
statsView.innerHTML = `
<div class="stats-head">
<div><h1>Статистика</h1><p class="hint" style="margin:5px 0 0">Продажи, загрузка, статусы и популярные позиции в графическом виде.</p></div>
<div class="stats-controls"><label>Период<select id="statsPeriod"><option value="month">Текущий месяц</option><option value="90">Последние 90 дней</option><option value="year" selected>Последние 12 месяцев</option><option value="all">Всё время</option></select></label><button type="button" class="outline" id="statsPrint">Печать / PDF</button></div>
</div>
<div id="statsList"></div>`;
const el = id => document.getElementById(id);
const esc=window.SunSafe.escapeHTML;
const fmt = value => typeof window.money === 'function' ? window.money(value) : `${Math.round(Number(value || 0)).toLocaleString('ru-RU')}`;
const totalFor = order => Number(order?.total || 0) || (order?.lines || []).reduce((sum,line) => {
const item = boxes.find(x => String(x.id) === String(line.id));
return sum + Number(item?.price || 0) * Number(line.qty || 0);
},0);
const parseDate = raw => raw ? new Date(`${raw}T12:00:00`) : null;
const today = () => { const d = new Date(); d.setHours(12,0,0,0); return d; };
const startOfMonth = d => new Date(d.getFullYear(), d.getMonth(), 1, 12);
const addDays = (d,n) => { const x = new Date(d); x.setDate(x.getDate()+n); return x; };
const addMonths = (d,n) => new Date(d.getFullYear(), d.getMonth()+n, 1, 12);
const pad = n => String(n).padStart(2,'0');
const dateKey = d => `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}`;
const monthKey = d => `${d.getFullYear()}-${pad(d.getMonth()+1)}`;
const monthLabel = d => d.toLocaleDateString('ru-RU',{month:'short'}).replace('.','');
function boundsFor(period){
const now = today();
if (period === 'month') return {from:startOfMonth(now),to:addMonths(startOfMonth(now),1)};
if (period === '90') return {from:addDays(now,-89),to:addDays(now,1)};
if (period === 'year') return {from:addMonths(startOfMonth(now),-11),to:addMonths(startOfMonth(now),1)};
return {from:null,to:null};
}
function ordersForPeriod(period){
const {from,to} = boundsFor(period);
return orders.filter(order => {
if ((order.status || 'Новый') === 'Отменён') return false;
const d = parseDate(order.date);
if (!d) return period === 'all';
return (!from || d >= from) && (!to || d < to);
});
}
function trendBuckets(period, valid){
const now=today(), buckets=[];
if(period==='month'){
const first=startOfMonth(now), end=addMonths(first,1);
for(let d=new Date(first);d<end;d=addDays(d,1)) buckets.push({key:dateKey(d),label:String(d.getDate()),sum:0,count:0});
valid.forEach(o=>{const b=buckets.find(x=>x.key===o.date);if(b){b.sum+=totalFor(o);b.count++;}});
}else if(period==='90'){
const from=addDays(now,-89);
for(let i=0;i<13;i++){const start=addDays(from,i*7),end=addDays(start,7);buckets.push({key:dateKey(start),label:`${pad(start.getDate())}.${pad(start.getMonth()+1)}`,start,end,sum:0,count:0});}
valid.forEach(o=>{const d=parseDate(o.date);const b=buckets.find(x=>d&&d>=x.start&&d<x.end);if(b){b.sum+=totalFor(o);b.count++;}});
}else{
let first;
if(period==='all'){
const dates=valid.map(o=>parseDate(o.date)).filter(Boolean).sort((a,b)=>a-b);
first=dates.length?startOfMonth(dates[0]):addMonths(startOfMonth(now),-11);
const totalMonths=Math.max(1,(now.getFullYear()-first.getFullYear())*12+now.getMonth()-first.getMonth()+1);
if(totalMonths>24){
const years=[...new Set(valid.map(o=>parseDate(o.date)?.getFullYear()).filter(Boolean))].sort();
years.forEach(y=>buckets.push({key:String(y),label:String(y),sum:0,count:0}));
valid.forEach(o=>{const d=parseDate(o.date);const b=buckets.find(x=>x.key===String(d?.getFullYear()));if(b){b.sum+=totalFor(o);b.count++;}});
return buckets;
}
}else first=addMonths(startOfMonth(now),-11);
for(let d=new Date(first);d<=startOfMonth(now);d=addMonths(d,1)) buckets.push({key:monthKey(d),label:monthLabel(d),sum:0,count:0});
valid.forEach(o=>{const d=parseDate(o.date);const b=buckets.find(x=>x.key===monthKey(d));if(b){b.sum+=totalFor(o);b.count++;}});
}
return buckets;
}
function barChart(buckets){
const max=Math.max(1,...buckets.map(x=>x.sum));
return `<div class="stats-chart-scroll"><div class="stats-bars">${buckets.map(b=>{const height=Math.max(b.sum?3:0,Math.round(b.sum/max*100));return `<div class="stats-bar-item" title="${esc(b.label)}: ${esc(fmt(b.sum))}, заказов ${b.count}"><span class="value">${b.sum?esc(Math.round(b.sum/1000).toLocaleString('ru-RU')+' тыс.'):'0'}</span><i style="height:${height}%"></i><label>${esc(b.label)}</label></div>`;}).join('')}</div></div>`;
}
const statusColors={"Новый":"#6f9fc1","В работе":"#9a82bd","Собран":"#e0b242","Передан курьеру":"#df8c43","Отдан заказчику":"#66a77b","Отменён":"#c96b6b"};
function donut(valid){
const statuses=['Новый','В работе','Собран','Передан курьеру','Отдан заказчику'];
const counts=statuses.map(status=>({status,count:valid.filter(o=>(o.status||'Новый')===status).length,color:statusColors[status]}));
const total=Math.max(1,counts.reduce((s,x)=>s+x.count,0));let cursor=0;
const parts=counts.filter(x=>x.count).map(x=>{const from=cursor;cursor+=x.count/total*360;return `${x.color} ${from}deg ${cursor}deg`;});
const background=parts.length?`conic-gradient(${parts.join(',')})`:'#edf0f1';
return `<div class="stats-donut-wrap"><div class="stats-donut" style="background:${background}"><div class="stats-donut-center"><b>${valid.length}</b><small>заказов</small></div></div><div class="stats-legend">${counts.map(x=>`<div class="stats-legend-row"><i style="background:${x.color}"></i><span>${esc(x.status)}</span><b>${x.count}</b></div>`).join('')}</div></div>`;
}
function topItems(valid){
const data=new Map();
valid.forEach(order=>{
const seenInOrder=new Set();
(order.lines||[]).forEach(line=>{
const item=boxes.find(x=>String(x.id)===String(line.id));
if(!item)return;
const key=String(item.id),row=data.get(key)||{name:item.name,qty:0,orders:0};
row.qty+=Number(line.qty||0);
if(!seenInOrder.has(key)){row.orders+=1;seenInOrder.add(key);}
data.set(key,row);
});
});
const list=[...data.values()].sort((a,b)=>b.orders-a.orders||b.qty-a.qty||String(a.name).localeCompare(String(b.name),'ru')).slice(0,8),max=Math.max(1,...list.map(x=>x.orders));
return list.length?`<div class="stats-hbars">${list.map(x=>`<div class="stats-hbar" title="${esc(x.name)}: ${x.orders} заказов, ${x.qty} шт."><span>${esc(x.name)}</span><div class="stats-hbar-track"><i style="width:${Math.max(3,x.orders/max*100)}%"></i></div><strong>${x.orders} ${x.orders===1?'заказ':'заказов'} · ${x.qty} шт.</strong></div>`).join('')}</div>`:'<div class="stats-empty">Позиции пока не продавались.</div>';
}
function weekdayChart(valid){
const labels=['Пн','Вт','Ср','Чт','Пт','Сб','Вс'],data=labels.map(label=>({label,count:0,sum:0}));
valid.forEach(o=>{const d=parseDate(o.date);if(!d)return;const i=(d.getDay()+6)%7;data[i].count++;data[i].sum+=totalFor(o);});
const max=Math.max(1,...data.map(x=>x.count));
return `<div class="stats-week">${data.map(x=>`<div class="stats-week-day" title="${x.label}: ${x.count} заказов, ${esc(fmt(x.sum))}"><i style="height:${Math.max(x.count?4:0,x.count/max*100)}%"></i><b>${x.count}</b><small>${x.label}</small></div>`).join('')}</div>`;
}
function renderStatsVisual(){
const period=el('statsPeriod')?.value||'year',valid=ordersForPeriod(period),sales=valid.reduce((s,o)=>s+totalFor(o),0),paid=valid.reduce((s,o)=>s+Math.max(0,Number(o.prepayment||0)),0),completed=valid.filter(o=>(o.status||'Новый')==='Отдан заказчику').length,active=valid.filter(o=>!['Отдан заказчику','Отменён'].includes(o.status||'Новый')).length,clients=new Set(valid.map(o=>String(o.phone||o.contact||'').trim().toLowerCase()).filter(Boolean)).size,avg=valid.length?sales/valid.length:0;
const buckets=trendBuckets(period,valid);
el('statsList').innerHTML=`
<div class="stats-kpi"><div><small>Заказов</small><b>${valid.length}</b></div><div><small>Продажи</small><b>${fmt(sales)}</b></div><div><small>Средний чек</small><b>${fmt(avg)}</b></div><div><small>Получено предоплат</small><b>${fmt(paid)}</b></div><div><small>Активных</small><b>${active}</b></div><div><small>Клиентов</small><b>${clients}</b></div></div>
<div class="stats-grid">
<section class="stats-panel"><div class="stats-panel-head"><h2>Динамика продаж</h2><small>сумма по периоду · завершено ${completed}</small></div>${barChart(buckets)}</section>
<section class="stats-panel"><div class="stats-panel-head"><h2>Статусы заказов</h2><small>распределение</small></div>${donut(valid)}</section>
<section class="stats-panel wide"><div class="stats-panel-head"><h2>Популярные позиции</h2><small>по количеству заказов</small></div>${topItems(valid)}</section>
<section class="stats-panel wide"><div class="stats-panel-head"><h2>Загрузка по дням недели</h2><small>количество мероприятий</small></div>${weekdayChart(valid)}</section>
</div>`;
}
window.renderStats = renderStatsVisual;
el('statsPeriod').addEventListener('change',renderStatsVisual);
el('statsPrint').onclick=()=>window.print();
renderStatsVisual();
})();
/* ===== STATIC DELIVERY MAP PREVIEWS / ORDER GEO MODULE v2 ===== */
(() => {
if (document.getElementById('sun-static-maps-style')) return;
const nav=document.querySelector('header nav'),detailsBox=document.getElementById('details'),addressInput=document.getElementById('address');
if(!nav||!detailsBox||!addressInput) return;
// The old Yandex API keys/settings are no longer used in this build.
try{localStorage.removeItem('sunYandexMapsSettings');}catch(_){ }
const GEO_CACHE_KEY='sunStaticMapGeocodeCacheV2';
const DEFAULT_CITY='Санкт-Петербург';
const statusColors={"Новый":"#6f9fc1","В работе":"#9a82bd","Собран":"#e0b242","Передан курьеру":"#df8c43","Отдан заказчику":"#66a77b","Отменён":"#c96b6b"};
const esc=window.SunSafe.escapeHTML;
const fmt=v=>typeof window.money==='function'?window.money(v):`${Math.round(Number(v||0)).toLocaleString('ru-RU')}`;
const el=id=>document.getElementById(id);
const todayKey=()=>new Date().toISOString().slice(0,10);
const plusDays=(key,n)=>{const d=new Date(`${key}T12:00:00`);d.setDate(d.getDate()+n);return d.toISOString().slice(0,10);};
const orderTotal=o=>Number(o?.total||0)||(o?.lines||[]).reduce((s,l)=>{const b=boxes.find(x=>String(x.id)===String(l.id));return s+Number(b?.price||0)*Number(l.qty||0);},0);
const geoValid=geo=>geo&&Number.isFinite(Number(geo.lon))&&Number.isFinite(Number(geo.lat));
let lastGeoRequest=0;
const style=document.createElement('style');
style.id='sun-static-maps-style';
style.textContent=`
.geo-order-box{margin:10px 0 14px;border:1px solid #dfe3e8;border-radius:14px;background:linear-gradient(145deg,#fff,#f8f9fc);padding:13px}.geo-order-head{display:flex;justify-content:space-between;align-items:flex-start;gap:10px}.geo-order-status{display:flex;align-items:center;gap:7px;font-size:12px;color:#68717a;margin-top:4px}.geo-dot{width:9px;height:9px;border-radius:50%;background:#b8c1c6;flex:0 0 auto}.geo-order-status.ok .geo-dot{background:#58a36f}.geo-order-status.warn .geo-dot{background:#d49a2a}.geo-order-actions{display:flex;gap:7px;flex-wrap:wrap;margin-top:11px}.geo-order-actions button{padding:7px 10px;font-size:12px}.geo-mini{margin-top:11px;border-radius:11px;overflow:hidden;border:1px solid #dfe3e5;background:#eef1f2;min-height:150px;display:grid;place-items:center}.geo-coords{padding:20px;text-align:center;color:#68717a;font-size:12px}.geo-coords img{width:58px;height:44px;object-fit:contain;opacity:.8;margin-bottom:7px}
.static-map-media{position:relative;background:#edf1f3;overflow:hidden}.static-map-frame{display:block;width:100%;height:100%;border:0;background:#edf1f3}.static-map-freeze{position:absolute;inset:0;z-index:3;cursor:default;background:transparent}.static-map-fallback{display:none;min-height:inherit;height:100%;place-items:center;text-align:center;padding:20px;background:linear-gradient(145deg,#f4f5f8,#eceff4);color:#68717a}.static-map-fallback img{width:70px;height:52px;object-fit:contain;display:block;margin:0 auto 8px}.static-map-credit{font-size:10px;color:#7e8790;margin-top:5px}
.map-page-head{display:flex;justify-content:space-between;align-items:flex-start;gap:12px;flex-wrap:wrap}.map-page-head p{margin:5px 0 0;color:#68717a;max-width:720px}.map-toolbar{display:grid;grid-template-columns:180px 190px minmax(220px,1fr);gap:9px;align-items:end;margin:14px 0}.map-summary{display:grid;grid-template-columns:repeat(3,minmax(130px,1fr));gap:10px;margin:14px 0}.map-summary>div{border:1px solid #e2e5ea;border-radius:12px;background:#fafbfc;padding:12px}.map-summary small{display:block;color:#68717a}.map-summary b{display:block;color:var(--n);font-size:20px;margin-top:5px}.static-map-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(310px,1fr));gap:13px}.static-map-card{border:1px solid #dfe3e8;border-radius:14px;background:#fff;overflow:hidden;box-shadow:0 5px 18px #24314a0b;transition:.16s}.static-map-card:hover{transform:translateY(-1px);box-shadow:0 8px 24px #24314a12}.static-map-card .static-map-media{height:190px}.static-map-card-body{padding:12px}.static-map-card-head{display:flex;justify-content:space-between;gap:10px;align-items:flex-start}.static-map-card h3{margin:0;color:#1f2c46;font-size:16px}.static-map-card p{margin:5px 0;color:#657080;font-size:12px;line-height:1.4}.static-map-badge{display:inline-flex;align-items:center;padding:4px 8px;border-radius:999px;color:#fff;font-size:10px;font-weight:800;white-space:nowrap}.static-map-card-actions{display:flex;gap:6px;flex-wrap:wrap;margin-top:10px}.static-map-card-actions button{font-size:11px;padding:6px 8px}.static-map-empty{grid-column:1/-1;padding:34px;text-align:center;border:1px dashed #cfd5dc;border-radius:14px;color:#7a8490;background:#fafbfc}.static-map-empty img{width:80px;height:58px;object-fit:contain;opacity:.8;display:block;margin:0 auto 10px}
.sun-doc-map{margin-top:20px;border:1px solid #d9dde0;border-radius:10px;overflow:hidden;break-inside:avoid}.sun-doc-map-head{padding:10px 12px;background:#f8f6f0;display:flex;justify-content:space-between;gap:10px}.sun-doc-map-head b{color:#15364c}.sun-doc-map .static-map-media{height:285px}.sun-doc-map-foot{padding:8px 12px;color:#68717a;font-size:11px}.sun-doc-map a{color:#15364c}.sun-doc-map-placeholder{padding:25px;text-align:center;background:#eef1f2;color:#68717a}
@media(max-width:850px){.map-toolbar{grid-template-columns:1fr 1fr}.map-toolbar label:last-child{grid-column:1/-1}.map-summary{grid-template-columns:1fr 1fr 1fr}}
@media(max-width:620px){.map-toolbar{grid-template-columns:1fr}.map-toolbar label:last-child{grid-column:auto}.map-summary{grid-template-columns:1fr 1fr}.static-map-grid{grid-template-columns:1fr}.sun-doc-map .static-map-media{height:210px}}
@media(max-width:430px){.map-summary{grid-template-columns:1fr}}
@media print{#order-map-view{display:none!important}.sun-doc-map .static-map-media{height:230px}.sun-doc-map-head{background:#f8f6f0!important;-webkit-print-color-adjust:exact;print-color-adjust:exact}.static-map-credit{font-size:8px}}
`;
document.head.appendChild(style);
function cleanMapAddress(value){
return String(value||'').split(',').filter(part=>!/^\s*(подъезд|этаж|кв\.?|квартира|офис)\b/i.test(part)).join(', ').trim();
}
function draftMapAddress(){
const region=el('deliveryRegion')?.value?.trim()||'';
const locality=el('deliveryLocality')?.value?.trim()||'';
const district=el('deliveryDistrict')?.value?.trim()||'';
const street=el('deliveryStreet')?.value?.trim()||'';
const house=el('deliveryHouse')?.value?.trim()||'';
const corpus=el('deliveryCorpus')?.value?.trim()||'';
const base=[region,locality,district,street,house?`д. ${house}`:'',corpus?`корп. ${corpus}`:''].filter(Boolean).join(', ');
return cleanMapAddress(base||addressInput.value);
}
function orderMapAddress(order){
const region=String(order?.deliveryRegion||'').trim(),locality=String(order?.deliveryLocality||'').trim(),district=String(order?.deliveryDistrict||'').trim();
const street=String(order?.deliveryStreet||'').trim(),house=String(order?.deliveryHouse||'').trim(),corpus=String(order?.deliveryCorpus||'').trim();
const base=[region,locality,district,street,house?`д. ${house}`:'',corpus?`корп. ${corpus}`:''].filter(Boolean).join(', ');
return cleanMapAddress(base||order?.address||'');
}
function withDefaultCity(address){
const a=String(address||'').trim();if(!a)return'';
if(/санкт[- ]?петербург|\bспб\b|москва|ленинградск|область|край|республика|красное\s+село|пушкин|павловск|петергоф|ломоносов|гатчин|стрельн|сестрорецк|кронштадт|всеволож|кудров|мурино|телези|ропш/i.test(a))return a;
return `${DEFAULT_CITY}, ${a}`;
}
function geoMatches(geo,mapAddress,fullAddress=''){
if(!geoValid(geo))return false;
const mapKey=String(mapAddress||'').trim();
if(Number(geo.geoVersion||0)!==3)return false;
return (geo.sourceMapAddress&&String(geo.sourceMapAddress)===mapKey)||(!geo.sourceMapAddress&&geo.sourceAddress&&String(geo.sourceAddress)===String(fullAddress||''));
}
function staticMapUrl(geo,{zoom=16}={}){
// Stable key-free OpenStreetMap embed. It is displayed as a frozen preview:
// interaction is blocked by an overlay, so for the user it behaves like a picture.
if(!geoValid(geo))return'';
const lat=Number(geo.lat),lon=Number(geo.lon);
const factor=Math.pow(2,16-Number(zoom||16));
const dLat=0.0032*factor,dLon=0.0062*factor;
const bbox=[lon-dLon,lat-dLat,lon+dLon,lat+dLat].map(v=>v.toFixed(6)).join(',');
return `https://www.openstreetmap.org/export/embed.html?bbox=${encodeURIComponent(bbox)}&layer=mapnik&marker=${encodeURIComponent(`${lat.toFixed(6)},${lon.toFixed(6)}`)}`;
}
function yandexUrl(geo,address){
if(geoValid(geo))return `https://yandex.ru/maps/?ll=${encodeURIComponent(`${geo.lon},${geo.lat}`)}&z=16&pt=${encodeURIComponent(`${geo.lon},${geo.lat},pm2rdm`)}`;
return `https://yandex.ru/maps/?text=${encodeURIComponent(address||'')}`;
}
function routeUrl(geo,address){
if(geoValid(geo))return `https://yandex.ru/maps/?rtext=${encodeURIComponent(`59.701058,29.977269~${geo.lat},${geo.lon}`)}&rtt=auto`;
return `https://yandex.ru/maps/?text=${encodeURIComponent(address||'')}`;
}
function mediaHtml(geo,label='Карта доставки'){
const src=staticMapUrl(geo);if(!src)return'';
return `<div class="static-map-media" aria-label="${esc(label)}"><iframe class="static-map-frame" loading="lazy" src="${esc(src)}" title="${esc(label)}" tabindex="-1" referrerpolicy="no-referrer-when-downgrade"></iframe><div class="static-map-freeze" aria-hidden="true"></div><div class="static-map-fallback"><div><img src="${window.CateriumBranding.logoHTML()}" alt="${window.CateriumBranding.nameHTML()}"><span>Не удалось загрузить превью карты</span></div></div></div>`;
}
function readGeoCache(){try{return JSON.parse(localStorage.getItem(GEO_CACHE_KEY)||'{}')||{};}catch{return{};}}
function writeGeoCache(cache){try{localStorage.setItem(GEO_CACHE_KEY,JSON.stringify(cache));}catch(_){ }}
async function geocode(mapAddress){
const clean=String(mapAddress||'').trim();if(!clean)throw new Error('Сначала укажите улицу и дом.');
const query=withDefaultCity(clean),key=query.toLowerCase(),cache=readGeoCache();
if(cache[key]&&geoValid(cache[key]))return {...cache[key],sourceMapAddress:clean};
const wait=Math.max(0,1100-(Date.now()-lastGeoRequest));if(wait)await new Promise(r=>setTimeout(r,wait));lastGeoRequest=Date.now();
let row=null,provider='nominatim';
try{
const url=`https://nominatim.openstreetmap.org/search?format=jsonv2&limit=5&countrycodes=ru&viewbox=27.3,61.4,34.5,58.0&bounded=1&accept-language=ru&q=${encodeURIComponent(query)}`;
const response=await sunFetchWithTimeout(url,{method:'GET',mode:'cors',credentials:'omit',cache:'default'},6000);
if(response.ok){const rows=await response.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 response=await sunFetchWithTimeout(url,{method:'GET',mode:'cors',credentials:'omit',cache:'default'},6000);
if(response.ok){
const data=await response.json(),f=Array.isArray(data?.features)?data.features[0]:null,c=f?.geometry?.coordinates;
if(Array.isArray(c)&&Number.isFinite(Number(c[0]))&&Number.isFinite(Number(c[1]))){
const pr=f.properties||{};
row={lon:Number(c[0]),lat:Number(c[1]),display_name:[pr.name,pr.street,pr.housenumber,pr.city,pr.state,pr.country].filter(Boolean).join(', ')||query};
provider='photon';
}
}
}catch(_){row=null;}
}
if(!row||!Number.isFinite(Number(row.lon))||!Number.isFinite(Number(row.lat)))throw new Error('Не удалось найти адрес. Проверьте улицу, дом и подключение к интернету.');
const result={lon:Number(row.lon),lat:Number(row.lat),formatted:String(row.display_name||query),sourceMapAddress:clean,sourceAddress:addressInput.value||'',verifiedAt:new Date().toISOString(),provider,geoVersion:3};
cache[key]=result;writeGeoCache(cache);return result;
}
function saveDraftGeo(geo){
draft.geo={...geo,sourceMapAddress:draftMapAddress(),sourceAddress:addressInput.value||'',verifiedAt:new Date().toISOString(),provider:'osm'};
if(draft.id){const idx=orders.findIndex(o=>String(o.id)===String(draft.id));if(idx>=0){orders[idx].geo=structuredClone(draft.geo);persist();}}
renderDraftGeo();return draft.geo;
}
async function ensureDraftGeo({silent=false,force=false}={}){
const mapAddress=draftMapAddress();
if(!mapAddress){if(!silent)alert('Сначала укажите улицу и дом.');return null;}
if(!force&&geoMatches(draft.geo,mapAddress,addressInput.value))return draft.geo;
try{return saveDraftGeo(await geocode(mapAddress));}catch(error){if(!silent)alert(error.message||'Не удалось создать карту.');return null;}
}
async function ensureOrderGeo(order,{silent=false,force=false}={}){
if(!order)return null;const mapAddress=orderMapAddress(order);
if(!mapAddress){if(!silent)alert('В заказе не указан адрес.');return null;}
if(!force&&geoMatches(order.geo,mapAddress,order.address))return order.geo;
try{const geo=await geocode(mapAddress);order.geo={...geo,sourceMapAddress:mapAddress,sourceAddress:order.address||'',verifiedAt:new Date().toISOString(),provider:'osm'};persist();if(draft.id&&String(draft.id)===String(order.id))draft.geo=structuredClone(order.geo);return order.geo;}catch(error){if(!silent)alert(error.message||'Не удалось создать карту.');return null;}
}
// Static map block in order details.
const addressLabel=addressInput.closest('label');
const geoBox=document.createElement('div');geoBox.id='orderGeoBox';geoBox.className='geo-order-box';
geoBox.innerHTML=`<div class="geo-order-head"><div><b>Проверка адреса доставки</b><div class="geo-order-status" id="orderGeoStatus"><i class="geo-dot"></i><span>Адрес ещё не проверен</span></div></div><span class="hint" id="orderGeoPrecision"></span></div><div class="geo-order-actions"><button type="button" class="primary" id="orderGeoCreate">Показать карту</button><button type="button" class="outline" id="orderGeoOpen">Открыть в Яндекс Картах</button><button type="button" class="outline" id="orderGeoRoute">Маршрут</button></div><div class="geo-mini" id="orderGeoMini"><div class="geo-coords"><img src="${window.CateriumBranding.logoHTML()}" alt="${window.CateriumBranding.nameHTML()}"><div>Укажите улицу и дом</div></div></div>`;
if(addressLabel)addressLabel.insertAdjacentElement('afterend',geoBox);else detailsBox.appendChild(geoBox);
function renderDraftGeo(){
const addr=addressInput.value||'',mapAddress=draftMapAddress(),valid=geoMatches(draft?.geo,mapAddress,addr),status=el('orderGeoStatus'),caption=el('orderGeoPrecision'),mini=el('orderGeoMini'),create=el('orderGeoCreate');
if(!mapAddress){status.className='geo-order-status';status.querySelector('span').textContent='Укажите улицу и дом';caption.textContent='';mini.innerHTML=("<div class=\"geo-coords\"><img src=\""+window.CateriumBranding.logoHTML()+"\" alt=\""+window.CateriumBranding.nameHTML()+"\"><div>После ввода адреса здесь появится превью карты.</div></div>");create.textContent='Показать карту';return;}
if(!valid){status.className='geo-order-status warn';status.querySelector('span').textContent='Превью не создано';caption.textContent='';mini.innerHTML=("<div class=\"geo-coords\"><img src=\""+window.CateriumBranding.logoHTML()+"\" alt=\""+window.CateriumBranding.nameHTML()+"\"><div>Нажмите «Показать карту».</div></div>");create.textContent='Показать карту';return;}
status.className='geo-order-status ok';status.querySelector('span').textContent='Адрес найден на карте';caption.textContent=`${Number(draft.geo.lat).toFixed(5)}, ${Number(draft.geo.lon).toFixed(5)}`;mini.innerHTML=mediaHtml(draft.geo,'Карта доставки')+'<div class="static-map-credit">© OpenStreetMap contributors</div>';create.textContent='Обновить карту';
}
// Separate page: no live map, only delivery cards with static images.
const mapButton=document.createElement('button');mapButton.type='button';mapButton.textContent='Карта';mapButton.classList.add('sun-nav-button','nav-map');mapButton.dataset.navLabel='Карта';nav.appendChild(mapButton);
const mapView=document.createElement('section');mapView.id='order-map-view';mapView.className='view page';
mapView.innerHTML=`<div class="map-page-head"><div><h1>Карты доставки</h1><p>Неинтерактивные превью с точкой доставки. Управление картой отключено; для навигации курьера используйте Яндекс Карты.</p></div></div><div class="map-toolbar"><label>Период<select id="mapPeriod"><option value="today">Сегодня</option><option value="tomorrow">Завтра</option><option value="week" selected>7 дней</option><option value="month">30 дней</option><option value="all">Все заказы</option></select></label><label>Статус<select id="mapStatus"><option value="all">Все статусы</option>${Object.keys(statusColors).map(s=>`<option>${s}</option>`).join('')}</select></label><label>Поиск<input id="mapSearch" placeholder="Заказ, клиент, улица..."></label></div><div class="map-summary"><div><small>Заказов</small><b id="mapCount">0</b></div><div><small>Превью готово</small><b id="mapReady">0</b></div><div><small>Без превью</small><b id="mapMissing">0</b></div></div><div id="staticMapGrid" class="static-map-grid"></div>`;
document.body.appendChild(mapView);
function filterMapOrders(){
const period=el('mapPeriod')?.value||'week',status=el('mapStatus')?.value||'all',q=(el('mapSearch')?.value||'').trim().toLowerCase(),today=todayKey();
return orders.slice().filter(o=>{
if(status!=='all'&&(o.status||'Новый')!==status)return false;
if(period==='today'&&o.date!==today)return false;if(period==='tomorrow'&&o.date!==plusDays(today,1))return false;if(period==='week'&&(o.date<today||o.date>plusDays(today,6)))return false;if(period==='month'&&(o.date<today||o.date>plusDays(today,29)))return false;
if(q&&!`${o.id} ${o.event||''} ${o.contact||''} ${o.phone||''} ${o.address||''}`.toLowerCase().includes(q))return false;return true;
}).sort((a,b)=>`${a.date||''}T${a.time||''}`.localeCompare(`${b.date||''}T${b.time||''}`));
}
function renderMapPage(){
const list=filterMapOrders(),ready=list.filter(o=>geoMatches(o.geo,orderMapAddress(o),o.address));el('mapCount').textContent=list.length;el('mapReady').textContent=ready.length;el('mapMissing').textContent=list.length-ready.length;
el('staticMapGrid').innerHTML=list.length?list.map(o=>{const has=geoMatches(o.geo,orderMapAddress(o),o.address),color=statusColors[o.status||'Новый']||'#6f9fc1',media=has?mediaHtml(o.geo,`Карта заказа №${o.id}`):`<div class="static-map-media"><div class="static-map-fallback" style="display:grid"><div><img src="${window.CateriumBranding.logoHTML()}" alt="${window.CateriumBranding.nameHTML()}"><span>Превью ещё не создано</span></div></div></div>`;return `<article class="static-map-card">${media}<div class="static-map-card-body"><div class="static-map-card-head"><div><h3>№${esc(o.id)} · ${esc(o.time||'—')}</h3><p>${esc(o.date||'—')} · ${esc(o.event||'Заказ')}</p></div><span class="static-map-badge" style="background:${color}">${esc(o.status||'Новый')}</span></div><p><b>${esc(o.contact||'Клиент не указан')}</b> · ${esc(fmt(orderTotal(o)))}</p><p>${esc(o.address||'Адрес не указан')}</p>${has?'<div class="static-map-credit">© OpenStreetMap contributors</div>':''}<div class="static-map-card-actions"><button type="button" class="outline" onclick="sunStaticMapOpenOrder('${esc(o.id)}')">Заказ</button>${o.address?`<button type="button" class="${has?'outline':'primary'}" onclick="sunStaticMapCreateForOrder('${esc(o.id)}')">${has?'Обновить':'Показать карту'}</button><button type="button" class="outline" onclick="sunStaticMapOpenYandex('${esc(o.id)}')">Яндекс Карты</button><button type="button" class="outline" onclick="sunStaticMapRoute('${esc(o.id)}')">Маршрут</button>`:''}</div></div></article>`;}).join(''):`<div class="static-map-empty"><img src="${window.CateriumBranding.logoHTML()}" alt="${window.CateriumBranding.nameHTML()}"><b>Заказов по выбранным фильтрам нет.</b></div>`;
}
window.sunStaticMapOpenOrder=id=>{const o=orders.find(x=>String(x.id)===String(id));if(o)window.openOrder?.(Number(o.id));};
window.sunStaticMapCreateForOrder=async id=>{const o=orders.find(x=>String(x.id)===String(id));if(!o)return;await ensureOrderGeo(o);renderMapPage();};
window.sunStaticMapOpenYandex=id=>{const o=orders.find(x=>String(x.id)===String(id));if(o)window.open(yandexUrl(o.geo,o.address),'_blank','noopener');};
window.sunStaticMapRoute=id=>{const o=orders.find(x=>String(x.id)===String(id));if(o)window.open(routeUrl(o.geo,o.address),'_blank','noopener');};
function documentMapBlock(){
const addr=addressInput.value||'',mapAddress=draftMapAddress(),geo=draft?.geo;if(!geoMatches(geo,mapAddress,addr))return'';
return `<div class="sun-doc-map"><div class="sun-doc-map-head"><b>Точка доставки</b><span>превью карты</span></div>${mediaHtml(geo,'Карта доставки')}<div class="sun-doc-map-foot">${esc(geo.formatted||addr)} · <a href="${esc(yandexUrl(geo,addr))}">Открыть в Яндекс Картах</a><div class="static-map-credit">© OpenStreetMap contributors</div></div></div>`;
}
function injectMapIntoDocument(){const doc=document.querySelector('#preview .sun-doc');if(!doc||doc.querySelector('.sun-doc-map'))return;const html=documentMapBlock();if(!html)return;const footer=doc.querySelector('.sun-doc-footer');if(footer)footer.insertAdjacentHTML('beforebegin',html);else doc.insertAdjacentHTML('beforeend',html);}
[...document.querySelectorAll('#new .actions button')].filter(b=>b.textContent.trim()==='Бланк заказа').forEach(button=>{const original=button.onclick;button.onclick=event=>{original?.call(button,event);injectMapIntoDocument();};});
el('orderGeoCreate').onclick=async()=>{await ensureDraftGeo();renderMapPage();};
el('orderGeoOpen').onclick=()=>window.open(yandexUrl(draft.geo,addressInput.value),'_blank','noopener');
el('orderGeoRoute').onclick=()=>window.open(routeUrl(draft.geo,addressInput.value),'_blank','noopener');
addressInput.addEventListener('input',()=>{const current=draftMapAddress();if(draft?.geo&&!geoMatches(draft.geo,current,addressInput.value))draft.geo=null;renderDraftGeo();});
['mapPeriod','mapStatus'].forEach(id=>el(id).addEventListener('change',renderMapPage));el('mapSearch').addEventListener('input',renderMapPage);
mapButton.onclick=()=>{document.querySelectorAll('.view').forEach(v=>v.classList.remove('on'));mapView.classList.add('on');[...nav.querySelectorAll('button')].forEach(b=>b.classList.remove('on'));mapButton.classList.add('on');renderMapPage();};
if(typeof window.openOrder==='function'){const previous=window.openOrder;window.openOrder=id=>{const result=previous(id);renderDraftGeo();return result;};}
if(typeof window.resetDraft==='function'){const previous=window.resetDraft;window.resetDraft=()=>{const result=previous();renderDraftGeo();return result;};}
if(typeof window.status==='function'){const previous=window.status;window.status=(id,value)=>{const result=previous(id,value);if(mapView.classList.contains('on'))renderMapPage();return result;};}
window.SunStaticMap={validGeo:geoValid,staticMapUrl,yandexUrl,routeUrl,mediaHtml,ensureOrderGeo,ensureDraftGeo,orderMapAddress,geoMatches};
renderDraftGeo();
})();
// === Preparations print preview + box photo sheet update ===
(() => {
const style = document.createElement('style');
style.textContent = `
.prep-doc{font-family:Arial,sans-serif;color:#203040;max-width:1000px;margin:0 auto}
.prep-doc-head{display:flex;justify-content:space-between;gap:20px;align-items:flex-start;border-bottom:2px solid #c99a32;padding-bottom:12px;margin-bottom:16px}
.prep-doc-brand{display:flex;align-items:center;gap:10px}.prep-doc-brand img{width:72px;height:52px;object-fit:contain}.prep-doc-brand b{display:block;color:#15364c;font-size:19px}.prep-doc-brand small{color:#68717a}
.prep-doc-title{text-align:right}.prep-doc-title h2{margin:0;color:#15364c;font-size:23px}.prep-doc-title p{margin:5px 0 0;color:#68717a}
.prep-doc-section{margin-top:18px}.prep-doc-section h3{margin:0 0 10px;color:#15364c;font-size:17px}
.prep-list{display:grid;grid-template-columns:1fr 1fr;gap:0 24px;border-top:1px solid #d9dde0}.prep-list p{display:flex;justify-content:space-between;gap:16px;margin:0;padding:8px 0;border-bottom:1px solid #e6e9eb}.prep-list p span{white-space:nowrap;font-weight:700;color:#15364c}
.prep-photo-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin-top:10px}.prep-photo-card{border:1px solid #d9dde0;border-radius:7px;padding:7px;background:#fff;break-inside:avoid;page-break-inside:avoid}.prep-photo-card img{width:100%;aspect-ratio:4/3;object-fit:cover;border-radius:5px;display:block;background:#f0f2f4}.prep-photo-card b{display:block;font-size:11px;line-height:1.25;margin-top:6px;color:#203040;min-height:28px}.prep-photo-card small{display:flex;justify-content:space-between;gap:6px;margin-top:4px;color:#68717a;font-size:10px}.prep-photo-card .qty{font-weight:700;color:#15364c}
.prep-doc-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:20px;position:sticky;bottom:0;background:#fff;padding:12px 0 2px;border-top:1px solid #eef0f1}
.prep-empty{color:#879198;padding:14px 0}
@media(max-width:720px){.prep-doc-head{flex-direction:column}.prep-doc-title{text-align:left}.prep-list{grid-template-columns:1fr}.prep-photo-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media print{
body *{visibility:hidden!important}
#preview,#preview *{visibility:visible!important}
#preview{display:block!important;position:absolute!important;inset:0!important;background:#fff!important;padding:0!important;margin:0!important}
#preview .dialog{display:block!important;width:100%!important;max-width:none!important;max-height:none!important;overflow:visible!important;padding:8mm!important;border-radius:0!important;box-shadow:none!important}
.prep-doc-actions,.dialog-head{display:none!important}
.prep-doc{max-width:none!important}
.prep-doc-head{margin-bottom:10px;padding-bottom:8px}
.prep-list{font-size:11px;gap:0 16px}.prep-list p{padding:5px 0}
.prep-doc-section{margin-top:12px}.prep-doc-section h3{font-size:14px;margin-bottom:6px}
.prep-photo-grid{grid-template-columns:repeat(4,1fr)!important;gap:5mm 4mm!important}
.prep-photo-card{height:61mm!important;padding:3mm!important;border-radius:2mm!important;overflow:hidden!important}
.prep-photo-card img{height:42mm!important;aspect-ratio:auto!important;object-fit:cover!important}
.prep-photo-card b{font-size:9.5pt!important;line-height:1.15!important;margin-top:2mm!important;min-height:0!important;max-height:8mm!important;overflow:hidden!important}
.prep-photo-card small{font-size:8pt!important;margin-top:1mm!important}
.prep-photo-card:nth-child(8n+1){break-before:auto}
.prep-photo-card:nth-child(8n){break-after:page;page-break-after:always}
.prep-photo-card:last-child{break-after:auto!important;page-break-after:auto!important}
@page{size:A4 portrait;margin:9mm}
}
`;
document.head.appendChild(style);
const htmlEscape = value => String(value ?? '').replace(/[&<>"']/g, ch => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[ch]));
const fmtQty = value => Number.isInteger(Number(value)) ? String(Number(value)) : Number(value).toLocaleString('ru-RU',{maximumFractionDigits:2});
function prepData(){
const map = new Map();
const selected = [];
(draft.lines || []).forEach(line => {
const item = boxes.find(x => String(x.id) === String(line.id));
if (!item) return;
const qty = Number(line.qty || 0);
if (qty <= 0) return;
selected.push({item, qty});
(Array.isArray(item.ingredients) ? item.ingredients : []).forEach(part => {
if (!part) return;
const name = String(part[0] || '').trim();
const unit = String(part[2] || '').trim();
if (!name) return;
const key = `${name}|${unit}`;
map.set(key, (map.get(key) || 0) + Number(part[1] || 0) * qty);
});
});
return {entries:[...map.entries()], selected};
}
window.openPreview = () => {
const {entries, selected} = prepData();
const dialog = document.querySelector('#preview .dialog');
if (!dialog) return;
const eventName = document.getElementById('event')?.value || '';
const orderDate = document.getElementById('date')?.value || '';
const orderTime = document.getElementById('time')?.value || '';
const photos = selected.filter(({item}) => [0,5].includes(Number(item.category ?? 0)) && item.photo);
const prepHtml = entries.length ? entries.map(([key,value]) => {
const split = key.lastIndexOf('|');
const product = split >= 0 ? key.slice(0,split) : key;
const unit = split >= 0 ? key.slice(split+1) : '';
return `<p><b>${htmlEscape(product)}</b><span>${htmlEscape(fmtQty(value))} ${htmlEscape(unit)}</span></p>`;
}).join('') : `<div class="prep-empty">В заказе нет позиций с составом для заготовок.</div>`;
const photoHtml = photos.length ? photos.map(({item,qty}) => `
<div class="prep-photo-card">
<img src="${htmlEscape(item.photo)}" alt="${htmlEscape(item.name)}">
<b>${htmlEscape(item.name)}</b>
<small><span>${htmlEscape(item.weight || '')}</span><span class="qty">× ${htmlEscape(qty)}</span></small>
</div>`).join('') : `<div class="prep-empty">В заказе нет боксов с фотографиями.</div>`;
dialog.innerHTML = `<div class="prep-doc">
<div class="prep-doc-head">
<div class="prep-doc-brand"><img src="${window.CateriumBranding.logoHTML()}" alt="${window.CateriumBranding.nameHTML()}"><div><b>${window.CateriumBranding.nameHTML()}</b><small>Производственная заготовка заказа</small></div></div>
<div class="prep-doc-title"><h2>Заготовки${draft.id ? ` · заказ № ${htmlEscape(draft.id)}` : ''}</h2><p>${htmlEscape(eventName)}${orderDate ? ` · ${htmlEscape(orderDate)}` : ''}${orderTime ? ` · ${htmlEscape(orderTime)}` : ''}</p></div>
</div>
<section class="prep-doc-section"><h3>Продукты и заготовки</h3><div class="prep-list">${prepHtml}</div></section>
<section class="prep-doc-section"><h3>Боксы в заказе</h3><div class="prep-photo-grid">${photoHtml}</div></section>
<div class="prep-doc-actions"><button class="primary" type="button" onclick="window.print()">Печать / PDF</button><button class="outline" type="button" onclick="closeModal('preview')">Закрыть</button></div>
</div>`;
modal('preview');
};
})();
/* END category-fix.js */
</script><script>
/* BEGIN qr-lite.js — PORTABLE INLINE */
/* QR Code generator based on Kazuhiko Arase's MIT-licensed QRCode for JavaScript. */
(function(){
'use strict';
// ---- QRErrorCorrectLevel.js ----
var QRErrorCorrectLevel = {
L : 1,
M : 0,
Q : 3,
H : 2
};
// ---- QRMode.js ----
var QRMode = {
MODE_NUMBER : 1 << 0,
MODE_ALPHA_NUM : 1 << 1,
MODE_8BIT_BYTE : 1 << 2,
MODE_KANJI : 1 << 3
};
// ---- QRMaskPattern.js ----
var QRMaskPattern = {
PATTERN000 : 0,
PATTERN001 : 1,
PATTERN010 : 2,
PATTERN011 : 3,
PATTERN100 : 4,
PATTERN101 : 5,
PATTERN110 : 6,
PATTERN111 : 7
};
// ---- QRMath.js ----
var QRMath = {
glog : function(n) {
if (n < 1) {
throw new Error("glog(" + n + ")");
}
return QRMath.LOG_TABLE[n];
},
gexp : function(n) {
while (n < 0) {
n += 255;
}
while (n >= 256) {
n -= 255;
}
return QRMath.EXP_TABLE[n];
},
EXP_TABLE : new Array(256),
LOG_TABLE : new Array(256)
};
for (var i = 0; i < 8; i++) {
QRMath.EXP_TABLE[i] = 1 << i;
}
for (var i = 8; i < 256; i++) {
QRMath.EXP_TABLE[i] = QRMath.EXP_TABLE[i - 4]
^ QRMath.EXP_TABLE[i - 5]
^ QRMath.EXP_TABLE[i - 6]
^ QRMath.EXP_TABLE[i - 8];
}
for (var i = 0; i < 255; i++) {
QRMath.LOG_TABLE[QRMath.EXP_TABLE[i] ] = i;
}
// ---- QRPolynomial.js ----
function QRPolynomial(num, shift) {
if (num.length === undefined) {
throw new Error(num.length + "/" + shift);
}
var offset = 0;
while (offset < num.length && num[offset] === 0) {
offset++;
}
this.num = new Array(num.length - offset + shift);
for (var i = 0; i < num.length - offset; i++) {
this.num[i] = num[i + offset];
}
}
QRPolynomial.prototype = {
get : function(index) {
return this.num[index];
},
getLength : function() {
return this.num.length;
},
multiply : function(e) {
var num = new Array(this.getLength() + e.getLength() - 1);
for (var i = 0; i < this.getLength(); i++) {
for (var j = 0; j < e.getLength(); j++) {
num[i + j] ^= QRMath.gexp(QRMath.glog(this.get(i) ) + QRMath.glog(e.get(j) ) );
}
}
return new QRPolynomial(num, 0);
},
mod : function(e) {
if (this.getLength() - e.getLength() < 0) {
return this;
}
var ratio = QRMath.glog(this.get(0) ) - QRMath.glog(e.get(0) );
var num = new Array(this.getLength() );
for (var i = 0; i < this.getLength(); i++) {
num[i] = this.get(i);
}
for (var x = 0; x < e.getLength(); x++) {
num[x] ^= QRMath.gexp(QRMath.glog(e.get(x) ) + ratio);
}
// recursive call
return new QRPolynomial(num, 0).mod(e);
}
};
// ---- QRRSBlock.js ----
function QRRSBlock(totalCount, dataCount) {
this.totalCount = totalCount;
this.dataCount = dataCount;
}
QRRSBlock.RS_BLOCK_TABLE = [
// L
// M
// Q
// H
// 1
[1, 26, 19],
[1, 26, 16],
[1, 26, 13],
[1, 26, 9],
// 2
[1, 44, 34],
[1, 44, 28],
[1, 44, 22],
[1, 44, 16],
// 3
[1, 70, 55],
[1, 70, 44],
[2, 35, 17],
[2, 35, 13],
// 4
[1, 100, 80],
[2, 50, 32],
[2, 50, 24],
[4, 25, 9],
// 5
[1, 134, 108],
[2, 67, 43],
[2, 33, 15, 2, 34, 16],
[2, 33, 11, 2, 34, 12],
// 6
[2, 86, 68],
[4, 43, 27],
[4, 43, 19],
[4, 43, 15],
// 7
[2, 98, 78],
[4, 49, 31],
[2, 32, 14, 4, 33, 15],
[4, 39, 13, 1, 40, 14],
// 8
[2, 121, 97],
[2, 60, 38, 2, 61, 39],
[4, 40, 18, 2, 41, 19],
[4, 40, 14, 2, 41, 15],
// 9
[2, 146, 116],
[3, 58, 36, 2, 59, 37],
[4, 36, 16, 4, 37, 17],
[4, 36, 12, 4, 37, 13],
// 10
[2, 86, 68, 2, 87, 69],
[4, 69, 43, 1, 70, 44],
[6, 43, 19, 2, 44, 20],
[6, 43, 15, 2, 44, 16],
// 11
[4, 101, 81],
[1, 80, 50, 4, 81, 51],
[4, 50, 22, 4, 51, 23],
[3, 36, 12, 8, 37, 13],
// 12
[2, 116, 92, 2, 117, 93],
[6, 58, 36, 2, 59, 37],
[4, 46, 20, 6, 47, 21],
[7, 42, 14, 4, 43, 15],
// 13
[4, 133, 107],
[8, 59, 37, 1, 60, 38],
[8, 44, 20, 4, 45, 21],
[12, 33, 11, 4, 34, 12],
// 14
[3, 145, 115, 1, 146, 116],
[4, 64, 40, 5, 65, 41],
[11, 36, 16, 5, 37, 17],
[11, 36, 12, 5, 37, 13],
// 15
[5, 109, 87, 1, 110, 88],
[5, 65, 41, 5, 66, 42],
[5, 54, 24, 7, 55, 25],
[11, 36, 12],
// 16
[5, 122, 98, 1, 123, 99],
[7, 73, 45, 3, 74, 46],
[15, 43, 19, 2, 44, 20],
[3, 45, 15, 13, 46, 16],
// 17
[1, 135, 107, 5, 136, 108],
[10, 74, 46, 1, 75, 47],
[1, 50, 22, 15, 51, 23],
[2, 42, 14, 17, 43, 15],
// 18
[5, 150, 120, 1, 151, 121],
[9, 69, 43, 4, 70, 44],
[17, 50, 22, 1, 51, 23],
[2, 42, 14, 19, 43, 15],
// 19
[3, 141, 113, 4, 142, 114],
[3, 70, 44, 11, 71, 45],
[17, 47, 21, 4, 48, 22],
[9, 39, 13, 16, 40, 14],
// 20
[3, 135, 107, 5, 136, 108],
[3, 67, 41, 13, 68, 42],
[15, 54, 24, 5, 55, 25],
[15, 43, 15, 10, 44, 16],
// 21
[4, 144, 116, 4, 145, 117],
[17, 68, 42],
[17, 50, 22, 6, 51, 23],
[19, 46, 16, 6, 47, 17],
// 22
[2, 139, 111, 7, 140, 112],
[17, 74, 46],
[7, 54, 24, 16, 55, 25],
[34, 37, 13],
// 23
[4, 151, 121, 5, 152, 122],
[4, 75, 47, 14, 76, 48],
[11, 54, 24, 14, 55, 25],
[16, 45, 15, 14, 46, 16],
// 24
[6, 147, 117, 4, 148, 118],
[6, 73, 45, 14, 74, 46],
[11, 54, 24, 16, 55, 25],
[30, 46, 16, 2, 47, 17],
// 25
[8, 132, 106, 4, 133, 107],
[8, 75, 47, 13, 76, 48],
[7, 54, 24, 22, 55, 25],
[22, 45, 15, 13, 46, 16],
// 26
[10, 142, 114, 2, 143, 115],
[19, 74, 46, 4, 75, 47],
[28, 50, 22, 6, 51, 23],
[33, 46, 16, 4, 47, 17],
// 27
[8, 152, 122, 4, 153, 123],
[22, 73, 45, 3, 74, 46],
[8, 53, 23, 26, 54, 24],
[12, 45, 15, 28, 46, 16],
// 28
[3, 147, 117, 10, 148, 118],
[3, 73, 45, 23, 74, 46],
[4, 54, 24, 31, 55, 25],
[11, 45, 15, 31, 46, 16],
// 29
[7, 146, 116, 7, 147, 117],
[21, 73, 45, 7, 74, 46],
[1, 53, 23, 37, 54, 24],
[19, 45, 15, 26, 46, 16],
// 30
[5, 145, 115, 10, 146, 116],
[19, 75, 47, 10, 76, 48],
[15, 54, 24, 25, 55, 25],
[23, 45, 15, 25, 46, 16],
// 31
[13, 145, 115, 3, 146, 116],
[2, 74, 46, 29, 75, 47],
[42, 54, 24, 1, 55, 25],
[23, 45, 15, 28, 46, 16],
// 32
[17, 145, 115],
[10, 74, 46, 23, 75, 47],
[10, 54, 24, 35, 55, 25],
[19, 45, 15, 35, 46, 16],
// 33
[17, 145, 115, 1, 146, 116],
[14, 74, 46, 21, 75, 47],
[29, 54, 24, 19, 55, 25],
[11, 45, 15, 46, 46, 16],
// 34
[13, 145, 115, 6, 146, 116],
[14, 74, 46, 23, 75, 47],
[44, 54, 24, 7, 55, 25],
[59, 46, 16, 1, 47, 17],
// 35
[12, 151, 121, 7, 152, 122],
[12, 75, 47, 26, 76, 48],
[39, 54, 24, 14, 55, 25],
[22, 45, 15, 41, 46, 16],
// 36
[6, 151, 121, 14, 152, 122],
[6, 75, 47, 34, 76, 48],
[46, 54, 24, 10, 55, 25],
[2, 45, 15, 64, 46, 16],
// 37
[17, 152, 122, 4, 153, 123],
[29, 74, 46, 14, 75, 47],
[49, 54, 24, 10, 55, 25],
[24, 45, 15, 46, 46, 16],
// 38
[4, 152, 122, 18, 153, 123],
[13, 74, 46, 32, 75, 47],
[48, 54, 24, 14, 55, 25],
[42, 45, 15, 32, 46, 16],
// 39
[20, 147, 117, 4, 148, 118],
[40, 75, 47, 7, 76, 48],
[43, 54, 24, 22, 55, 25],
[10, 45, 15, 67, 46, 16],
// 40
[19, 148, 118, 6, 149, 119],
[18, 75, 47, 31, 76, 48],
[34, 54, 24, 34, 55, 25],
[20, 45, 15, 61, 46, 16]
];
QRRSBlock.getRSBlocks = function(typeNumber, errorCorrectLevel) {
var rsBlock = QRRSBlock.getRsBlockTable(typeNumber, errorCorrectLevel);
if (rsBlock === undefined) {
throw new Error("bad rs block @ typeNumber:" + typeNumber + "/errorCorrectLevel:" + errorCorrectLevel);
}
var length = rsBlock.length / 3;
var list = [];
for (var i = 0; i < length; i++) {
var count = rsBlock[i * 3 + 0];
var totalCount = rsBlock[i * 3 + 1];
var dataCount = rsBlock[i * 3 + 2];
for (var j = 0; j < count; j++) {
list.push(new QRRSBlock(totalCount, dataCount) );
}
}
return list;
};
QRRSBlock.getRsBlockTable = function(typeNumber, errorCorrectLevel) {
switch(errorCorrectLevel) {
case QRErrorCorrectLevel.L :
return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 0];
case QRErrorCorrectLevel.M :
return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 1];
case QRErrorCorrectLevel.Q :
return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 2];
case QRErrorCorrectLevel.H :
return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 3];
default :
return undefined;
}
};
// ---- QRBitBuffer.js ----
function QRBitBuffer() {
this.buffer = [];
this.length = 0;
}
QRBitBuffer.prototype = {
get : function(index) {
var bufIndex = Math.floor(index / 8);
return ( (this.buffer[bufIndex] >>> (7 - index % 8) ) & 1) == 1;
},
put : function(num, length) {
for (var i = 0; i < length; i++) {
this.putBit( ( (num >>> (length - i - 1) ) & 1) == 1);
}
},
getLengthInBits : function() {
return this.length;
},
putBit : function(bit) {
var bufIndex = Math.floor(this.length / 8);
if (this.buffer.length <= bufIndex) {
this.buffer.push(0);
}
if (bit) {
this.buffer[bufIndex] |= (0x80 >>> (this.length % 8) );
}
this.length++;
}
};
// ---- QR8bitByte.js ----
function QR8bitByte(data) {
this.mode = QRMode.MODE_8BIT_BYTE;
this.data = data;
}
QR8bitByte.prototype = {
getLength : function() {
return this.data.length;
},
write : function(buffer) {
for (var i = 0; i < this.data.length; i++) {
// not JIS ...
buffer.put(this.data.charCodeAt(i), 8);
}
}
};
// ---- QRUtil.js ----
var QRUtil = {
PATTERN_POSITION_TABLE : [
[],
[6, 18],
[6, 22],
[6, 26],
[6, 30],
[6, 34],
[6, 22, 38],
[6, 24, 42],
[6, 26, 46],
[6, 28, 50],
[6, 30, 54],
[6, 32, 58],
[6, 34, 62],
[6, 26, 46, 66],
[6, 26, 48, 70],
[6, 26, 50, 74],
[6, 30, 54, 78],
[6, 30, 56, 82],
[6, 30, 58, 86],
[6, 34, 62, 90],
[6, 28, 50, 72, 94],
[6, 26, 50, 74, 98],
[6, 30, 54, 78, 102],
[6, 28, 54, 80, 106],
[6, 32, 58, 84, 110],
[6, 30, 58, 86, 114],
[6, 34, 62, 90, 118],
[6, 26, 50, 74, 98, 122],
[6, 30, 54, 78, 102, 126],
[6, 26, 52, 78, 104, 130],
[6, 30, 56, 82, 108, 134],
[6, 34, 60, 86, 112, 138],
[6, 30, 58, 86, 114, 142],
[6, 34, 62, 90, 118, 146],
[6, 30, 54, 78, 102, 126, 150],
[6, 24, 50, 76, 102, 128, 154],
[6, 28, 54, 80, 106, 132, 158],
[6, 32, 58, 84, 110, 136, 162],
[6, 26, 54, 82, 110, 138, 166],
[6, 30, 58, 86, 114, 142, 170]
],
G15 : (1 << 10) | (1 << 8) | (1 << 5) | (1 << 4) | (1 << 2) | (1 << 1) | (1 << 0),
G18 : (1 << 12) | (1 << 11) | (1 << 10) | (1 << 9) | (1 << 8) | (1 << 5) | (1 << 2) | (1 << 0),
G15_MASK : (1 << 14) | (1 << 12) | (1 << 10) | (1 << 4) | (1 << 1),
getBCHTypeInfo : function(data) {
var d = data << 10;
while (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G15) >= 0) {
d ^= (QRUtil.G15 << (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G15) ) );
}
return ( (data << 10) | d) ^ QRUtil.G15_MASK;
},
getBCHTypeNumber : function(data) {
var d = data << 12;
while (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G18) >= 0) {
d ^= (QRUtil.G18 << (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G18) ) );
}
return (data << 12) | d;
},
getBCHDigit : function(data) {
var digit = 0;
while (data !== 0) {
digit++;
data >>>= 1;
}
return digit;
},
getPatternPosition : function(typeNumber) {
return QRUtil.PATTERN_POSITION_TABLE[typeNumber - 1];
},
getMask : function(maskPattern, i, j) {
switch (maskPattern) {
case QRMaskPattern.PATTERN000 : return (i + j) % 2 === 0;
case QRMaskPattern.PATTERN001 : return i % 2 === 0;
case QRMaskPattern.PATTERN010 : return j % 3 === 0;
case QRMaskPattern.PATTERN011 : return (i + j) % 3 === 0;
case QRMaskPattern.PATTERN100 : return (Math.floor(i / 2) + Math.floor(j / 3) ) % 2 === 0;
case QRMaskPattern.PATTERN101 : return (i * j) % 2 + (i * j) % 3 === 0;
case QRMaskPattern.PATTERN110 : return ( (i * j) % 2 + (i * j) % 3) % 2 === 0;
case QRMaskPattern.PATTERN111 : return ( (i * j) % 3 + (i + j) % 2) % 2 === 0;
default :
throw new Error("bad maskPattern:" + maskPattern);
}
},
getErrorCorrectPolynomial : function(errorCorrectLength) {
var a = new QRPolynomial([1], 0);
for (var i = 0; i < errorCorrectLength; i++) {
a = a.multiply(new QRPolynomial([1, QRMath.gexp(i)], 0) );
}
return a;
},
getLengthInBits : function(mode, type) {
if (1 <= type && type < 10) {
// 1 - 9
switch(mode) {
case QRMode.MODE_NUMBER : return 10;
case QRMode.MODE_ALPHA_NUM : return 9;
case QRMode.MODE_8BIT_BYTE : return 8;
case QRMode.MODE_KANJI : return 8;
default :
throw new Error("mode:" + mode);
}
} else if (type < 27) {
// 10 - 26
switch(mode) {
case QRMode.MODE_NUMBER : return 12;
case QRMode.MODE_ALPHA_NUM : return 11;
case QRMode.MODE_8BIT_BYTE : return 16;
case QRMode.MODE_KANJI : return 10;
default :
throw new Error("mode:" + mode);
}
} else if (type < 41) {
// 27 - 40
switch(mode) {
case QRMode.MODE_NUMBER : return 14;
case QRMode.MODE_ALPHA_NUM : return 13;
case QRMode.MODE_8BIT_BYTE : return 16;
case QRMode.MODE_KANJI : return 12;
default :
throw new Error("mode:" + mode);
}
} else {
throw new Error("type:" + type);
}
},
getLostPoint : function(qrCode) {
var moduleCount = qrCode.getModuleCount();
var lostPoint = 0;
var row = 0;
var col = 0;
// LEVEL1
for (row = 0; row < moduleCount; row++) {
for (col = 0; col < moduleCount; col++) {
var sameCount = 0;
var dark = qrCode.isDark(row, col);
for (var r = -1; r <= 1; r++) {
if (row + r < 0 || moduleCount <= row + r) {
continue;
}
for (var c = -1; c <= 1; c++) {
if (col + c < 0 || moduleCount <= col + c) {
continue;
}
if (r === 0 && c === 0) {
continue;
}
if (dark === qrCode.isDark(row + r, col + c) ) {
sameCount++;
}
}
}
if (sameCount > 5) {
lostPoint += (3 + sameCount - 5);
}
}
}
// LEVEL2
for (row = 0; row < moduleCount - 1; row++) {
for (col = 0; col < moduleCount - 1; col++) {
var count = 0;
if (qrCode.isDark(row, col ) ) count++;
if (qrCode.isDark(row + 1, col ) ) count++;
if (qrCode.isDark(row, col + 1) ) count++;
if (qrCode.isDark(row + 1, col + 1) ) count++;
if (count === 0 || count === 4) {
lostPoint += 3;
}
}
}
// LEVEL3
for (row = 0; row < moduleCount; row++) {
for (col = 0; col < moduleCount - 6; col++) {
if (qrCode.isDark(row, col) &&
!qrCode.isDark(row, col + 1) &&
qrCode.isDark(row, col + 2) &&
qrCode.isDark(row, col + 3) &&
qrCode.isDark(row, col + 4) &&
!qrCode.isDark(row, col + 5) &&
qrCode.isDark(row, col + 6) ) {
lostPoint += 40;
}
}
}
for (col = 0; col < moduleCount; col++) {
for (row = 0; row < moduleCount - 6; row++) {
if (qrCode.isDark(row, col) &&
!qrCode.isDark(row + 1, col) &&
qrCode.isDark(row + 2, col) &&
qrCode.isDark(row + 3, col) &&
qrCode.isDark(row + 4, col) &&
!qrCode.isDark(row + 5, col) &&
qrCode.isDark(row + 6, col) ) {
lostPoint += 40;
}
}
}
// LEVEL4
var darkCount = 0;
for (col = 0; col < moduleCount; col++) {
for (row = 0; row < moduleCount; row++) {
if (qrCode.isDark(row, col) ) {
darkCount++;
}
}
}
var ratio = Math.abs(100 * darkCount / moduleCount / moduleCount - 50) / 5;
lostPoint += ratio * 10;
return lostPoint;
}
};
// ---- index.js ----
//---------------------------------------------------------------------
// QRCode for JavaScript
//
// Copyright (c) 2009 Kazuhiko Arase
//
// URL: http://www.d-project.com/
//
// Licensed under the MIT license:
// http://www.opensource.org/licenses/mit-license.php
//
// The word "QR Code" is registered trademark of
// DENSO WAVE INCORPORATED
// http://www.denso-wave.com/qrcode/faqpatent-e.html
//
//---------------------------------------------------------------------
// Modified to work in node for this project (and some refactoring)
//---------------------------------------------------------------------
function QRCode(typeNumber, errorCorrectLevel) {
this.typeNumber = typeNumber;
this.errorCorrectLevel = errorCorrectLevel;
this.modules = null;
this.moduleCount = 0;
this.dataCache = null;
this.dataList = [];
}
QRCode.prototype = {
addData : function(data) {
var newData = new QR8bitByte(data);
this.dataList.push(newData);
this.dataCache = null;
},
isDark : function(row, col) {
if (row < 0 || this.moduleCount <= row || col < 0 || this.moduleCount <= col) {
throw new Error(row + "," + col);
}
return this.modules[row][col];
},
getModuleCount : function() {
return this.moduleCount;
},
make : function() {
// Calculate automatically typeNumber if provided is < 1
if (this.typeNumber < 1 ){
var typeNumber = 1;
for (typeNumber = 1; typeNumber < 40; typeNumber++) {
var rsBlocks = QRRSBlock.getRSBlocks(typeNumber, this.errorCorrectLevel);
var buffer = new QRBitBuffer();
var totalDataCount = 0;
for (var i = 0; i < rsBlocks.length; i++) {
totalDataCount += rsBlocks[i].dataCount;
}
for (var x = 0; x < this.dataList.length; x++) {
var data = this.dataList[x];
buffer.put(data.mode, 4);
buffer.put(data.getLength(), QRUtil.getLengthInBits(data.mode, typeNumber) );
data.write(buffer);
}
if (buffer.getLengthInBits() <= totalDataCount * 8)
break;
}
this.typeNumber = typeNumber;
}
this.makeImpl(false, this.getBestMaskPattern() );
},
makeImpl : function(test, maskPattern) {
this.moduleCount = this.typeNumber * 4 + 17;
this.modules = new Array(this.moduleCount);
for (var row = 0; row < this.moduleCount; row++) {
this.modules[row] = new Array(this.moduleCount);
for (var col = 0; col < this.moduleCount; col++) {
this.modules[row][col] = null;//(col + row) % 3;
}
}
this.setupPositionProbePattern(0, 0);
this.setupPositionProbePattern(this.moduleCount - 7, 0);
this.setupPositionProbePattern(0, this.moduleCount - 7);
this.setupPositionAdjustPattern();
this.setupTimingPattern();
this.setupTypeInfo(test, maskPattern);
if (this.typeNumber >= 7) {
this.setupTypeNumber(test);
}
if (this.dataCache === null) {
this.dataCache = QRCode.createData(this.typeNumber, this.errorCorrectLevel, this.dataList);
}
this.mapData(this.dataCache, maskPattern);
},
setupPositionProbePattern : function(row, col) {
for (var r = -1; r <= 7; r++) {
if (row + r <= -1 || this.moduleCount <= row + r) continue;
for (var c = -1; c <= 7; c++) {
if (col + c <= -1 || this.moduleCount <= col + c) continue;
if ( (0 <= r && r <= 6 && (c === 0 || c === 6) ) ||
(0 <= c && c <= 6 && (r === 0 || r === 6) ) ||
(2 <= r && r <= 4 && 2 <= c && c <= 4) ) {
this.modules[row + r][col + c] = true;
} else {
this.modules[row + r][col + c] = false;
}
}
}
},
getBestMaskPattern : function() {
var minLostPoint = 0;
var pattern = 0;
for (var i = 0; i < 8; i++) {
this.makeImpl(true, i);
var lostPoint = QRUtil.getLostPoint(this);
if (i === 0 || minLostPoint > lostPoint) {
minLostPoint = lostPoint;
pattern = i;
}
}
return pattern;
},
createMovieClip : function(target_mc, instance_name, depth) {
var qr_mc = target_mc.createEmptyMovieClip(instance_name, depth);
var cs = 1;
this.make();
for (var row = 0; row < this.modules.length; row++) {
var y = row * cs;
for (var col = 0; col < this.modules[row].length; col++) {
var x = col * cs;
var dark = this.modules[row][col];
if (dark) {
qr_mc.beginFill(0, 100);
qr_mc.moveTo(x, y);
qr_mc.lineTo(x + cs, y);
qr_mc.lineTo(x + cs, y + cs);
qr_mc.lineTo(x, y + cs);
qr_mc.endFill();
}
}
}
return qr_mc;
},
setupTimingPattern : function() {
for (var r = 8; r < this.moduleCount - 8; r++) {
if (this.modules[r][6] !== null) {
continue;
}
this.modules[r][6] = (r % 2 === 0);
}
for (var c = 8; c < this.moduleCount - 8; c++) {
if (this.modules[6][c] !== null) {
continue;
}
this.modules[6][c] = (c % 2 === 0);
}
},
setupPositionAdjustPattern : function() {
var pos = QRUtil.getPatternPosition(this.typeNumber);
for (var i = 0; i < pos.length; i++) {
for (var j = 0; j < pos.length; j++) {
var row = pos[i];
var col = pos[j];
if (this.modules[row][col] !== null) {
continue;
}
for (var r = -2; r <= 2; r++) {
for (var c = -2; c <= 2; c++) {
if (Math.abs(r) === 2 ||
Math.abs(c) === 2 ||
(r === 0 && c === 0) ) {
this.modules[row + r][col + c] = true;
} else {
this.modules[row + r][col + c] = false;
}
}
}
}
}
},
setupTypeNumber : function(test) {
var bits = QRUtil.getBCHTypeNumber(this.typeNumber);
var mod;
for (var i = 0; i < 18; i++) {
mod = (!test && ( (bits >> i) & 1) === 1);
this.modules[Math.floor(i / 3)][i % 3 + this.moduleCount - 8 - 3] = mod;
}
for (var x = 0; x < 18; x++) {
mod = (!test && ( (bits >> x) & 1) === 1);
this.modules[x % 3 + this.moduleCount - 8 - 3][Math.floor(x / 3)] = mod;
}
},
setupTypeInfo : function(test, maskPattern) {
var data = (this.errorCorrectLevel << 3) | maskPattern;
var bits = QRUtil.getBCHTypeInfo(data);
var mod;
// vertical
for (var v = 0; v < 15; v++) {
mod = (!test && ( (bits >> v) & 1) === 1);
if (v < 6) {
this.modules[v][8] = mod;
} else if (v < 8) {
this.modules[v + 1][8] = mod;
} else {
this.modules[this.moduleCount - 15 + v][8] = mod;
}
}
// horizontal
for (var h = 0; h < 15; h++) {
mod = (!test && ( (bits >> h) & 1) === 1);
if (h < 8) {
this.modules[8][this.moduleCount - h - 1] = mod;
} else if (h < 9) {
this.modules[8][15 - h - 1 + 1] = mod;
} else {
this.modules[8][15 - h - 1] = mod;
}
}
// fixed module
this.modules[this.moduleCount - 8][8] = (!test);
},
mapData : function(data, maskPattern) {
var inc = -1;
var row = this.moduleCount - 1;
var bitIndex = 7;
var byteIndex = 0;
for (var col = this.moduleCount - 1; col > 0; col -= 2) {
if (col === 6) col--;
while (true) {
for (var c = 0; c < 2; c++) {
if (this.modules[row][col - c] === null) {
var dark = false;
if (byteIndex < data.length) {
dark = ( ( (data[byteIndex] >>> bitIndex) & 1) === 1);
}
var mask = QRUtil.getMask(maskPattern, row, col - c);
if (mask) {
dark = !dark;
}
this.modules[row][col - c] = dark;
bitIndex--;
if (bitIndex === -1) {
byteIndex++;
bitIndex = 7;
}
}
}
row += inc;
if (row < 0 || this.moduleCount <= row) {
row -= inc;
inc = -inc;
break;
}
}
}
}
};
QRCode.PAD0 = 0xEC;
QRCode.PAD1 = 0x11;
QRCode.createData = function(typeNumber, errorCorrectLevel, dataList) {
var rsBlocks = QRRSBlock.getRSBlocks(typeNumber, errorCorrectLevel);
var buffer = new QRBitBuffer();
for (var i = 0; i < dataList.length; i++) {
var data = dataList[i];
buffer.put(data.mode, 4);
buffer.put(data.getLength(), QRUtil.getLengthInBits(data.mode, typeNumber) );
data.write(buffer);
}
// calc num max data.
var totalDataCount = 0;
for (var x = 0; x < rsBlocks.length; x++) {
totalDataCount += rsBlocks[x].dataCount;
}
if (buffer.getLengthInBits() > totalDataCount * 8) {
throw new Error("code length overflow. (" +
buffer.getLengthInBits() +
">" +
totalDataCount * 8 +
")");
}
// end code
if (buffer.getLengthInBits() + 4 <= totalDataCount * 8) {
buffer.put(0, 4);
}
// padding
while (buffer.getLengthInBits() % 8 !== 0) {
buffer.putBit(false);
}
// padding
while (true) {
if (buffer.getLengthInBits() >= totalDataCount * 8) {
break;
}
buffer.put(QRCode.PAD0, 8);
if (buffer.getLengthInBits() >= totalDataCount * 8) {
break;
}
buffer.put(QRCode.PAD1, 8);
}
return QRCode.createBytes(buffer, rsBlocks);
};
QRCode.createBytes = function(buffer, rsBlocks) {
var offset = 0;
var maxDcCount = 0;
var maxEcCount = 0;
var dcdata = new Array(rsBlocks.length);
var ecdata = new Array(rsBlocks.length);
for (var r = 0; r < rsBlocks.length; r++) {
var dcCount = rsBlocks[r].dataCount;
var ecCount = rsBlocks[r].totalCount - dcCount;
maxDcCount = Math.max(maxDcCount, dcCount);
maxEcCount = Math.max(maxEcCount, ecCount);
dcdata[r] = new Array(dcCount);
for (var i = 0; i < dcdata[r].length; i++) {
dcdata[r][i] = 0xff & buffer.buffer[i + offset];
}
offset += dcCount;
var rsPoly = QRUtil.getErrorCorrectPolynomial(ecCount);
var rawPoly = new QRPolynomial(dcdata[r], rsPoly.getLength() - 1);
var modPoly = rawPoly.mod(rsPoly);
ecdata[r] = new Array(rsPoly.getLength() - 1);
for (var x = 0; x < ecdata[r].length; x++) {
var modIndex = x + modPoly.getLength() - ecdata[r].length;
ecdata[r][x] = (modIndex >= 0)? modPoly.get(modIndex) : 0;
}
}
var totalCodeCount = 0;
for (var y = 0; y < rsBlocks.length; y++) {
totalCodeCount += rsBlocks[y].totalCount;
}
var data = new Array(totalCodeCount);
var index = 0;
for (var z = 0; z < maxDcCount; z++) {
for (var s = 0; s < rsBlocks.length; s++) {
if (z < dcdata[s].length) {
data[index++] = dcdata[s][z];
}
}
}
for (var xx = 0; xx < maxEcCount; xx++) {
for (var t = 0; t < rsBlocks.length; t++) {
if (xx < ecdata[t].length) {
data[index++] = ecdata[t][xx];
}
}
}
return data;
};
function renderToCanvas(canvas, text, size, margin){
size = Math.max(64, Number(size || 128)); margin = Math.max(0, Number(margin == null ? 4 : margin));
var qr = new QRCode(0, QRErrorCorrectLevel.M);
qr.addData(String(text || ''));
qr.make();
var count = qr.getModuleCount();
var scale = Math.max(1, Math.floor((size - margin * 2) / count));
var actual = count * scale + margin * 2;
canvas.width = actual; canvas.height = actual;
var ctx = canvas.getContext('2d');
ctx.fillStyle = '#ffffff'; ctx.fillRect(0, 0, actual, actual);
ctx.fillStyle = '#111111';
for (var r=0;r<count;r++) for(var c=0;c<count;c++) if(qr.isDark(r,c)) ctx.fillRect(margin+c*scale,margin+r*scale,scale,scale);
return canvas;
}
window.SunQR = {
toCanvas: renderToCanvas,
toDataURL: function(text,size,margin){var c=document.createElement('canvas');renderToCanvas(c,text,size,margin);return c.toDataURL('image/png');}
};
})();
/* END qr-lite.js */
</script><script>
/* BEGIN enterprise-update.js — PORTABLE INLINE */
/* Sun Catering enterprise extension: backup, roles, sidebar, search, finance, notifications, print, routes and Excel */
(() => {
'use strict';
if (window.SunEnterprise) return;
const VERSION = 'v17.6.0 · 2026.09.07';
const SETTINGS_KEY = 'sunEnterpriseSettingsV1';
const USERS_KEY = 'sunUsersV1';
const AUDIT_KEY = 'sunAuditLogV1';
const READ_NOTIFICATIONS_KEY = 'sunReadNotificationsV1';
const PRINT_SETTINGS_KEY = 'sunPrintSettingsV1';
const ROUTES_KEY = 'sunRoutePlansV1';
const AUTOSAVE_KEY = 'sunOrderDraftAutosaveV1';
const SESSION_KEY = 'sunSessionUserV1';
const $ = id => document.getElementById(id);
const qs = (sel, root = document) => root.querySelector(sel);
const qsa = (sel, root = document) => [...root.querySelectorAll(sel)];
const esc=window.SunSafe.escapeHTML;
const moneyFmt = value => `${Number(value || 0).toLocaleString('ru-RU', {maximumFractionDigits: 2})} ₽`;
const numFmt = value => Number(value || 0).toLocaleString('ru-RU', {maximumFractionDigits: 3});
const todayKey = () => new Date().toISOString().slice(0, 10);
const safeJson = (raw, fallback) => { try { const parsed = JSON.parse(raw); return parsed ?? fallback; } catch (_) { return fallback; } };
const clone = value => typeof structuredClone === 'function' ? structuredClone(value) : JSON.parse(JSON.stringify(value));
const uid = () => typeof window.sunUUID === 'function' ? window.sunUUID() : `sun-${Date.now()}-${Math.random().toString(36).slice(2)}`;
const appOrders = () => { try { return Array.isArray(orders) ? orders : []; } catch (_) { return safeJson(localStorage.sunOrders, []); } };
const appBoxes = () => { try { return Array.isArray(boxes) ? boxes : []; } catch (_) { return safeJson(localStorage.sunBoxes, []); } };
const appDraft = () => { try { return draft; } catch (_) { return {}; } };
const activeOrder = order => !['Отменён', 'Отдан заказчику'].includes(order?.status || 'Новый');
const orderTotal = order => {
if (Number.isFinite(Number(order?.total))) return Number(order.total || 0);
return (order?.lines || []).reduce((sum, line) => {
const item = appBoxes().find(x => String(x.id) === String(line.id));
return sum + Number(item?.price || 0) * Number(line.qty || 0);
}, 0);
};
const paymentTotal = order => Math.max(0, Number(order?.prepayment || 0));
const dateLabel = raw => {
if (!raw) return '—';
const d = new Date(`${raw}T00:00:00`);
return Number.isNaN(d.getTime()) ? raw : d.toLocaleDateString('ru-RU');
};
const getEmployees = () => safeJson(localStorage.getItem('sunEmployees') || '[]', []);
const getSuppliers = () => safeJson(localStorage.getItem('sunSuppliers') || '[]', []);
const getStock = () => safeJson(localStorage.getItem('sunStock') || '[]', []);
const getFinance = () => safeJson(localStorage.getItem('sunFinanceRecordsV2') || '[]', []);
const defaultSettings = {
sidebarCollapsed: false,
rolesEnabled: false,
autoBackup: true,
browserNotifications: false,
cloud: { enabled: false, supabaseUrl: '', anonKey: '', workspaceId: 'sun-catering-main', autoSync: false, lastSync: '' }
};
let settings = Object.assign({}, defaultSettings, safeJson(localStorage.getItem(SETTINGS_KEY) || '{}', {}));
settings.cloud = Object.assign({}, defaultSettings.cloud, settings.cloud || {});
const saveSettings = () => localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
// ---------- Toasts ----------
const toastHost = document.createElement('div');
toastHost.id = 'sunToastHost';
document.body.appendChild(toastHost);
function toast(text, type = 'info', timeout = 3500) {
const node = document.createElement('div');
node.className = `sun-toast ${type}`;
node.innerHTML = `<span>${esc(text)}</span><button type="button" aria-label="Закрыть">×</button>`;
node.querySelector('button').onclick = () => node.remove();
toastHost.appendChild(node);
requestAnimationFrame(() => node.classList.add('show'));
setTimeout(() => { node.classList.remove('show'); setTimeout(() => node.remove(), 220); }, timeout);
}
// ---------- Common styles / desktop sidebar ----------
const style = document.createElement('style');
style.id = 'sun-enterprise-style';
style.textContent = `
:root{--sun-side:274px;--sun-side-collapsed:74px}
#sunToastHost{position:fixed;right:18px;bottom:18px;z-index:5000;display:grid;gap:8px;max-width:min(390px,calc(100vw - 36px))}
.sun-toast{background:#15364c;color:#fff;border-radius:9px;padding:11px 12px;box-shadow:0 8px 30px #102c3d4d;display:flex;align-items:center;gap:12px;opacity:0;transform:translateY(8px);transition:.2s}.sun-toast.show{opacity:1;transform:none}.sun-toast.success{background:#347154}.sun-toast.warn{background:#986d2f}.sun-toast.error{background:#943d3d}.sun-toast button{margin-left:auto;color:inherit;border:0;background:transparent;font-size:20px}
.sun-side-tools{display:grid;gap:7px;width:100%;margin:2px 0 4px}.sun-side-action{width:100%;min-height:40px;border:1px solid #ffffff1f;background:#ffffff0d;color:#e8f0f3;border-radius:9px;padding:8px 10px;text-align:left;font-weight:700;display:flex;align-items:center;gap:8px}.sun-side-action:hover{background:#ffffff19}.sun-side-action .badge{margin-left:auto;min-width:20px;height:20px;padding:0 6px;border-radius:999px;background:#d76a56;color:#fff;display:grid;place-items:center;font-size:11px}.sun-side-action .badge:empty{display:none}.sun-current-user{font-size:12px;color:#adc0cc;padding:4px 4px 0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.sun-side-footer{margin-top:auto;width:100%;display:grid;gap:6px;padding-top:9px;border-top:1px solid #ffffff18}
body.sun-enterprise-sidebar{padding-left:var(--sun-side);transition:padding-left .2s ease}.sun-enterprise-sidebar header{position:fixed!important;left:0;top:0;bottom:0;width:var(--sun-side);height:100vh!important;min-height:0!important;padding:14px 12px!important;display:flex!important;flex-direction:column!important;align-items:stretch!important;gap:10px!important;overflow-y:auto;overflow-x:hidden;z-index:900;transition:width .2s ease}.sun-enterprise-sidebar header .brand{font-size:20px!important;padding:2px 5px 10px;justify-content:flex-start}.sun-enterprise-sidebar header .brand .real-logo{flex:0 0 auto}.sun-enterprise-sidebar header nav{display:flex!important;flex-direction:column!important;gap:8px!important;overflow:visible!important;width:100%;white-space:normal!important}.sun-enterprise-sidebar .sun-nav-group{display:flex!important;flex-direction:column!important;gap:3px!important;padding:22px 4px 5px!important;min-width:0!important;background:#ffffff08!important}.sun-enterprise-sidebar header nav .sun-nav-button{width:100%!important;min-height:40px!important;height:auto!important;justify-content:flex-start!important;padding:9px 10px!important;font-size:12.5px!important;border-radius:8px!important;white-space:normal!important;text-align:left!important;transform:none!important}.sun-enterprise-sidebar header nav .sun-nav-button::before{margin-right:9px!important}.sun-enterprise-sidebar header nav .sun-nav-button.on::after{left:9px!important;right:auto!important;top:8px!important;bottom:8px!important;width:3px!important;height:auto!important}.sun-enterprise-sidebar header nav .sun-nav-button.on{padding-left:15px!important}.sun-enterprise-sidebar header nav .nav-print::before{background:#d18d55}.sun-enterprise-sidebar header nav .nav-routes::before{background:#5c9f8c}.sun-enterprise-sidebar header nav .nav-settings::before{background:#9d8d76}
body.sun-side-collapsed{padding-left:var(--sun-side-collapsed)}.sun-side-collapsed header{width:var(--sun-side-collapsed)}.sun-side-collapsed header .brand{justify-content:center;font-size:0!important}.sun-side-collapsed header .brand .real-logo{margin:0!important;width:48px!important;height:48px!important}.sun-side-collapsed .sun-nav-group{padding-top:13px!important;background:transparent!important;border:0!important}.sun-side-collapsed .sun-nav-group::before,.sun-side-collapsed .sun-nav-button{font-size:0!important}.sun-side-collapsed header nav .sun-nav-button{justify-content:center!important;padding:10px!important;min-height:44px!important}.sun-side-collapsed header nav .sun-nav-button::before{width:10px!important;height:10px!important;margin:0!important}.sun-side-collapsed .sun-side-action{justify-content:center;font-size:0;padding:10px}.sun-side-collapsed .sun-side-action .badge{font-size:10px;position:absolute;margin:0 0 25px 25px}.sun-side-collapsed .sun-current-user{display:none}.sun-side-collapsed .sun-side-footer{border:0}.sun-sidebar-collapse{font-size:12px!important}.sun-side-collapsed .sun-sidebar-collapse::after{content:'';font-size:24px}.sun-enterprise-sidebar:not(.sun-side-collapsed) .sun-sidebar-collapse::after{content:'';font-size:24px;margin-left:auto}
.sun-global-search{width:min(920px,calc(100vw - 36px));max-height:86vh}.sun-search-input{width:100%;font-size:18px;padding:13px 14px;border-radius:9px;border:1px solid #ccd4d9}.sun-search-results{margin-top:12px;display:grid;gap:12px}.sun-search-group h3{margin:0 0 7px;color:#15364c;font-size:14px;text-transform:uppercase;letter-spacing:.04em}.sun-search-item{width:100%;border:1px solid #d9dde0;border-radius:8px;background:#fff;padding:10px 11px;text-align:left;display:grid;grid-template-columns:1fr auto;gap:8px;align-items:center}.sun-search-item:hover{border-color:#c99a32;background:#fffaf0}.sun-search-item b{display:block;color:#15364c}.sun-search-item small{display:block;color:#68717a;margin-top:3px}.sun-search-empty{padding:24px;text-align:center;color:#879198}
.enterprise-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin-top:15px}.enterprise-card{border:1px solid #d9dde0;border-radius:11px;background:#fff;padding:15px;min-width:0}.enterprise-card.wide{grid-column:1/-1}.enterprise-card h2{font-size:18px;color:#15364c;margin:0 0 6px}.enterprise-card h3{font-size:15px;color:#15364c;margin:15px 0 7px}.enterprise-card .actions{flex-wrap:wrap}.enterprise-card .status{padding:9px 11px;border-radius:8px;background:#f4f6f7;color:#596a74;margin:9px 0}.enterprise-card .status.good{background:#eaf6ed;color:#347154}.enterprise-card .status.bad{background:#fbeeee;color:#943d3d}.enterprise-fields{display:grid;grid-template-columns:1fr 1fr;gap:9px}.enterprise-fields .wide{grid-column:1/-1}.backup-list,.audit-list,.users-list{display:grid;gap:7px;margin-top:10px}.backup-row,.audit-row,.user-row{border:1px solid #e2e6e8;border-radius:8px;padding:9px 10px;display:grid;grid-template-columns:1fr auto;gap:10px;align-items:center}.audit-row{grid-template-columns:150px 120px 1fr}.backup-row small,.audit-row small,.user-row small{color:#68717a}.role-chip{display:inline-flex;padding:3px 7px;border-radius:999px;background:#eef2f4;color:#53636e;font-size:11px;font-weight:700}
.sun-login-overlay{position:fixed;inset:0;background:linear-gradient(145deg,#102c3d,#15364c);z-index:10000;display:grid;place-items:center;padding:18px}.sun-login-card{width:min(420px,100%);background:#fff;border-radius:16px;padding:24px;box-shadow:0 24px 70px #0006}.sun-login-brand{display:flex;align-items:center;gap:12px;margin-bottom:18px}.sun-login-brand img{width:60px;height:60px;object-fit:contain}.sun-login-brand h1{margin:0;color:#15364c;font-size:22px}.sun-login-card label{margin-top:10px}.sun-login-card .error{color:#9c3d3d;min-height:20px;margin-top:8px}
.autosave-banner{margin:12px 14px 0;border:1px solid #d9c58c;background:#fff9e7;border-radius:9px;padding:10px 12px;display:flex;gap:10px;align-items:center;flex-wrap:wrap}.autosave-banner b{color:#6e5924}.autosave-banner .actions{margin:0 0 0 auto}
/* Premium sidebar concept — visual experiment. Existing logo asset is intentionally reused everywhere. */
@media(min-width:901px){
:root{--sun-side:286px;--sun-side-collapsed:74px}
body.sun-enterprise-sidebar{background:#f6f7f8}
.sun-enterprise-sidebar header{background:linear-gradient(180deg,#102634 0%,#0d202c 100%)!important;padding:18px 14px 14px!important;gap:9px!important;box-shadow:8px 0 28px #0d243018}
.sun-enterprise-sidebar header .brand{min-height:78px;padding:7px 8px 14px!important;font-size:18px!important;line-height:1.15;letter-spacing:.02em;border-bottom:1px solid #ffffff14;display:flex!important;align-items:center!important}
.sun-enterprise-sidebar header .brand .real-logo{width:58px!important;height:58px!important;margin-right:12px!important;filter:none!important}
.sun-enterprise-sidebar header nav{gap:4px!important;margin-top:2px}
.sun-enterprise-sidebar .sun-nav-home-standalone{margin:4px 4px 7px!important;background:#ffffff0b!important;border:1px solid #ffffff10!important}
.sun-enterprise-sidebar .sun-nav-group{position:relative!important;display:flex!important;flex-direction:column!important;gap:2px!important;margin:0 4px!important;padding:31px 0 10px!important;background:transparent!important;border:0!important;border-bottom:1px solid #ffffff14!important;border-radius:0!important}
.sun-enterprise-sidebar .sun-nav-group::before{content:attr(data-label)!important;position:absolute!important;left:8px!important;top:11px!important;color:#91a5b2!important;font-size:10px!important;font-weight:800!important;letter-spacing:.10em!important;text-transform:uppercase!important}
.sun-enterprise-sidebar .sun-nav-group::after{content:'⌃';position:absolute;right:9px;top:7px;color:#d8ad50;font-size:15px;opacity:.9}
.sun-enterprise-sidebar .sun-nav-work::before{content:'ЗАКАЗЫ'!important}
.sun-enterprise-sidebar .sun-nav-operations::before{content:'КУХНЯ И ЛОГИСТИКА'!important}
.sun-enterprise-sidebar .sun-nav-management::before{content:'УПРАВЛЕНИЕ'!important}
.sun-enterprise-sidebar header nav .sun-nav-button{position:relative!important;min-height:42px!important;padding:9px 10px!important;border-radius:10px!important;color:#e6eef2!important;font-size:13.5px!important;font-weight:650!important;letter-spacing:0!important;transition:background .15s ease,color .15s ease,transform .15s ease!important}
.sun-enterprise-sidebar header nav .sun-nav-button:hover{background:#ffffff10!important;color:#fff!important;transform:none!important}
.sun-enterprise-sidebar header nav .sun-nav-button.on{background:#ffffff12!important;color:#ffd56a!important;box-shadow:inset 0 0 0 1px #ffffff08!important;padding-left:10px!important}
.sun-enterprise-sidebar header nav .sun-nav-button.on::after{content:''!important;position:absolute!important;left:-4px!important;top:8px!important;bottom:8px!important;width:3px!important;height:auto!important;background:#e2b64f!important;border-radius:4px!important}
.sun-enterprise-sidebar header nav .sun-nav-button::before{display:inline-grid!important;place-items:center!important;width:24px!important;height:24px!important;margin-right:9px!important;border-radius:7px!important;background:transparent!important;color:#cbd8df!important;font-size:18px!important;line-height:1!important;opacity:1!important;flex:0 0 24px!important}
.sun-enterprise-sidebar header nav .nav-home::before{content:'⌂'!important;color:#f2c85c!important}
.sun-enterprise-sidebar header nav .nav-new::before{content:''!important}
.sun-enterprise-sidebar header nav .nav-orders::before{content:'▣'!important}
.sun-enterprise-sidebar header nav .nav-calendar::before{content:'▦'!important}
.sun-enterprise-sidebar header nav .nav-map::before{content:'⌖'!important}
.sun-enterprise-sidebar header nav .nav-print::before{content:'▤'!important;color:#d7dee3!important}
.sun-enterprise-sidebar header nav .nav-production::before{content:'≡'!important}
.sun-enterprise-sidebar header nav .nav-prep::before{content:'✓'!important}
.sun-enterprise-sidebar header nav .nav-shopping::before{content:'⌑'!important}
.sun-enterprise-sidebar header nav .nav-stock::before{content:'□'!important}
.sun-enterprise-sidebar header nav .nav-routes::before{content:'⌁'!important}
.sun-enterprise-sidebar header nav .nav-clients::before{content:'♙'!important}
.sun-enterprise-sidebar header nav .nav-money::before{content:'₽'!important}
.sun-enterprise-sidebar header nav .nav-team::before{content:'♟'!important}
.sun-enterprise-sidebar header nav .nav-suppliers::before{content:'▧'!important}
.sun-enterprise-sidebar header nav .nav-stats::before{content:'⌁'!important}
.sun-enterprise-sidebar header nav .nav-settings::before{content:'⚙'!important;font-size:16px!important}
.sun-side-tools{grid-template-columns:1fr auto;gap:6px!important;margin:4px 4px 2px!important;width:auto!important}
.sun-side-action{min-height:38px!important;border-color:#ffffff13!important;background:#ffffff08!important;border-radius:10px!important;color:#d9e4e9!important;font-size:12px!important}
#sunNotificationsBtn{width:44px!important;padding:8px!important;justify-content:center!important;position:relative}
#sunNotificationsBtn>span:nth-child(2){display:none}
#sunNotificationsBtn>span:first-child{font-size:0}
#sunNotificationsBtn>span:first-child::before{content:'●';font-size:17px;color:#dce8ec}
#sunNotificationsBtn .badge{position:absolute;right:-3px;top:-5px;margin:0!important;background:#e5533f!important}
#sunGlobalSearchBtn kbd{display:none}
.sun-current-user{grid-column:1/-1;padding:4px 8px 1px!important;color:#8fa5b2!important}
.sun-side-footer{margin:4px!important;width:auto!important;padding:9px 0 0!important}
.sun-side-collapsed .sun-nav-home-standalone{margin-inline:0!important}
.sun-side-collapsed .sun-nav-group{margin-inline:0!important;padding-top:8px!important;border-bottom:0!important}
.sun-side-collapsed .sun-nav-group::after{display:none}
.sun-side-collapsed header .brand{min-height:68px!important;border-bottom:0!important}
.sun-side-collapsed header .brand .real-logo{width:50px!important;height:50px!important}
}
@media(max-width:900px){body.sun-enterprise-sidebar,body.sun-side-collapsed{padding-left:0;padding-bottom:76px}.sun-enterprise-sidebar header{position:relative!important;width:auto!important;height:auto!important;min-height:64px!important;padding:9px 14px 10px!important;overflow:visible!important;display:flex!important;flex-direction:column!important}.sun-enterprise-sidebar header .brand{font-size:20px!important;padding:0;justify-content:flex-start}.sun-enterprise-sidebar header .brand .real-logo{width:42px!important;height:42px!important;margin-right:9px!important}.sun-side-tools{display:flex;gap:6px;overflow:auto}.sun-side-action{min-width:45px;width:auto;font-size:0;justify-content:center}.sun-side-action .badge{font-size:10px;position:absolute;margin:-27px -28px 0 0}.sun-current-user{display:none}.sun-side-footer{display:none}.sun-enterprise-sidebar header nav{position:fixed!important;left:0;right:0;bottom:0;z-index:100;display:flex!important;flex-direction:row!important;overflow-x:auto!important;width:auto!important}.sun-enterprise-sidebar .sun-nav-group{display:contents!important}.enterprise-grid{grid-template-columns:1fr}.enterprise-card.wide{grid-column:auto}.enterprise-fields{grid-template-columns:1fr}.enterprise-fields .wide{grid-column:auto}.audit-row{grid-template-columns:1fr}.sun-sidebar-collapse{display:none}.autosave-banner{margin:8px}.autosave-banner .actions{margin-left:0}}
@media(max-width:900px){
#sunGlobalSearchBtn>span:first-child,#sunNotificationsBtn>span:first-child{font-size:0!important;display:block!important;width:19px!important;height:19px!important;min-width:19px!important;background:currentColor!important;-webkit-mask-repeat:no-repeat!important;mask-repeat:no-repeat!important;-webkit-mask-position:center!important;mask-position:center!important;-webkit-mask-size:19px 19px!important;mask-size:19px 19px!important}
#sunGlobalSearchBtn>span:first-child{-webkit-mask-image:url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%3E%3Ccircle%20cx%3D%2211%22%20cy%3D%2211%22%20r%3D%227%22%2F%3E%3Cpath%20d%3D%22m20%2020-4-4%22%2F%3E%3C%2Fsvg%3E")!important;mask-image:url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%3E%3Ccircle%20cx%3D%2211%22%20cy%3D%2211%22%20r%3D%227%22%2F%3E%3Cpath%20d%3D%22m20%2020-4-4%22%2F%3E%3C%2Fsvg%3E")!important}
#sunNotificationsBtn>span:first-child{-webkit-mask-image:url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%221.9%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M18%208a6%206%200%200%200-12%200c0%207-3%207-3%209h18c0-2-3-2-3-9Z%22%2F%3E%3Cpath%20d%3D%22M10%2021h4%22%2F%3E%3C%2Fsvg%3E")!important;mask-image:url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%221.9%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M18%208a6%206%200%200%200-12%200c0%207-3%207-3%209h18c0-2-3-2-3-9Z%22%2F%3E%3Cpath%20d%3D%22M10%2021h4%22%2F%3E%3C%2Fsvg%3E")!important}
#sunGlobalSearchBtn>span:nth-child(2),#sunGlobalSearchBtn kbd,#sunNotificationsBtn>span:nth-child(2){display:none!important}
#sunNotificationsBtn .badge{position:absolute!important;right:-4px!important;top:-6px!important;margin:0!important;min-width:16px!important;height:16px!important;padding:0 4px!important;font-size:9px!important}
#new .catalog-head{align-items:center!important}
#new .catalog-head h1{flex:1 1 100%;order:-1}
#new .sun-catalog-search{flex:1 1 100%;order:0;min-width:0;width:auto}
#new .catalog-head button{flex:1 1 0;order:1;min-width:0;padding-inline:8px;white-space:nowrap}
}
@media print{.sun-side-tools,.sun-side-footer,#sunToastHost{display:none!important}}
`;
document.head.appendChild(style);
document.body.classList.add('sun-enterprise-sidebar');
if (settings.sidebarCollapsed) document.body.classList.add('sun-side-collapsed');
// ---------- Audit ----------
let auditLog = safeJson(localStorage.getItem(AUDIT_KEY) || '[]', []);
function currentUser() {
try {
const ws=window.SunCloudV2?.getWorkspace?.(), ss=window.SunCloudV2?.getSession?.();
if(ws&&ss?.user)return {id:ss.user.id,name:ws.display_name||ss.user.email||'Пользователь',role:ws.role||'viewer'};
} catch (_) {}
const id = sessionStorage.getItem(SESSION_KEY);
const users = safeJson(localStorage.getItem(USERS_KEY) || '[]', []);
return users.find(u => String(u.id) === String(id)) || { id: 'system', name: 'Система', role: 'viewer' };
}
function recordAudit(action, details = '') {
auditLog.unshift({ id: uid(), at: new Date().toISOString(), user: currentUser().name || 'Система', action, details: String(details || '') });
auditLog = auditLog.slice(0, 500);
localStorage.setItem(AUDIT_KEY, JSON.stringify(auditLog));
if ($('auditList')) renderAudit();
}
// ---------- Header tools ----------
const header = qs('header');
const nav = qs('header nav');
const sideTools = document.createElement('div');
sideTools.className = 'sun-side-tools';
sideTools.innerHTML = `
<button class="sun-side-action" id="sunGlobalSearchBtn" type="button" title="Поиск по приложению"><span>⌕</span><span>Поиск</span><kbd style="margin-left:auto;font-size:10px;opacity:.65">Ctrl K</kbd></button>
<button class="sun-side-action" id="sunNotificationsBtn" type="button" title="Уведомления"><span>●</span><span>Уведомления</span><span class="badge" id="sunNotificationBadge"></span></button>
<div class="sun-current-user" id="sunCurrentUserLabel"></div>`;
window.SunSafe.insertBefore(header,sideTools,nav);
const sideFooter = document.createElement('div');
sideFooter.className = 'sun-side-footer';
sideFooter.innerHTML = `<button class="sun-side-action sun-sidebar-collapse" id="sunSidebarCollapse" type="button"><span>Свернуть меню</span></button><button class="sun-side-action" id="sunLogoutBtn" type="button" style="display:none"><span>↪</span><span>Выйти</span></button>`;
header.appendChild(sideFooter);
$('sunSidebarCollapse').onclick = () => {
document.body.classList.toggle('sun-side-collapsed');
settings.sidebarCollapsed = document.body.classList.contains('sun-side-collapsed');
saveSettings();
};
function openView(view, button) {
qsa('.view').forEach(v => v.classList.remove('on'));
view.classList.add('on');
qsa('header nav button').forEach(b => b.classList.remove('on'));
if (button) button.classList.add('on');
window.scrollTo({top: 0, behavior: 'instant'});
}
function navButtonByLabel(label) { return qsa('header nav button').find(b => b.textContent.trim() === label); }
function activateNav(label) { const b = navButtonByLabel(label); if (b) b.click(); }
function navGroup(name) { return qs(`.sun-nav-${name}`, nav) || nav; }
function makeNavButton(label, group, cls, view, onRender) {
const button = document.createElement('button');
button.type = 'button'; button.textContent = label; button.dataset.navLabel = label;
button.className = `sun-nav-button ${cls || ''}`;
navGroup(group).appendChild(button);
button.onclick = () => { openView(view, button); if (typeof onRender === 'function') onRender(); };
return button;
}
// ---------- Global search ----------
const searchModal = document.createElement('div');
searchModal.id = 'sun-global-search-modal'; searchModal.className = 'modal';
searchModal.innerHTML = `<div class="dialog sun-global-search"><div class="dialog-head"><h2>Поиск по приложению</h2><button type="button" onclick="closeModal('sun-global-search-modal')">×</button></div><input class="sun-search-input" id="sunGlobalSearchInput" type="search" placeholder="Заказ, клиент, адрес, бокс, продукт, сотрудник или поставщик"><div class="sun-search-results" id="sunGlobalSearchResults"><div class="sun-search-empty">Начните вводить запрос.</div></div></div>`;
document.body.appendChild(searchModal);
function clientIndex() {
const map = new Map();
appOrders().forEach(o => {
const key = String(o.phone || o.contact || '').trim(); if (!key) return;
if (!map.has(key)) map.set(key, { key, name: o.contact || 'Без имени', phone: o.phone || '', addresses: new Set(), orders: [] });
const c = map.get(key); if (o.address) c.addresses.add(o.address); c.orders.push(o);
});
return [...map.values()].map(c => ({...c, addresses: [...c.addresses]}));
}
function searchData(query) {
const q = query.trim().toLowerCase(); if (!q) return [];
const groups = [];
const orderMatches = appOrders().filter(o => [o.id, o.event, o.contact, o.phone, o.address, o.note].join(' ').toLowerCase().includes(q)).slice(0, 12).map(o => ({ title: `Заказ №${o.id} · ${o.event || 'Без названия'}`, meta: `${o.date || '—'} ${o.time || ''} · ${o.contact || 'клиент не указан'} · ${moneyFmt(orderTotal(o))}`, action: 'order', id: o.id }));
if (orderMatches.length) groups.push({name:'Заказы', items:orderMatches});
const clientMatches = clientIndex().filter(c => [c.name,c.phone,...c.addresses].join(' ').toLowerCase().includes(q)).slice(0,10).map(c => ({title:c.name, meta:`${c.phone || 'без телефона'} · заказов: ${c.orders.length}`, action:'client', id:encodeURIComponent(c.key)}));
if (clientMatches.length) groups.push({name:'Клиенты', items:clientMatches});
const boxMatches = appBoxes().filter(b => [b.name,b.catalogSection,(b.ingredients||[]).map(x=>x[0]).join(' ')].join(' ').toLowerCase().includes(q)).slice(0,12).map(b=>({title:b.name,meta:`${moneyFmt(b.price)} · ${b.catalogSection || 'каталог'}`,action:'box',id:b.id}));
if (boxMatches.length) groups.push({name:'Каталог',items:boxMatches});
const stockMatches = getStock().filter(p => [p.name,p.unit].join(' ').toLowerCase().includes(q)).slice(0,12).map(p=>({title:p.name,meta:`Остаток ${numFmt(p.qty)} ${p.unit} · ${moneyFmt(p.cost)}`,action:'stock',id:p.id}));
if (stockMatches.length) groups.push({name:'Склад',items:stockMatches});
const employeeMatches = getEmployees().filter(e=>[e.name,e.role,e.phone,e.note].join(' ').toLowerCase().includes(q)).slice(0,10).map(e=>({title:e.name,meta:`${e.role || 'сотрудник'} · ${e.phone || 'без телефона'}`,action:'employee',id:e.id}));
if(employeeMatches.length)groups.push({name:'Команда',items:employeeMatches});
const supplierMatches = getSuppliers().filter(s=>[s.name,s.contact,s.phone,s.email,s.address].join(' ').toLowerCase().includes(q)).slice(0,10).map(s=>({title:s.name,meta:`${s.contact || ''} ${s.phone || ''}`.trim() || 'поставщик',action:'supplier',id:s.id}));
if(supplierMatches.length)groups.push({name:'Поставщики',items:supplierMatches});
return groups;
}
function renderGlobalSearch() {
const groups = searchData($('sunGlobalSearchInput').value);
$('sunGlobalSearchResults').innerHTML = groups.length ? groups.map(g=>`<section class="sun-search-group"><h3>${esc(g.name)}</h3>${g.items.map(item=>`<button type="button" class="sun-search-item" data-action="${esc(item.action)}" data-id="${esc(item.id)}"><span><b>${esc(item.title)}</b><small>${esc(item.meta)}</small></span><span>→</span></button>`).join('')}</section>`).join('') : `<div class="sun-search-empty">Ничего не найдено.</div>`;
}
function openGlobalSearch() { modal('sun-global-search-modal'); setTimeout(()=>{$('sunGlobalSearchInput').focus(); $('sunGlobalSearchInput').select();},40); }
$('sunGlobalSearchBtn').onclick = openGlobalSearch;
$('sunGlobalSearchInput').addEventListener('input', renderGlobalSearch);
$('sunGlobalSearchResults').addEventListener('click', event => {
const btn = event.target.closest('[data-action]'); if (!btn) return;
closeModal('sun-global-search-modal');
const action = btn.dataset.action, id = btn.dataset.id;
if (action === 'order' && typeof window.openOrder === 'function') window.openOrder(Number(id));
else if (action === 'client' && typeof window.openClientCard === 'function') { activateNav('Клиенты'); setTimeout(()=>window.openClientCard(id),40); }
else if (action === 'box') { activateNav('Новый заказ'); const item=appBoxes().find(x=>String(x.id)===String(id)); if(item){ const categoryButtons=qsa('.cats button'); const target=categoryButtons.find(b=>b.textContent.trim()===(['Боксы','Посуда и упаковка','Дополнения','Напитки','Доставка'][Number(item.category||0)]||'Боксы')); target?.click(); setTimeout(()=>window.editBox?.(id),40); } }
else if (action === 'stock') { activateNav('Склад'); setTimeout(()=>window.editStockProduct?.(id),70); }
else if (action === 'employee') { activateNav('Команда'); setTimeout(()=>window.sunEditEmployee?.(id),70); }
else if (action === 'supplier') { activateNav('Поставщики'); setTimeout(()=>window.sunEditSupplier?.(id),70); }
});
document.addEventListener('keydown', event => { if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'k') { event.preventDefault(); openGlobalSearch(); } });
// ---------- Settings page ----------
const settingsView = document.createElement('section');
settingsView.id = 'enterprise-settings'; settingsView.className = 'view page';
document.body.appendChild(settingsView);
const settingsButton = makeNavButton('Настройки', 'management', 'nav-settings', settingsView, renderSettings);
// IndexedDB backups.
function openBackupDb() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('SunCateringEnterprise', 1);
request.onupgradeneeded = () => { const db=request.result; if(!db.objectStoreNames.contains('backups'))db.createObjectStore('backups',{keyPath:'id'}); };
request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error);
});
}
const FALLBACK_BACKUPS_KEY='sunFallbackBackupsV1';
function fallbackBackups(){
try{const rows=JSON.parse(localStorage.getItem(FALLBACK_BACKUPS_KEY)||'[]');return Array.isArray(rows)?rows:[]}catch(_){return []}
}
function saveFallbackBackups(rows){
const compact=(Array.isArray(rows)?rows:[]).slice(0,4);
localStorage.setItem(FALLBACK_BACKUPS_KEY,JSON.stringify(compact));
}
function collectState() {
const storage = {};
for (let i=0;i<localStorage.length;i++) {
const key=localStorage.key(i); if (!key || !key.startsWith('sun') || key === AUDIT_KEY || key === FALLBACK_BACKUPS_KEY) continue;
storage[key]=localStorage.getItem(key);
}
return { format:'sun-catering-backup', version:VERSION, createdAt:new Date().toISOString(), storage };
}
function applyState(state) {
if (!state || state.format !== 'sun-catering-backup' || !state.storage) throw new Error('Неверный формат резервной копии.');
Object.entries(state.storage).forEach(([key,value])=>{ if(key.startsWith('sun'))localStorage.setItem(key,String(value)); });
}
function downloadBlob(name, content, type='application/octet-stream') {
const blob = content instanceof Blob ? content : new Blob([content],{type});
const url=URL.createObjectURL(blob), a=document.createElement('a'); a.href=url;a.download=name;document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(url),1000);
}
async function createBackup(reason='Ручная копия') {
const state=collectState(); const row={id:uid(),createdAt:state.createdAt,reason,state};
let storedInIdb=false;
try{
const db=await openBackupDb();
await new Promise((resolve,reject)=>{const tx=db.transaction('backups','readwrite');tx.objectStore('backups').put(row);tx.oncomplete=resolve;tx.onerror=()=>reject(tx.error);tx.onabort=()=>reject(tx.error||new Error('IndexedDB transaction aborted'));});
storedInIdb=true;
}catch(_){
// Some browsers restrict IndexedDB for local file:// pages. Use a small
// localStorage fallback so the button still works in portable mode.
const rows=fallbackBackups().filter(x=>x&&x.id!==row.id);rows.unshift(row);saveFallbackBackups(rows);
}
const rows=await listBackups();
if(storedInIdb&&rows.length>12){const excess=rows.slice(12);await Promise.all(excess.map(x=>deleteBackup(x.id,false)));}
recordAudit('Резервная копия', reason); renderBackups(); return row;
}
async function listBackups() {
let idbRows=[];
try{
const db=await openBackupDb();
idbRows=await new Promise((resolve,reject)=>{const req=db.transaction('backups','readonly').objectStore('backups').getAll();req.onsuccess=()=>resolve(req.result||[]);req.onerror=()=>reject(req.error);});
}catch(_){idbRows=[];}
const merged=new Map();[...idbRows,...fallbackBackups()].forEach(row=>{if(row?.id&&!merged.has(String(row.id)))merged.set(String(row.id),row);});
return [...merged.values()].sort((a,b)=>String(b.createdAt||'').localeCompare(String(a.createdAt||'')));
}
async function deleteBackup(id, rerender=true){
let deleted=false;
try{const db=await openBackupDb();await new Promise((resolve,reject)=>{const tx=db.transaction('backups','readwrite');tx.objectStore('backups').delete(id);tx.oncomplete=resolve;tx.onerror=()=>reject(tx.error);});deleted=true;}catch(_){}
const fallback=fallbackBackups(),next=fallback.filter(x=>String(x?.id)!==String(id));
if(next.length!==fallback.length){saveFallbackBackups(next);deleted=true;}
if(rerender)renderBackups();return deleted;
}
async function restoreBackup(id){
let row=null;
try{const db=await openBackupDb();row=await new Promise((resolve,reject)=>{const req=db.transaction('backups','readonly').objectStore('backups').get(id);req.onsuccess=()=>resolve(req.result||null);req.onerror=()=>reject(req.error);});}catch(_){}
if(!row)row=fallbackBackups().find(x=>String(x?.id)===String(id))||null;
if(!row)return toast('Резервная копия не найдена.','warn');
if(!confirm(`Восстановить копию от ${new Date(row.createdAt).toLocaleString('ru-RU')}? Текущие данные будут заменены.`))return;
await createBackup('Перед восстановлением');applyState(row.state);recordAudit('Восстановление базы',row.reason);location.reload();
}
let users = safeJson(localStorage.getItem(USERS_KEY) || '[]', []);
const roleNames = { owner:'Владелец', manager:'Менеджер', kitchen:'Кухня', purchaser:'Закупщик', courier:'Курьер', waiter:'Официант' };
const permissions = {
owner:['*'],
manager:['Главная','Новый заказ','Заказы','Календарь','Карта','Клиенты','Рассылки','Деньги','Статистика','Печать'],
kitchen:['Главная','Новый заказ','Календарь','Производство','Склад','Печать'],
purchaser:['Главная','Закупки','Склад','Поставщики','Печать'],
courier:['Главная','Заказы','Календарь','Карта','Маршруты'],
waiter:['Главная','Заказы','Календарь']
};
async function pinHash(pin){if(!pin)return'';if(crypto?.subtle){const data=new TextEncoder().encode(String(pin));const hash=await crypto.subtle.digest('SHA-256',data);return [...new Uint8Array(hash)].map(x=>x.toString(16).padStart(2,'0')).join('');}let h=2166136261;for(const c of String(pin)){h^=c.charCodeAt(0);h=Math.imul(h,16777619);}return String(h>>>0);}
const saveUsers=()=>localStorage.setItem(USERS_KEY,JSON.stringify(users));
const userModal=document.createElement('div');userModal.id='sun-user-modal';userModal.className='modal';userModal.innerHTML=`<div class="dialog"><div class="dialog-head"><h2 id="sunUserTitle">Сотрудник системы</h2><button type="button" onclick="closeModal('sun-user-modal')">×</button></div><div class="enterprise-fields"><label>Имя<input id="sunUserName"></label><label>Роль<select id="sunUserRole">${Object.entries(roleNames).map(([k,v])=>`<option value="${k}">${v}</option>`).join('')}</select></label><label>PIN-код<input id="sunUserPin" type="password" inputmode="numeric" placeholder="Не менять — оставьте пустым"></label><label style="flex-direction:row;align-items:center;margin-top:23px"><input id="sunUserActive" type="checkbox" checked style="width:18px;height:18px"> Активный пользователь</label></div><div class="actions"><button class="primary" id="sunUserSave" type="button">Сохранить</button><button class="danger" id="sunUserDelete" type="button">Удалить</button></div></div>`;document.body.appendChild(userModal);
let editingUserId=null;
function openUserEditor(id=null){editingUserId=id;const u=users.find(x=>String(x.id)===String(id));$('sunUserTitle').textContent=u?'Изменить пользователя':'Новый пользователь';$('sunUserName').value=u?.name||'';$('sunUserRole').value=u?.role||'manager';$('sunUserPin').value='';$('sunUserActive').checked=u?u.active!==false:true;$('sunUserDelete').style.display=u?'inline-block':'none';modal('sun-user-modal');}
$('sunUserSave').onclick=async()=>{const name=$('sunUserName').value.trim();if(!name)return toast('Введите имя пользователя.','warn');const pin=$('sunUserPin').value;let u=users.find(x=>String(x.id)===String(editingUserId));if(!u){u={id:uid(),pinHash:'',createdAt:new Date().toISOString()};users.push(u);}u.name=name;u.role=$('sunUserRole').value;u.active=$('sunUserActive').checked;if(pin)u.pinHash=await pinHash(pin);saveUsers();recordAudit('Пользователь системы',`${name} · ${roleNames[u.role]}`);closeModal('sun-user-modal');renderSettings();};
$('sunUserDelete').onclick=()=>{const u=users.find(x=>String(x.id)===String(editingUserId));if(!u||!confirm(`Удалить пользователя «${u.name}»?`))return;users=users.filter(x=>String(x.id)!==String(editingUserId));saveUsers();recordAudit('Удалён пользователь',u.name);closeModal('sun-user-modal');renderSettings();};
window.sunEditSystemUser=openUserEditor;
function renderUsers(){const host=$('usersList');if(!host)return;host.innerHTML=users.length?users.map(u=>`<div class="user-row"><div><b>${esc(u.name)}</b> <span class="role-chip">${esc(roleNames[u.role]||u.role)}</span><small style="display:block">${u.active===false?'Неактивен':u.pinHash?'PIN установлен':'PIN не установлен'}</small></div><button class="mini" type="button" onclick="sunEditSystemUser('${esc(u.id)}')">Изменить</button></div>`).join(''):'<p class="hint">Пользователей пока нет. Добавьте владельца с PIN-кодом перед включением входа.</p>';}
async function renderBackups(){const host=$('backupList');if(!host)return;try{const rows=await listBackups();host.innerHTML=rows.length?rows.map(r=>`<div class="backup-row"><div><b>${new Date(r.createdAt).toLocaleString('ru-RU')}</b><small style="display:block">${esc(r.reason||'Резервная копия')}</small></div><div><button class="mini" type="button" data-restore-backup="${esc(r.id)}">Восстановить</button> <button class="mini danger" type="button" data-delete-backup="${esc(r.id)}">Удалить</button></div></div>`).join(''):'<p class="hint">Резервных копий пока нет.</p>';host.querySelectorAll('[data-restore-backup]').forEach(b=>b.onclick=()=>restoreBackup(b.dataset.restoreBackup));host.querySelectorAll('[data-delete-backup]').forEach(b=>b.onclick=()=>deleteBackup(b.dataset.deleteBackup));}catch(e){host.innerHTML=`<p class="hint">Не удалось открыть хранилище копий: ${esc(e.message)}</p>`;}}
function renderAudit(){const host=$('auditList');if(!host)return;const rows=auditLog.slice(0,80);host.innerHTML=rows.length?rows.map(r=>`<div class="audit-row"><small>${new Date(r.at).toLocaleString('ru-RU')}</small><b>${esc(r.user)}</b><div>${esc(r.action)}${r.details?`<small style="display:block">${esc(r.details)}</small>`:''}</div></div>`).join(''):'<p class="hint">История пока пуста.</p>';}
function renderSettings(){
settingsView.innerHTML=`<div class="catalog-head"><div><h1>Настройки</h1><p class="hint" style="margin:5px 0 0">Профиль, аккаунт, пользователи и рабочие настройки.</p></div><span class="role-chip">Версия ${VERSION}</span></div>
<div class="enterprise-grid">
<section class="enterprise-card wide"><h2>Облачная синхронизация</h2><p class="hint">Профиль и рабочая база компании.</p><div class="enterprise-fields"><label class="wide">Supabase URL<input id="cloudUrl" value="${esc(settings.cloud.supabaseUrl)}" placeholder="https://project.supabase.co"></label><label class="wide">Anon key<input id="cloudKey" type="password" value="${esc(settings.cloud.anonKey)}"></label><label>Рабочая база<input id="cloudWorkspace" value="${esc(settings.cloud.workspaceId)}"></label><label style="flex-direction:row;align-items:center;margin-top:23px"><input id="cloudAuto" type="checkbox" ${settings.cloud.autoSync?'checked':''} style="width:18px;height:18px"> Автосинхронизация</label></div><div class="actions"><button class="primary" id="cloudSave" type="button">Сохранить</button><button class="outline" id="cloudPush" type="button">Отправить в облако</button><button class="outline" id="cloudPull" type="button">Загрузить из облака</button></div><div class="status" id="cloudStatus">${settings.cloud.lastSync?`Последняя синхронизация: ${esc(new Date(settings.cloud.lastSync).toLocaleString('ru-RU'))}`:'Облако не синхронизировалось.'}</div></section>
<section class="enterprise-card"><h2>Пользователи и роли</h2><p class="hint">Локальные роли скрывают недоступные разделы на этом устройстве. Для полноценной защиты нескольких устройств нужна облачная авторизация.</p><label style="flex-direction:row;align-items:center"><input id="rolesToggle" type="checkbox" ${settings.rolesEnabled?'checked':''} style="width:18px;height:18px"> Включить вход по PIN-коду</label><div class="actions"><button class="primary" id="addSystemUser" type="button">Добавить пользователя</button></div><div class="users-list" id="usersList"></div></section>
<section class="enterprise-card wide"><h2>История изменений</h2><div class="actions"><button class="outline" id="clearAuditBtn" type="button">Очистить историю</button></div><div class="audit-list" id="auditList"></div></section>
</div>`;
$('cloudSave').onclick=saveCloudSettings;$('cloudPush').onclick=cloudPush;$('cloudPull').onclick=cloudPull;
$('rolesToggle').onchange=async e=>{if(e.target.checked){const owner=users.find(u=>u.active!==false&&u.role==='owner'&&u.pinHash);if(!owner){e.target.checked=false;return toast('Сначала добавьте активного владельца и задайте ему PIN-код.','warn',6000);}}settings.rolesEnabled=e.target.checked;saveSettings();recordAudit('Режим входа',settings.rolesEnabled?'включён':'выключен');if(settings.rolesEnabled)toast('Режим входа включён. После выхода потребуется PIN.','success');else{sessionStorage.removeItem(SESSION_KEY);applyPermissions();}};
$('addSystemUser').onclick=()=>openUserEditor();
$('clearAuditBtn').onclick=()=>{if(confirm('Очистить историю изменений?')){auditLog=[];localStorage.setItem(AUDIT_KEY,'[]');renderAudit();}};
renderUsers();renderAudit();
}
function saveCloudSettings(){settings.cloud.supabaseUrl=$('cloudUrl').value.trim().replace(/\/$/,'');settings.cloud.anonKey=$('cloudKey').value.trim();settings.cloud.workspaceId=$('cloudWorkspace').value.trim()||'sun-catering-main';settings.cloud.autoSync=$('cloudAuto').checked;settings.cloud.enabled=Boolean(settings.cloud.supabaseUrl&&settings.cloud.anonKey);saveSettings();toast('Настройки облака сохранены.','success');}
function cloudHeaders(){return {'apikey':settings.cloud.anonKey,'Authorization':`Bearer ${settings.cloud.anonKey}`,'Content-Type':'application/json'};}
function cloudStatus(text,bad=false){const el=$('cloudStatus');if(el){el.textContent=text;el.className=`status ${bad?'bad':'good'}`;}}
async function cloudPush(){saveCloudSettings();if(!settings.cloud.enabled)return cloudStatus('Укажите Supabase URL и Anon key.',true);cloudStatus('Отправляю данные…');try{const url=`${settings.cloud.supabaseUrl}/rest/v1/sun_app_state?on_conflict=workspace_id`;const payload={workspace_id:settings.cloud.workspaceId,payload:collectState(),updated_at:new Date().toISOString()};const res=await sunFetchWithTimeout(url,{method:'POST',headers:{...cloudHeaders(),'Prefer':'resolution=merge-duplicates,return=minimal'},body:JSON.stringify(payload)},12000);if(!res.ok)throw new Error(`${res.status}: ${await res.text()}`);settings.cloud.lastSync=new Date().toISOString();saveSettings();recordAudit('Облачная синхронизация','отправлено');cloudStatus(`Отправлено: ${new Date().toLocaleString('ru-RU')}`);toast('База отправлена в облако.','success');}catch(e){cloudStatus(`Ошибка: ${e.message}`,true);toast('Не удалось отправить данные в облако.','error');}}
async function cloudPull(){saveCloudSettings();if(!settings.cloud.enabled)return cloudStatus('Укажите Supabase URL и Anon key.',true);cloudStatus('Загружаю данные…');try{const url=`${settings.cloud.supabaseUrl}/rest/v1/sun_app_state?workspace_id=eq.${encodeURIComponent(settings.cloud.workspaceId)}&select=payload,updated_at&limit=1`;const res=await sunFetchWithTimeout(url,{headers:cloudHeaders()},12000);if(!res.ok)throw new Error(`${res.status}: ${await res.text()}`);const rows=await res.json();if(!rows.length)throw new Error('В облаке нет сохранённой базы.');if(!confirm(`Загрузить облачную копию от ${new Date(rows[0].updated_at).toLocaleString('ru-RU')}?`))return;await createBackup('Перед загрузкой из облака');applyState(rows[0].payload);recordAudit('Облачная синхронизация','загружено');location.reload();}catch(e){cloudStatus(`Ошибка: ${e.message}`,true);toast('Не удалось загрузить облачную базу.','error');}}
let cloudTimer=null;function scheduleCloudPush(){/* Legacy Supabase snapshot sync disabled in Cloud v2 build. */return;}
// Track important local changes and queue optional cloud sync.
(()=>{
if(Storage.prototype.__sunEnterpriseTracked)return;
const nativeSet=Storage.prototype.setItem;
const tracked={sunOrders:'Заказы',sunBoxes:'Каталог',sunStock:'Склад',sunStockMoves:'Движения склада',sunFinanceRecordsV2:'Финансы',sunEmployees:'Команда',sunSuppliers:'Поставщики',sunRoutePlansV1:'Маршруты'};
const lastAudit=new Map();
Object.defineProperty(Storage.prototype,'__sunEnterpriseTracked',{value:true,configurable:true});
Storage.prototype.setItem=function(key,value){
const old=this===localStorage?this.getItem(key):null;
const result=nativeSet.call(this,key,value);
if(this===localStorage&&tracked[key]&&old!==String(value)){
scheduleCloudPush();
const now=Date.now();
if(now-(lastAudit.get(key)||0)>1500){lastAudit.set(key,now);recordAudit('Изменён раздел',tracked[key]);}
}
return result;
};
})();
// ---------- Roles / login ----------
function applyPermissions(){
const user=currentUser();$('sunCurrentUserLabel').textContent=settings.rolesEnabled?`${user.name} · ${roleNames[user.role]||user.role}`:'Владелец';$('sunLogoutBtn').style.display=settings.rolesEnabled?'flex':'none';
const allowed=permissions[user.role]||[];qsa('header nav button').forEach(button=>{const label=button.textContent.trim();button.style.display=(!settings.rolesEnabled||allowed.includes('*')||allowed.includes(label))?'':'none';});
settingsButton.style.display=(!settings.rolesEnabled||user.role==='owner')?'':'none';
}
$('sunLogoutBtn').onclick=()=>{recordAudit('Выход из системы',currentUser().name);sessionStorage.removeItem(SESSION_KEY);location.reload();};
// ---------- Draft autosave UI disabled ----------
// Draft banners/restoration prompts are intentionally disabled to keep “Новый заказ” clean.
function saveDraftAutosave(){}
function checkAutosave(){}
// ---------- Portable mode: background installers are intentionally absent ----------
// Auto backup and public API.
async function maybeAutoBackup(){if(!settings.autoBackup)return;try{const rows=await listBackups();const latest=rows[0];if(!latest||Date.now()-new Date(latest.createdAt).getTime()>20*3600000)await createBackup('Автоматическая ежедневная копия');}catch(_) {}}
window.SunEnterprise={VERSION,toast,recordAudit,collectState,applyState,createBackup,scheduleCloudPush,activateNav,currentUser,openView};
// Initial setup after all existing modules are loaded.
applyPermissions();checkAutosave();maybeAutoBackup();
})();
/* ===== Notifications and deadline control ===== */
(() => {
'use strict';
const $=id=>document.getElementById(id),qsa=(s,r=document)=>[...r.querySelectorAll(s)];
if(!$('sunNotificationsBtn')||document.getElementById('sun-notifications-modal'))return;
const esc=window.SunSafe.escapeHTML;
const readKey='sunReadNotificationsV1';
const getOrders=()=>{try{return orders}catch(_){try{return JSON.parse(localStorage.sunOrders||'[]')}catch(__){return[]}}};
const getBoxes=()=>{try{return boxes}catch(_){try{return JSON.parse(localStorage.sunBoxes||'[]')}catch(__){return[]}}};
const getEmployees=()=>{try{return JSON.parse(localStorage.sunEmployees||'[]')}catch(_){return[]}};
const getStock=()=>{try{return JSON.parse(localStorage.sunStock||'[]')}catch(_){return[]}};
const orderTotal=o=>Number(o.total||0)||((o.lines||[]).reduce((s,l)=>{const b=getBoxes().find(x=>String(x.id)===String(l.id));return s+Number(b?.price||0)*Number(l.qty||0)},0));
const paid=o=>Math.max(0,Number(o.prepayment||0));
const toStamp=o=>new Date(`${o.date||'1970-01-01'}T${o.time||'00:00'}:00`).getTime();
const now=()=>Date.now();
const readSet=()=>new Set((()=>{try{return JSON.parse(localStorage.getItem(readKey)||'[]')}catch(_){return[]}})());
const saveRead=set=>localStorage.setItem(readKey,JSON.stringify([...set].slice(-500)));
const style=document.createElement('style');style.textContent=`
.notification-list{display:grid;gap:8px;margin-top:12px}.notification-item{border:1px solid #d9dde0;border-left:5px solid #7da4c4;border-radius:9px;background:#fff;padding:11px 12px;display:grid;grid-template-columns:1fr auto;gap:10px;align-items:start}.notification-item.critical{border-left-color:#c85b52;background:#fffafa}.notification-item.warn{border-left-color:#d5a13f;background:#fffdf7}.notification-item.info{border-left-color:#6c9fbc}.notification-item.read{opacity:.58}.notification-item h3{margin:0;color:#15364c;font-size:15px}.notification-item p{margin:4px 0 0;color:#68717a;font-size:13px}.notification-actions{display:flex;gap:5px;flex-wrap:wrap;justify-content:flex-end}.notification-filter{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.notification-empty{text-align:center;color:#879198;padding:28px}.notification-summary{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-top:12px}.notification-summary>div{background:#f5f7f7;border-radius:8px;padding:10px}.notification-summary small{display:block;color:#68717a}.notification-summary b{display:block;color:#15364c;font-size:18px;margin-top:3px}@media(max-width:650px){.notification-item{grid-template-columns:1fr}.notification-actions{justify-content:flex-start}.notification-summary{grid-template-columns:1fr 1fr}}
`;document.head.appendChild(style);
const notificationModal=document.createElement('div');notificationModal.id='sun-notifications-modal';notificationModal.className='modal';notificationModal.innerHTML=`<div class="dialog" style="width:min(900px,100%)"><div class="dialog-head"><h2>Уведомления и контроль сроков</h2><button type="button" onclick="closeModal('sun-notifications-modal')">×</button></div><div class="notification-filter"><label>Показывать<select id="notificationFilter"><option value="all">Все</option><option value="unread" selected>Только новые</option><option value="critical">Только важные</option></select></label><button class="outline" id="markAllNotifications" type="button">Отметить всё прочитанным</button><button class="outline" id="requestBrowserNotifications" type="button">Разрешить уведомления браузера</button></div><div class="notification-summary" id="notificationSummary"></div><div class="notification-list" id="notificationList"></div></div>`;document.body.appendChild(notificationModal);
function hasCourier(order){const employees=getEmployees(),ids=(order.staffIds||[]).map(String);return employees.some(e=>ids.includes(String(e.id))&&String(e.role||'').toLowerCase().includes('курьер'));}
function checklistReady(order){const values=Object.values(order.checklist||{});return values.length&&values.every(Boolean);}
function generate(){
const items=[],orders=getOrders(),employees=getEmployees(),stock=getStock(),boxes=getBoxes(),current=now();
const add=(id,severity,title,text,action='order',entityId='')=>items.push({id,severity,title,text,action,entityId});
orders.filter(o=>!['Отменён','Отдан заказчику'].includes(o.status||'Новый')).forEach(o=>{
const stamp=toStamp(o),hours=(stamp-current)/3600000;
if(hours>=0&&hours<=48&&!o.address)add(`address-${o.id}`,'critical',`Заказ №${o.id}: нет адреса`,`${o.event||'Мероприятие'} · ${o.date||'—'} ${o.time||''}`,'order',o.id);
if(hours>=0&&hours<=48&&o.address&&!(o.geo&&Number.isFinite(Number(o.geo.lat))&&Number.isFinite(Number(o.geo.lon))))add(`geo-${o.id}`,'warn',`Заказ №${o.id}: нет картинки карты`,`Создайте статичную карту доставки: ${o.address}`,'order',o.id);
const debt=Math.max(0,orderTotal(o)-paid(o));
if(hours>=0&&hours<=72&&debt>0)add(`debt-${o.id}`,'info',`Заказ №${o.id}: осталось получить оплату`,`${debt.toLocaleString('ru-RU')} ₽ · ${o.contact||'клиент не указан'}`,'order',o.id);
if(hours>=0&&hours<=3)add(`soon-${o.id}`,'critical',`Скоро выдача заказа №${o.id}`,`${o.time||'—'} · ${o.event||'Мероприятие'} · ${o.address||'самовывоз/адрес не указан'}`,'order',o.id);
});
// Team conflicts at the same date/time.
const slots=new Map();orders.filter(o=>!['Отменён','Отдан заказчику'].includes(o.status||'Новый')).forEach(o=>(o.staffIds||[]).forEach(id=>{const k=`${id}|${o.date}|${o.time}`;if(!slots.has(k))slots.set(k,[]);slots.get(k).push(o);}));
slots.forEach((list,key)=>{if(list.length<2)return;const employee=employees.find(e=>String(e.id)===String(key.split('|')[0]));add(`conflict-${key}`,'critical',`Пересечение сотрудника: ${employee?.name||'сотрудник'}`,list.map(o=>`${o.id} ${o.event||''}`).join(' и '),'team','');});
// Low stock.
stock.filter(p=>Number(p.min||0)>0&&Number(p.qty||0)<=Number(p.min||0)).slice(0,30).forEach(p=>add(`stock-${p.id}`,Number(p.qty||0)<=0?'critical':'warn',`Низкий остаток: ${p.name}`,`Есть ${Number(p.qty||0).toLocaleString('ru-RU')} ${p.unit||''}, минимум ${Number(p.min||0).toLocaleString('ru-RU')}`,'stock',p.id));
return items;
}
let cached=[];
function refreshBadge(){cached=generate();const read=readSet(),unread=cached.filter(x=>!read.has(x.id)).length;$('sunNotificationBadge').textContent=unread?String(unread):'';return cached;}
function render(){
const items=refreshBadge(),read=readSet(),filter=$('notificationFilter').value,shown=items.filter(x=>filter==='all'||(filter==='unread'&&!read.has(x.id))||(filter==='critical'&&x.severity==='critical'));
$('notificationSummary').innerHTML=`<div><small>Всего</small><b>${items.length}</b></div><div><small>Новых</small><b>${items.filter(x=>!read.has(x.id)).length}</b></div><div><small>Важных</small><b>${items.filter(x=>x.severity==='critical').length}</b></div>`;
$('notificationList').innerHTML=shown.length?shown.map(x=>`<article class="notification-item ${x.severity} ${read.has(x.id)?'read':''}" data-notification="${esc(x.id)}"><div><h3>${esc(x.title)}</h3><p>${esc(x.text)}</p></div><div class="notification-actions"><button class="mini" type="button" data-open-notification="${esc(x.id)}">Открыть</button><button class="mini" type="button" data-read-notification="${esc(x.id)}">${read.has(x.id)?'Вернуть в новые':'Прочитано'}</button></div></article>`).join(''):'<div class="notification-empty">Уведомлений по выбранному фильтру нет.</div>';
qsa('[data-read-notification]',$('notificationList')).forEach(b=>b.onclick=()=>{const set=readSet(),id=b.dataset.readNotification;set.has(id)?set.delete(id):set.add(id);saveRead(set);render();});
qsa('[data-open-notification]',$('notificationList')).forEach(b=>b.onclick=()=>{const item=items.find(x=>x.id===b.dataset.openNotification);if(!item)return;const set=readSet();set.add(item.id);saveRead(set);closeModal('sun-notifications-modal');if(item.action==='order')window.openOrder?.(Number(item.entityId));else if(item.action==='stock'){SunEnterprise?.activateNav?.('Склад');setTimeout(()=>window.editStockProduct?.(item.entityId),80);}else if(item.action==='team')SunEnterprise?.activateNav?.('Команда');refreshBadge();});
}
$('sunNotificationsBtn').onclick=()=>{notificationModal.classList.add('on');render();};
$('notificationFilter').onchange=render;
$('markAllNotifications').onclick=()=>{const set=readSet();generate().forEach(x=>set.add(x.id));saveRead(set);render();};
$('requestBrowserNotifications').onclick=async()=>{if(!('Notification'in window))return SunEnterprise?.toast?.('Уведомления браузера не поддерживаются.','warn');const permission=await Notification.requestPermission();SunEnterprise?.toast?.(permission==='granted'?'Уведомления браузера разрешены.':'Разрешение не выдано.',permission==='granted'?'success':'warn');};
refreshBadge();setInterval(refreshBadge,60000);
window.addEventListener('sun:stock-changed',refreshBadge);
if(typeof window.saveOrder==='function'){const prev=window.saveOrder;window.saveOrder=async(...args)=>{const result=prev(...args);if(result&&typeof result.then==='function')await result;setTimeout(refreshBadge,30);return result;};}
if(typeof window.status==='function'){const prev=window.status;window.status=(...args)=>{const result=prev(...args);setTimeout(refreshBadge,20);return result;};}
})();
/* ===== ORDER PAYMENTS + PROFITABILITY ===== */
(() => {
'use strict';
if (document.getElementById('sun-order-economy-style')) return;
const $ = id => document.getElementById(id);
const esc=window.SunSafe.escapeHTML;
const fmt = value => `${Number(value || 0).toLocaleString('ru-RU',{maximumFractionDigits:2})} ₽`;
const uid = () => typeof window.sunUUID === 'function' ? window.sunUUID() : `pay-${Date.now()}-${Math.random().toString(36).slice(2)}`;
const style=document.createElement('style');style.id='sun-order-economy-style';style.textContent=`
.order-finance-panel{margin-top:15px;border:1px solid var(--l);border-radius:10px;background:#fff;overflow:hidden}.order-finance-head{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:11px 13px;background:#f4f6f7}.order-finance-head h3{margin:0;color:var(--n);font-size:16px}.payment-list{padding:0 13px}.payment-row{display:grid;grid-template-columns:105px 1fr 105px auto;gap:8px;align-items:center;padding:9px 0;border-bottom:1px solid #e8ebed}.payment-row:last-child{border-bottom:0}.payment-row small{color:#68717a}.payment-empty{padding:14px;color:#879198}.economy-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:9px;padding:13px}.economy-summary{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;padding:0 13px 13px}.economy-summary>div{padding:10px;border-radius:8px;background:#f7f8f8}.economy-summary small{display:block;color:#68717a}.economy-summary b{display:block;color:var(--n);font-size:17px;margin-top:4px}.economy-summary .profit.good{background:#eaf6ed}.economy-summary .profit.bad{background:#fbeeee}.profit-table{width:100%;border-collapse:collapse}.profit-table th,.profit-table td{padding:9px;border-bottom:1px solid #e4e7e9;text-align:left}.profit-table th{font-size:12px;color:#68717a}.profit-table .num{text-align:right}.profit-badge{display:inline-flex;padding:3px 7px;border-radius:999px;font-size:11px;font-weight:700;background:#eaf6ed;color:#347154}.profit-badge.bad{background:#fbeeee;color:#943d3d}
@media(max-width:720px){.payment-row{grid-template-columns:1fr 1fr}.payment-row>*:last-child{justify-self:end}.economy-grid,.economy-summary{grid-template-columns:1fr 1fr}}
`;document.head.appendChild(style);
const details=document.getElementById('details');if(!details)return;
const panel=document.createElement('div');panel.id='orderEconomyPanel';panel.innerHTML=`
<section class="order-finance-panel"><div class="order-finance-head"><h3>Платежи клиента</h3><button class="outline" id="addOrderPayment" type="button"> Добавить платёж</button></div><div class="payment-list" id="orderPaymentList"></div></section>
<section class="order-finance-panel"><div class="order-finance-head"><h3>Экономика заказа</h3><span class="hint">Себестоимость продуктов рассчитывается по закупочным ценам склада</span></div><div class="economy-grid"><label>Доставка / транспорт<input id="orderDeliveryCost" type="number" min="0" step="1" value="0"></label><label>Работа команды<input id="orderLaborCost" type="number" min="0" step="1" value="0"></label><label>Прочие расходы<input id="orderOtherCost" type="number" min="0" step="1" value="0"></label></div><div class="economy-summary" id="orderEconomySummary"></div></section>`;
details.appendChild(panel);
const paymentModalEl=document.createElement('div');paymentModalEl.id='order-payment-modal';paymentModalEl.className='modal';paymentModalEl.innerHTML=`<div class="dialog"><div class="dialog-head"><h2 id="paymentModalTitle">Платёж</h2><button type="button" onclick="closeModal('order-payment-modal')">×</button></div><div class="enterprise-fields"><label>Дата<input id="paymentDate" type="date"></label><label>Сумма<input id="paymentAmount" type="number" min="0" step="1"></label><label>Способ<select id="paymentMethod"><option>Перевод</option><option>Наличные</option><option>Карта</option><option>СБП</option><option>Безналичный счёт</option><option>Другое</option></select></label><label>Комментарий<input id="paymentNote" placeholder="Предоплата, доплата…"></label></div><div class="actions"><button class="primary" id="paymentSave" type="button">Сохранить</button><button class="danger" id="paymentDelete" type="button">Удалить</button></div></div>`;document.body.appendChild(paymentModalEl);
let editingPaymentId=null;
function currentDraft(){try{return draft;}catch(_){return {};}}
function normalizePayments(){const d=currentDraft();if(!Array.isArray(d.payments)){d.payments=[];const legacy=Math.max(0,Number(d.prepayment||$('prepayment')?.value||0));if(legacy>0)d.payments.push({id:uid(),date:d.date||$('date')?.value||new Date().toISOString().slice(0,10),amount:legacy,method:'Не указан',note:'Перенесено из поля предоплаты',legacy:true});}return d.payments;}
function totalPaid(){return normalizePayments().reduce((sum,p)=>sum+Math.max(0,Number(p.amount||0)),0);}
function stockCostMap(){const list=(()=>{try{return JSON.parse(localStorage.getItem('sunStock')||'[]')}catch(_){return[]}})();const map=new Map();list.forEach(p=>map.set(`${String(p.name||'').trim().toLowerCase()}|${String(p.unit||'').trim().toLowerCase()}`,Number(p.cost||0)));return map;}
function ingredientCost(){const costs=stockCostMap();const d=currentDraft();return (d.lines||[]).reduce((sum,line)=>{let item;try{item=boxes.find(x=>String(x.id)===String(line.id));}catch(_){item=null;}if(!item)return sum;return sum+(item.ingredients||[]).reduce((s,part)=>s+Number(part?.[1]||0)*Number(line.qty||0)*Number(costs.get(`${String(part?.[0]||'').trim().toLowerCase()}|${String(part?.[2]||'').trim().toLowerCase()}`)||0),0);},0);}
function currentTotal(){try{return typeof calcOrderTotal==='function'?Number(calcOrderTotal()||0):Number(currentDraft().total||0)}catch(_){return Number(currentDraft().total||0)}}
function syncFieldsToDraft(){const d=currentDraft();d.prepayment=Math.max(0,Number($('prepayment')?.value||d.prepayment||0));d.costs={delivery:Math.max(0,Number($('orderDeliveryCost')?.value||0)),labor:Math.max(0,Number($('orderLaborCost')?.value||0)),other:Math.max(0,Number($('orderOtherCost')?.value||0)),ingredients:ingredientCost()};try{window.updateOrderSummary?.();}catch(_){}renderEconomy();}
function renderPayments(){const host=$('orderPaymentList');if(host)host.innerHTML='';if($('prepayment')){$('prepayment').readOnly=false;$('prepayment').title='';$('prepayment').value=Math.max(0,Number(currentDraft().prepayment||0));}try{window.updateOrderSummary?.();}catch(_){}renderEconomy();}
function renderEconomy(){const total=currentTotal(),ingredients=ingredientCost(),delivery=Math.max(0,Number($('orderDeliveryCost')?.value||0)),labor=Math.max(0,Number($('orderLaborCost')?.value||0)),other=Math.max(0,Number($('orderOtherCost')?.value||0)),cost=ingredients+delivery+labor+other,profit=total-cost,margin=total>0?profit/total*100:0,paid=Math.max(0,Number($('prepayment')?.value||currentDraft().prepayment||0)),debt=Math.max(0,total-paid);const host=$('orderEconomySummary');if(!host)return;host.innerHTML=`<div><small>Продукты</small><b>${fmt(ingredients)}</b></div><div><small>Все расходы</small><b>${fmt(cost)}</b></div><div class="profit ${profit>=0?'good':'bad'}"><small>Прибыль</small><b>${fmt(profit)} · ${margin.toFixed(1)}%</b></div><div><small>Остаток клиента</small><b>${fmt(debt)}</b></div>`;}
function loadEconomy(){const d=currentDraft();const c=d.costs||{};$('orderDeliveryCost').value=Number(c.delivery||0);$('orderLaborCost').value=Number(c.labor||0);$('orderOtherCost').value=Number(c.other||0);renderPayments();}
function openPayment(id=null){editingPaymentId=id;const p=normalizePayments().find(x=>String(x.id)===String(id));$('paymentModalTitle').textContent=p?'Изменить платёж':'Новый платёж';$('paymentDate').value=p?.date||$('date')?.value||new Date().toISOString().slice(0,10);$('paymentAmount').value=p?.amount||'';$('paymentMethod').value=p?.method||'Перевод';$('paymentNote').value=p?.note||'';$('paymentDelete').style.display=p?'inline-block':'none';modal('order-payment-modal');setTimeout(()=>$('paymentAmount').focus(),50);}
$('addOrderPayment').onclick=()=>openPayment();
$('paymentSave').onclick=()=>{const amount=Math.max(0,Number($('paymentAmount').value||0));if(amount<=0)return window.SunEnterprise?.toast('Введите сумму платежа.','warn');let list=normalizePayments(),p=list.find(x=>String(x.id)===String(editingPaymentId));if(!p){p={id:uid(),createdAt:new Date().toISOString()};list.push(p);}Object.assign(p,{date:$('paymentDate').value||new Date().toISOString().slice(0,10),amount,method:$('paymentMethod').value,note:$('paymentNote').value.trim()});syncFieldsToDraft();renderPayments();closeModal('order-payment-modal');window.SunEnterprise?.recordAudit('Платёж по заказу',`${fmt(amount)} · ${p.method}`);};
$('paymentDelete').onclick=()=>{const list=normalizePayments(),p=list.find(x=>String(x.id)===String(editingPaymentId));if(!p||!confirm(`Удалить платёж ${fmt(p.amount)}?`))return;currentDraft().payments=list.filter(x=>String(x.id)!==String(editingPaymentId));syncFieldsToDraft();renderPayments();closeModal('order-payment-modal');window.SunEnterprise?.recordAudit('Удалён платёж',fmt(p.amount));};
['orderDeliveryCost','orderLaborCost','orderOtherCost'].forEach(id=>$(id).addEventListener('input',syncFieldsToDraft));
// Wrap order lifecycle.
if(typeof window.saveOrder==='function'){const prior=window.saveOrder;window.saveOrder=async(...args)=>{syncFieldsToDraft();const result=prior(...args);if(result&&typeof result.then==='function')await result;localStorage.removeItem('sunOrderDraftAutosaveV1');window.SunEnterprise?.recordAudit('Сохранён заказ',`${currentDraft().id||'новый'} · ${currentDraft().event||''}`);window.SunEnterprise?.scheduleCloudPush();return result;};}
if(typeof window.openOrder==='function'){const prior=window.openOrder;window.openOrder=id=>{const result=prior(id);setTimeout(loadEconomy,0);return result;};}
if(typeof window.resetDraft==='function'){const prior=window.resetDraft;window.resetDraft=()=>{const result=prior();currentDraft().payments=[];currentDraft().costs={delivery:0,labor:0,other:0};setTimeout(loadEconomy,0);return result;};}
if(typeof window.render==='function'){const prior=window.render;window.render=(...args)=>{const result=prior(...args);setTimeout(renderEconomy,0);return result;};}
// Profitability tab in Money.
const tabs=document.getElementById('money-tabs');const content=document.getElementById('money-content');if(tabs&&content&&!tabs.querySelector('[data-money="profit"]')){const btn=document.createElement('button');btn.type='button';btn.dataset.money='profit';btn.textContent='Прибыль по заказам';tabs.appendChild(btn);btn.onclick=()=>{tabs.querySelectorAll('button').forEach(x=>x.classList.remove('on'));btn.classList.add('on');renderProfitability();};}
function rangeFilter(order){const mode=$('moneyPeriod')?.value||'month';if(mode==='all')return true;let from='',to='';if(mode==='custom'){from=$('moneyFrom')?.value||'';to=$('moneyTo')?.value||'';}else{const now=new Date();from=`${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,'0')}-01`;to=`${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,'0')}-${String(new Date(now.getFullYear(),now.getMonth()+1,0).getDate()).padStart(2,'0')}`;}return(!from||order.date>=from)&&(!to||order.date<=to);}
function orderEconomy(order){const stock=(()=>{try{return JSON.parse(localStorage.getItem('sunStock')||'[]')}catch(_){return[]}})();const map=new Map(stock.map(p=>[`${String(p.name||'').trim().toLowerCase()}|${String(p.unit||'').trim().toLowerCase()}`,Number(p.cost||0)]));const ingredients=(order.lines||[]).reduce((sum,line)=>{let item;try{item=boxes.find(x=>String(x.id)===String(line.id));}catch(_){item=null;}return sum+(item?.ingredients||[]).reduce((s,p)=>s+Number(p?.[1]||0)*Number(line.qty||0)*Number(map.get(`${String(p?.[0]||'').trim().toLowerCase()}|${String(p?.[2]||'').trim().toLowerCase()}`)||0),0);},0);const c=order.costs||{},cost=ingredients+Number(c.delivery||0)+Number(c.labor||0)+Number(c.other||0),total=Number(order.total||0)||orderTotalFallback(order),profit=total-cost;return{ingredients,cost,total,profit,margin:total?profit/total*100:0,paid:Math.max(0,Number(order.prepayment||0))};}
function orderTotalFallback(order){return (order.lines||[]).reduce((s,l)=>{let item;try{item=boxes.find(x=>String(x.id)===String(l.id));}catch(_){item=null;}return s+Number(item?.price||0)*Number(l.qty||0)},0);}
function renderProfitability(){const list=(()=>{try{return orders}catch(_){return[]}})().filter(o=>(o.status||'Новый')!=='Отменён'&&rangeFilter(o)).sort((a,b)=>String(b.date||'').localeCompare(String(a.date||'')));const rows=list.map(o=>({o,e:orderEconomy(o)}));const total=rows.reduce((s,x)=>s+x.e.total,0),cost=rows.reduce((s,x)=>s+x.e.cost,0),profit=total-cost,margin=total?profit/total*100:0,debt=rows.reduce((s,x)=>s+Math.max(0,x.e.total-x.e.paid),0);content.innerHTML=`<div class="money-summary" style="grid-template-columns:repeat(4,minmax(130px,1fr))"><div><small>Продажи</small><b>${fmt(total)}</b></div><div><small>Себестоимость</small><b>${fmt(cost)}</b></div><div><small>Валовая прибыль</small><b>${fmt(profit)} · ${margin.toFixed(1)}%</b></div><div><small>Долг клиентов</small><b>${fmt(debt)}</b></div></div><div style="overflow:auto;margin-top:14px"><table class="profit-table"><thead><tr><th>Заказ</th><th>Дата</th><th class="num">Продажа</th><th class="num">Расходы</th><th class="num">Прибыль</th><th class="num">Маржа</th><th class="num">Долг</th></tr></thead><tbody>${rows.map(({o,e})=>`<tr><td><button class="mini" type="button" data-profit-order="${esc(o.id)}">${esc(o.id)} · ${esc(o.event||'Заказ')}</button></td><td>${esc(o.date||'')}</td><td class="num">${fmt(e.total)}</td><td class="num">${fmt(e.cost)}</td><td class="num"><b>${fmt(e.profit)}</b></td><td class="num"><span class="profit-badge ${e.margin<20?'bad':''}">${e.margin.toFixed(1)}%</span></td><td class="num">${fmt(Math.max(0,e.total-e.paid))}</td></tr>`).join('')||'<tr><td colspan="7">Нет заказов за выбранный период.</td></tr>'}</tbody></table></div>`;content.querySelectorAll('[data-profit-order]').forEach(b=>b.onclick=()=>window.openOrder?.(Number(b.dataset.profitOrder)));}
loadEconomy();
})();
/* ===== Print center, box photos, labels and QR codes ===== */
(() => {
'use strict';
if(document.getElementById('sun-print-center'))return;
const $=id=>document.getElementById(id),qsa=(s,r=document)=>[...r.querySelectorAll(s)],esc=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
const money=v=>`${Number(v||0).toLocaleString('ru-RU')}`,settingsKey='sunPrintSettingsV1';
const readSettings=()=>Object.assign({photosPerPage:8,showNames:true,showQty:true,showComposition:false,startNewPage:true,photosOnly:false},(()=>{try{return JSON.parse(localStorage.getItem(settingsKey)||'{}')}catch(_){return{}}})());
const saveSettings=s=>localStorage.setItem(settingsKey,JSON.stringify(s));
const getOrders=()=>{try{return orders}catch(_){return[]}},getBoxes=()=>{try{return boxes}catch(_){return[]}};
const orderTotal=o=>Number(o.total||0)||((o.lines||[]).reduce((s,l)=>{const b=getBoxes().find(x=>String(x.id)===String(l.id));return s+Number(b?.price||0)*Number(l.qty||0)},0));
const paid=o=>Math.max(0,Number(o.prepayment||0));
const style=document.createElement('style');style.textContent=`
.print-center-head{display:flex;justify-content:space-between;gap:12px;align-items:flex-start;flex-wrap:wrap}.print-center-grid{display:grid;grid-template-columns:minmax(280px,.8fr) minmax(0,1.4fr);gap:12px;margin-top:14px}.print-card{border:1px solid var(--l);border-radius:11px;background:#fff;padding:15px}.print-card h2{margin:0 0 10px;color:var(--n);font-size:18px}.print-settings{display:grid;gap:8px}.print-check{display:flex;flex-direction:row;align-items:center;gap:8px;color:#203040}.print-check input{width:18px;height:18px}.print-order-summary{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin:10px 0}.print-order-summary>div{background:#f6f7f7;border-radius:8px;padding:10px}.print-order-summary small{display:block;color:#68717a}.print-order-summary b{display:block;color:#15364c;margin-top:3px}.print-help{padding:10px 12px;background:#fffaf0;border-left:4px solid #c99a32;color:#596a74;margin-top:12px}
.sun-custom-doc{font-family:Arial,sans-serif;color:#203040}.sun-custom-head{display:flex;justify-content:space-between;gap:15px;border-bottom:2px solid #c99a32;padding-bottom:10px;margin-bottom:14px}.sun-custom-brand{display:flex;align-items:center;gap:9px}.sun-custom-brand img{width:68px;height:50px;object-fit:contain}.sun-custom-brand b{display:block;color:#15364c;font-size:18px}.sun-custom-title{text-align:right}.sun-custom-title h2{margin:0;color:#15364c}.sun-custom-title p{margin:4px 0;color:#68717a}.sun-photo-sheet{display:grid;grid-template-columns:repeat(var(--photo-cols,4),1fr);gap:8px}.sun-photo-sheet-card{border:1px solid #d9dde0;border-radius:7px;padding:6px;break-inside:avoid;background:#fff}.sun-photo-sheet-card img{width:100%;height:160px;object-fit:cover;border-radius:5px}.sun-photo-sheet-card b{display:block;font-size:11px;line-height:1.2;margin-top:5px}.sun-photo-sheet-card small{display:block;color:#68717a;font-size:10px;margin-top:3px}.sun-label-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:7mm 5mm}.sun-label{border:1px solid #333;border-radius:3mm;padding:4mm;min-height:58mm;display:grid;grid-template-columns:1fr 32mm;gap:3mm;break-inside:avoid}.sun-label h3{margin:0 0 2mm;font-size:12pt}.sun-label p{margin:1mm 0;font-size:9pt}.sun-label img.qr{width:30mm;height:30mm;image-rendering:pixelated}.sun-courier-grid{display:grid;grid-template-columns:1fr 1fr;gap:9px}.sun-courier-box{border:1px solid #d9dde0;border-radius:8px;padding:10px}.sun-courier-box h3{margin:0 0 6px;color:#15364c;font-size:15px}.sun-payment-table{width:100%;border-collapse:collapse}.sun-payment-table th,.sun-payment-table td{border:1px solid #cfd5da;padding:7px}.sun-payment-table td:last-child,.sun-payment-table th:last-child{text-align:right}.sun-custom-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:16px;position:sticky;bottom:0;background:#fff;padding:10px 0}
@media(max-width:780px){.print-center-grid{grid-template-columns:1fr}.print-order-summary{grid-template-columns:1fr 1fr}.sun-photo-sheet{grid-template-columns:repeat(2,1fr)!important}.sun-courier-grid{grid-template-columns:1fr}.sun-label-grid{grid-template-columns:1fr}}
@media print{body *{visibility:hidden!important}#preview,#preview *{visibility:visible!important}#preview{display:block!important;position:absolute!important;inset:0!important;background:#fff!important;padding:0!important}#preview .dialog{width:100%!important;max-width:none!important;max-height:none!important;overflow:visible!important;padding:8mm!important;box-shadow:none!important}.sun-custom-actions{display:none!important}.sun-photo-sheet-card img{height:42mm}.sun-label-grid{grid-template-columns:repeat(3,1fr)!important}.sun-label{min-height:58mm!important}.sun-custom-doc{font-size:10pt}@page{size:A4 portrait;margin:9mm}}
`;document.head.appendChild(style);
const view=document.createElement('section');view.id='sun-print-center';view.className='view page';document.body.appendChild(view);
const nav=document.querySelector('header nav'),group=nav?.querySelector('.sun-nav-work')||nav,button=document.createElement('button');button.type='button';button.textContent='Печать';button.dataset.navLabel='Печать';button.className='sun-nav-button nav-print';group?.appendChild(button);button.onclick=()=>{SunEnterprise.openView(view,button);render();};
let returnToPrintCenterAfterPreview=false;
if(!window.__sunPrintClosePatched){
const originalCloseModal=window.closeModal;
window.closeModal=function(id){
originalCloseModal(id);
if(id==='preview' && window.__sunPrintPreviewReturn){
window.__sunPrintPreviewReturn=false;
returnToPrintCenterAfterPreview=false;
SunEnterprise.openView(view,button);
}
};
window.__sunPrintClosePatched=true;
}
function orderOptions(){return getOrders().slice().sort((a,b)=>`${b.date||''}T${b.time||''}`.localeCompare(`${a.date||''}T${a.time||''}`)).map(o=>`<option value="${esc(o.id)}">№${esc(o.id)} · ${esc(o.date||'—')} · ${esc(o.event||'Заказ')}</option>`).join('');}
function selectedOrder(){return getOrders().find(o=>String(o.id)===String($('printOrder').value));}
function currentSettings(){return{photosPerPage:Number($('printPhotosPerPage').value||8),showNames:$('printShowNames').checked,showQty:$('printShowQty').checked,showComposition:$('printShowComposition').checked,startNewPage:$('printStartNewPage').checked,photosOnly:$('printPhotosOnly').checked};}
function renderSummary(){const o=selectedOrder(),host=$('printOrderSummary');if(!o){host.innerHTML='<p class="hint">Выберите заказ.</p>';return;}host.innerHTML=`<div><small>Сумма</small><b>${money(orderTotal(o))}</b></div><div><small>Оплачено</small><b>${money(paid(o))}</b></div><div><small>Остаток</small><b>${money(Math.max(0,orderTotal(o)-paid(o)))}</b></div>`;}
function render(){const s=readSettings();view.innerHTML=`<div class="print-center-head"><div><h1>Центр печати</h1><p class="hint" style="margin:5px 0 0">Бланки, чеки, заготовки, фотографии, лист курьеру и этикетки.</p></div><button class="outline" id="printRefresh" type="button">Обновить список</button></div><div class="print-center-grid"><section class="print-card"><h2>Документ</h2><label>Заказ<select id="printOrder"><option value="">Выберите заказ</option>${orderOptions()}</select></label><label style="margin-top:9px">Что подготовить<select id="printType"><option value="order">Бланк заказа</option><option value="receipt">Товарный чек</option><option value="prep">Заготовки + фотографии</option><option value="photos">Только фотографии боксов</option><option value="courier">Лист курьеру</option><option value="labels">Этикетки с QR</option></select></label><div class="print-order-summary" id="printOrderSummary"></div><div class="actions"><button class="primary" id="printPreviewBtn" type="button">Предварительный просмотр</button></div><div class="print-help">В предварительном просмотре нажмите «Печать / PDF». В системном окне выберите принтер или «Сохранить как PDF».</div></section><section class="print-card"><h2>Настройки фотографий</h2><div class="print-settings"><label>Фотографий на лист<select id="printPhotosPerPage"><option value="4" ${s.photosPerPage===4?'selected':''}>4 — крупно</option><option value="6" ${s.photosPerPage===6?'selected':''}>6 — средне</option><option value="8" ${s.photosPerPage===8?'selected':''}>8 — компактно</option></select></label><label class="print-check"><input id="printShowNames" type="checkbox" ${s.showNames?'checked':''}> Показывать названия</label><label class="print-check"><input id="printShowQty" type="checkbox" ${s.showQty?'checked':''}> Показывать количество</label><label class="print-check"><input id="printShowComposition" type="checkbox" ${s.showComposition?'checked':''}> Показывать краткий состав</label><label class="print-check"><input id="printStartNewPage" type="checkbox" ${s.startNewPage?'checked':''}> Начинать фотографии с нового листа</label><label class="print-check"><input id="printPhotosOnly" type="checkbox" ${s.photosOnly?'checked':''}> В заготовках печатать только фотографии</label></div><p class="hint">Настройки сохраняются автоматически и применяются к следующей печати.</p></section></div>`;
$('printOrder').onchange=renderSummary;$('printRefresh').onclick=render;$('printPreviewBtn').onclick=preview;
['printPhotosPerPage','printShowNames','printShowQty','printShowComposition','printStartNewPage','printPhotosOnly'].forEach(id=>$(id).onchange=()=>saveSettings(currentSettings()));
const latest=getOrders().slice().sort((a,b)=>`${b.date||''}T${b.time||''}`.localeCompare(`${a.date||''}T${a.time||''}`))[0];if(latest){$('printOrder').value=String(latest.id);renderSummary();}
}
function showDoc(html){const dialog=document.querySelector('#preview .dialog');if(!dialog)return;dialog.innerHTML=html;modal('preview');setTimeout(()=>qsa('canvas[data-sun-qr]',dialog).forEach(canvas=>{const text=decodeURIComponent(canvas.dataset.sunQr||'');if(window.SunQR)window.SunQR.toCanvas(canvas,text,170,4);}),30);}
function header(o,title,subtitle=''){return `<div class="sun-custom-head"><div class="sun-custom-brand"><img src="${window.CateriumBranding.logoHTML()}"><div><b>${window.CateriumBranding.nameHTML()}</b><small>${window.CateriumBranding.cityHTML()}</small></div></div><div class="sun-custom-title"><h2>${esc(title)}</h2><p>Заказ №${esc(o.id)} · ${esc(o.date||'—')} ${esc(o.time||'')}</p>${subtitle?`<p>${esc(subtitle)}</p>`:''}</div></div>`;}
const actions=()=>`<div class="sun-custom-actions"><button class="primary" type="button" onclick="window.print()">Печать / PDF</button><button class="outline" type="button" onclick="closeModal('preview')">Закрыть</button></div>`;
function photoCards(o,s){const selected=(o.lines||[]).map(l=>({item:getBoxes().find(x=>String(x.id)===String(l.id)),qty:Number(l.qty||0)})).filter(x=>x.item&&[0,5].includes(Number(x.item.category||0))&&x.item.photo);const cols=s.photosPerPage===4?2:s.photosPerPage===6?3:4;return `<div class="sun-photo-sheet" style="--photo-cols:${cols}">${selected.map(({item,qty})=>`<article class="sun-photo-sheet-card"><img src="${esc(item.photo)}" alt="${esc(item.name)}">${s.showNames?`<b>${esc(item.name)}</b>`:''}${s.showQty?`<small>Количество боксов: × ${esc(qty)}</small>`:''}${s.showComposition?`<small>${esc((item.composition||[]).slice(0,3).join(' · '))}</small>`:''}</article>`).join('')||'<p class="hint">В заказе нет боксов с фотографиями.</p>'}</div>`;}
function photosDoc(o){const s=currentSettings();showDoc(`<div class="sun-custom-doc">${header(o,'Фотографии боксов',o.event||'')}${photoCards(o,s)}${actions()}</div>`);}
function courierDoc(o){const route=o.routePreview,routeUrl=window.SunDeliveryRoute?.yandexRouteUrl?window.SunDeliveryRoute.yandexRouteUrl(route,o.address):window.SunStaticMap?window.SunStaticMap.routeUrl(o.geo,o.address):`https://yandex.ru/maps/?text=${encodeURIComponent(o.address||'')}`;const mapHtml=route&&window.SunDeliveryRoute?.routeMedia?`<div class="sun-doc-route"><div class="sun-doc-route-head"><b>Маршрут доставки</b><span>Телези, 20 → ${esc(o.address||'')}</span></div><div class="sun-route-preview">${window.SunDeliveryRoute.routeMedia(route,760,250)}</div><div class="sun-doc-route-foot">Маршрут по дорогам · © OpenStreetMap contributors</div></div>`:'';showDoc(`<div class="sun-custom-doc">${header(o,'Лист курьеру',o.event||'')}<div class="sun-courier-grid"><div class="sun-courier-box"><h3>Доставка</h3><p><b>Адрес:</b> ${esc(o.address||'—')}</p><p><b>Время:</b> ${esc(o.time||'—')}</p><p><b>Клиент:</b> ${esc(o.contact||'—')}</p><p><b>Телефон:</b> ${esc(o.phone||'—')}</p><p><b>Комментарий:</b> ${esc(o.courierNote||o.note||'—')}</p></div><div class="sun-courier-box"><h3>Оплата</h3><p><b>Сумма:</b> ${money(orderTotal(o))}</p><p><b>Оплачено:</b> ${money(paid(o))}</p><p><b>Получить:</b> ${money(Math.max(0,orderTotal(o)-paid(o)))}</p><p><a href="${esc(routeUrl)}">Открыть маршрут в Яндекс Картах</a></p><canvas data-sun-qr="${encodeURIComponent(routeUrl)}" style="width:38mm;height:38mm"></canvas></div></div>${mapHtml}<h3 style="margin-top:16px">Состав заказа</h3><table class="sun-payment-table"><tr><th>Позиция</th><th>Количество</th></tr>${(o.lines||[]).map(l=>{const b=getBoxes().find(x=>String(x.id)===String(l.id));return`<tr><td>${esc(b?.name||l.id)}</td><td>${esc(l.qty)}</td></tr>`}).join('')}</table>${actions()}</div>`);}
function labelsDoc(o){const labels=(o.lines||[]).map(l=>({item:getBoxes().find(x=>String(x.id)===String(l.id)),qty:Number(l.qty||0)})).filter(x=>x.item).flatMap(({item,qty})=>Array.from({length:Math.max(1,qty)},(_,i)=>({item,index:i+1,total:qty})));showDoc(`<div class="sun-custom-doc">${header(o,'Этикетки на коробки',o.event||'')}<div class="sun-label-grid">${labels.map(({item,index,total})=>{const code=`SUN-ORDER-${o.id};BOX-${item.id};UNIT-${index}-OF-${total}`;return`<article class="sun-label"><div><h3>${esc(item.name)}</h3><p><b>Заказ ${esc(o.id)}</b></p><p>${esc(o.contact||'Клиент')} · ${esc(o.date||'')} ${esc(o.time||'')}</p><p>Коробка ${index} из ${total}</p><p>${esc(item.weight||'')}</p></div><canvas data-sun-qr="${encodeURIComponent(code)}"></canvas></article>`}).join('')||'<p class="hint">В заказе нет позиций.</p>'}</div>${actions()}</div>`);}
function paymentsDoc(o){const list=Array.isArray(o.payments)?o.payments:[];showDoc(`<div class="sun-custom-doc">${header(o,'История платежей',o.contact||'')}<table class="sun-payment-table"><thead><tr><th>Дата</th><th>Способ</th><th>Комментарий</th><th>Сумма</th></tr></thead><tbody>${list.map(p=>`<tr><td>${esc(p.date||'—')}</td><td>${esc(p.method||'—')}</td><td>${esc(p.note||'')}</td><td>${p.status==='refund'?'':''}${money(p.amount)}</td></tr>`).join('')||'<tr><td colspan="4">Платежей нет</td></tr>'}<tr><td colspan="3"><b>Итого получено</b></td><td><b>${money(paid(o))}</b></td></tr><tr><td colspan="3"><b>Остаток</b></td><td><b>${money(Math.max(0,orderTotal(o)-paid(o)))}</b></td></tr></tbody></table>${actions()}</div>`);}
async function preview(){const o=selectedOrder();if(!o)return SunEnterprise.toast('Выберите заказ.','warn');saveSettings(currentSettings());const type=$('printType').value;returnToPrintCenterAfterPreview=true;window.__sunPrintPreviewReturn=true;if(type==='photos')return photosDoc(o);if(type==='courier')return courierDoc(o);if(type==='labels')return labelsDoc(o);if(type==='payments')return paymentsDoc(o);window.openOrder?.(Number(o.id));await new Promise(r=>setTimeout(r,60));if(type==='prep'){window.openPreview?.();return;}const text=type==='order'?'Бланк заказа':'Товарный чек';const btn=qsa('#new .actions button').find(b=>b.textContent.trim()===text);if(btn)btn.click();else{window.__sunPrintPreviewReturn=false;returnToPrintCenterAfterPreview=false;SunEnterprise.openView(view,button);SunEnterprise.toast(`Кнопка «${text}» не найдена.`,'error');}}
window.sunOpenPrintCenterForOrder=id=>{button.click();setTimeout(()=>{$('printOrder').value=String(id);renderSummary();},30);};
})();
/* ===== Courier route planner ===== */
(() => {
'use strict';
if(document.getElementById('sun-routes-view'))return;
const $=id=>document.getElementById(id),qsa=(s,r=document)=>[...r.querySelectorAll(s)],esc=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
const ROUTES_KEY='sunRoutePlansV1',BASE_KEY='sunRouteBaseV1';
const getOrders=()=>{try{return orders}catch(_){return[]}},getEmployees=()=>{try{return JSON.parse(localStorage.sunEmployees||'[]')}catch(_){return[]}};
const active=o=>!['Отменён','Отдан заказчику'].includes(o.status||'Новый');
const validGeo=o=>o?.geo&&Number.isFinite(Number(o.geo.lat))&&Number.isFinite(Number(o.geo.lon));
const plans=()=>{try{return JSON.parse(localStorage.getItem(ROUTES_KEY)||'{}')}catch(_){return{}}};
const savePlans=p=>localStorage.setItem(ROUTES_KEY,JSON.stringify(p));
const base=()=>({address:'Ленинградская область, Ломоносовский район, деревня Телези, 20',lat:59.701058,lon:29.977269});
const 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));};
const style=document.createElement('style');style.textContent=`
.route-head{display:flex;justify-content:space-between;gap:12px;align-items:flex-start;flex-wrap:wrap}.route-toolbar{display:grid;grid-template-columns:160px 220px 1fr auto;gap:9px;align-items:end;margin:14px 0}.route-summary{display:grid;grid-template-columns:repeat(5,1fr);gap:8px;margin-bottom:12px}.route-summary>div{background:#f5f7f7;border-radius:8px;padding:10px}.route-summary small{display:block;color:#68717a}.route-summary b{display:block;color:#15364c;font-size:18px;margin-top:3px}.route-layout{display:grid;grid-template-columns:minmax(0,1.25fr) minmax(280px,.75fr);gap:12px}.route-list,.route-side{border:1px solid var(--l);border-radius:10px;background:#fff;padding:13px}.route-row{display:grid;grid-template-columns:40px 80px 1fr 100px 120px;gap:8px;align-items:center;border-bottom:1px solid #e7eaeb;padding:10px 0}.route-row:last-child{border-bottom:0}.route-order-buttons{display:flex;flex-direction:column;gap:3px}.route-order-buttons button{height:25px;border:1px solid #cfd5da;background:#fff;border-radius:4px}.route-row h3{margin:0;color:#15364c;font-size:15px}.route-row p{margin:3px 0;color:#68717a;font-size:12px}.route-row .no-geo{color:#9c3d3d;font-weight:700}.route-side h2{font-size:17px;color:#15364c;margin:0 0 10px}.route-side .actions{flex-direction:column}.route-side .actions>*{width:100%;text-align:center}.route-base-grid{display:grid;grid-template-columns:1fr 1fr;gap:7px}.route-base-grid .wide{grid-column:1/-1}.route-empty{padding:28px;text-align:center;color:#879198}.route-print-table{width:100%;border-collapse:collapse}.route-print-table th,.route-print-table td{border:1px solid #cfd5da;padding:6px}.route-print-table th{background:#f4f6f7}.route-print-table .num{text-align:right}
@media(max-width:950px){.route-toolbar{grid-template-columns:1fr 1fr}.route-layout{grid-template-columns:1fr}.route-summary{grid-template-columns:1fr 1fr}.route-row{grid-template-columns:34px 70px 1fr}.route-row>:nth-child(4),.route-row>:nth-child(5){grid-column:3}}
@media(max-width:560px){.route-toolbar{grid-template-columns:1fr}.route-summary{grid-template-columns:1fr 1fr}.route-row{grid-template-columns:34px 1fr}.route-row>:not(:first-child){grid-column:2}}
`;document.head.appendChild(style);
const view=document.createElement('section');view.id='sun-routes-view';view.className='view page';document.body.appendChild(view);
const nav=document.querySelector('header nav'),group=nav?.querySelector('.sun-nav-operations')||nav,button=document.createElement('button');button.type='button';button.textContent='Маршруты';button.dataset.navLabel='Маршруты';button.className='sun-nav-button nav-routes';group?.appendChild(button);button.onclick=()=>{SunEnterprise.openView(view,button);render();};
function employees(){return getEmployees().filter(e=>e.active!==false&&String(e.role||'').toLowerCase().includes('курьер'));}
function selectedKey(){return `${$('routeDate').value}|${$('routeCourier').value||'all'}`;}
function ordersFor(){const date=$('routeDate').value,courier=$('routeCourier').value;let list=getOrders().filter(o=>active(o)&&o.date===date&&(o.address||validGeo(o)));if(courier)list=list.filter(o=>(o.staffIds||[]).map(String).includes(String(courier)));const p=plans(),ids=p[selectedKey()]||[];const map=new Map(list.map(o=>[String(o.id),o]));const 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 persistOrder(list){const p=plans();p[selectedKey()]=list.map(o=>String(o.id));savePlans(p);}
function routePoints(list){const b=base(),pts=[];if(Number.isFinite(Number(b.lat))&&Number.isFinite(Number(b.lon))&&b.lat!==''&&b.lon!=='')pts.push({lat:Number(b.lat),lon:Number(b.lon),label:'База'});list.filter(validGeo).forEach(o=>pts.push({lat:Number(o.geo.lat),lon:Number(o.geo.lon),label:`${o.id}`}));return pts;}
function routeDistance(list){const pts=routePoints(list);let total=0;for(let i=1;i<pts.length;i++)total+=hav(pts[i-1],pts[i]);return total;}
function yandexUrl(list){const pts=routePoints(list);if(!pts.length)return'https://yandex.ru/maps/';return`https://yandex.ru/maps/?rtext=${encodeURIComponent(pts.map(p=>`${p.lat},${p.lon}`).join('~'))}&rtt=auto`;}
function courierName(){return employees().find(e=>String(e.id)===String($('routeCourier').value))?.name||'Все курьеры';}
function render(){const b=base(),today=new Date().toISOString().slice(0,10);view.innerHTML=`<div class="route-head"><div><h1>Маршруты курьеров</h1><p class="hint" style="margin:5px 0 0">Порядок доставок, километраж, маршрутный лист и ссылка для курьера.</p></div></div><div class="route-toolbar"><label>Дата<input id="routeDate" type="date" value="${today}"></label><label>Курьер<select id="routeCourier"><option value="">Все курьеры</option>${employees().map(e=>`<option value="${esc(e.id)}">${esc(e.name)}</option>`).join('')}</select></label><label>Поиск<input id="courierRouteSearch" type="search" placeholder="Клиент, заказ или адрес"></label><button class="outline" id="routeRefresh" type="button">Обновить</button></div><div id="routeContent"></div>`;$('routeDate').onchange=renderRoute;$('routeCourier').onchange=renderRoute;$('courierRouteSearch').oninput=renderRoute;$('routeRefresh').onclick=renderRoute;renderRoute();}
function renderRoute(){const all=ordersFor(),query=($('courierRouteSearch')?.value||'').trim().toLowerCase(),list=query?all.filter(o=>[o.id,o.event,o.contact,o.phone,o.address].join(' ').toLowerCase().includes(query)):all,geoCount=all.filter(validGeo).length,distance=routeDistance(all),total=all.reduce((s,o)=>s+Number(o.total||0),0),b=base();$('routeContent').innerHTML=`<div class="route-summary"><div><small>Доставок</small><b>${all.length}</b></div><div><small>Точек на карте</small><b>${geoCount}</b></div><div><small>Без координат</small><b>${all.length-geoCount}</b></div><div><small>Примерно км</small><b>${distance.toLocaleString('ru-RU',{maximumFractionDigits:1})}</b></div><div><small>Сумма заказов</small><b>${total.toLocaleString('ru-RU')} ₽</b></div></div><div class="route-layout"><section class="route-list"><h2 style="margin:0 0 8px;color:#15364c;font-size:17px">Порядок доставок · ${esc(courierName())}</h2>${list.length?list.map((o,index)=>`<div class="route-row" data-route-id="${esc(o.id)}"><div class="route-order-buttons"><button type="button" data-route-up="${esc(o.id)}" ${index===0?'disabled':''}>↑</button><button type="button" data-route-down="${esc(o.id)}" ${index===list.length-1?'disabled':''}>↓</button></div><div><b>${esc(o.time||'—')}</b><p>№${esc(o.id)}</p></div><div><h3>${esc(o.event||'Заказ')}</h3><p>${esc(o.contact||'Клиент не указан')} · ${esc(o.phone||'')}</p><p>${esc(o.address||'Адрес не указан')}</p>${validGeo(o)?'':`<p class="no-geo">Точка не подтверждена</p>`}</div><div><b>${Number(o.total||0).toLocaleString('ru-RU')} ₽</b><p>${esc(o.status||'Новый')}</p></div><div><button class="mini" type="button" data-route-open="${esc(o.id)}">Открыть заказ</button></div></div>`).join(''):'<div class="route-empty">На выбранную дату доставок нет.</div>'}</section><aside class="route-side"><h2>Начало маршрута</h2><div style="padding:11px 12px;border:1px solid #e3e5eb;border-radius:10px;background:#f8f9fb;margin-bottom:10px"><b style="color:#2d294d">Телези, 20</b><div class="hint" style="margin-top:4px">Постоянная точка старта для всех маршрутов</div></div><div class="actions"><button class="primary" id="routeOpenYandex" type="button">Открыть маршрут в Яндекс Картах</button><button class="outline" id="routeCopy" type="button">Скопировать маршрут курьеру</button><button class="outline" id="routePrint" type="button">Печать маршрутного листа</button></div><p class="hint">На печати маршрут строится по дорогам от Телези, 20 через точки в указанном порядке.</p></aside></div>`;
qsa('[data-route-up]').forEach(btn=>btn.onclick=()=>move(btn.dataset.routeUp,-1));qsa('[data-route-down]').forEach(btn=>btn.onclick=()=>move(btn.dataset.routeDown,1));qsa('[data-route-open]').forEach(btn=>btn.onclick=()=>window.openOrder?.(Number(btn.dataset.routeOpen)));
const refreshAllRouteGeos=async(force=false)=>{for(const o of all){if(!o?.address)continue;try{await window.SunStaticMap?.ensureOrderGeo?.(o,{silent:true,force});}catch(_){}}};
$('routeOpenYandex').onclick=async()=>{const btn=$('routeOpenYandex'),old=btn.textContent;btn.disabled=true;btn.textContent='Проверяем адреса…';try{await refreshAllRouteGeos(true);window.open(yandexUrl(all),'_blank','noopener');renderRoute();}finally{btn.disabled=false;btn.textContent=old;}};
$('routeCopy').onclick=async()=>{const btn=$('routeCopy'),old=btn.textContent;btn.disabled=true;btn.textContent='Проверяем адреса…';try{await refreshAllRouteGeos(true);await copyRoute(all);renderRoute();}finally{btn.disabled=false;btn.textContent=old;}};
$('routePrint').onclick=async()=>{const btn=$('routePrint');const old=btn.textContent;btn.disabled=true;btn.textContent='Строим маршрут…';try{await printRoute(all);renderRoute();}finally{btn.disabled=false;btn.textContent=old;}};
}
function move(id,delta){const list=ordersFor(),index=list.findIndex(o=>String(o.id)===String(id)),next=index+delta;if(index<0||next<0||next>=list.length)return;[list[index],list[next]]=[list[next],list[index]];persistOrder(list);renderRoute();}
async function copyRoute(list){const text=[`Маршрут ${$('routeDate').value} · ${courierName()}`,base().address?`Старт: ${base().address}`:'',...list.map((o,i)=>`${i+1}. ${o.time||'—'} · Заказ №${o.id} · ${o.contact||''}\n${o.address||''}\n${o.phone||''}${o.courierNote?`\nКомментарий: ${o.courierNote}`:''}`),`Яндекс Карты: ${yandexUrl(list)}`].filter(Boolean).join('\n\n');try{await navigator.clipboard.writeText(text);SunEnterprise.toast('Маршрут скопирован.','success');}catch(_){const ta=document.createElement('textarea');ta.value=text;document.body.appendChild(ta);ta.select();document.execCommand('copy');ta.remove();SunEnterprise.toast('Маршрут скопирован.','success');}}
async function buildPrintedRoute(list){
const b=base(),stops=[];
for(const o of list){
let geo=null;
if(window.SunStaticMap?.ensureOrderGeo){try{geo=await window.SunStaticMap.ensureOrderGeo(o,{silent:true,force:true});}catch(_){}}
if(!geo&&validGeo(o)) geo=o.geo;
if(geo&&Number.isFinite(Number(geo.lat))&&Number.isFinite(Number(geo.lon)))stops.push({lat:Number(geo.lat),lon:Number(geo.lon),label:`${o.id}`,orderId:o.id,address:o.address||''});
}
if(!stops.length)return null;
const coords=[{lat:Number(b.lat),lon:Number(b.lon)},...stops];
const url=`https://router.project-osrm.org/route/v1/driving/${coords.map(x=>`${x.lon},${x.lat}`).join(';')}?overview=full&geometries=geojson&steps=false`;
try{
const response=await sunFetchWithTimeout(url,{mode:'cors',credentials:'omit'},8000);if(!response.ok)throw new Error('route');
const data=await response.json(),r=data?.routes?.[0],geometry=r?.geometry?.coordinates;if(!r||!Array.isArray(geometry)||geometry.length<2)throw new Error('route');
const simplified=geometry.length>320?geometry.filter((_,i)=>i%Math.ceil(geometry.length/320)===0||i===geometry.length-1):geometry;
return {origin:{lat:Number(b.lat),lon:Number(b.lon),address:b.address},destination:{lat:stops[stops.length-1].lat,lon:stops[stops.length-1].lon,address:stops[stops.length-1].address},stops,coords:simplified.map(c=>[Number(c[0]),Number(c[1])]),distance:Number(r.distance||0),duration:Number(r.duration||0),sourceAddress:'route-sheet',routeVersion:3};
}catch(_){return null;}
}
function routeSheetDistance(v){return `${(Number(v||0)/1000).toLocaleString('ru-RU',{maximumFractionDigits:1})} км`;}
function routeSheetTime(v){const n=Math.max(1,Math.round(Number(v||0)/60));return n>=60?`${Math.floor(n/60)} ч ${n%60?`${n%60} мин`:''}`:`${n} мин`;}
async function printRoute(list){
const dialog=document.querySelector('#preview .dialog'),b=base(),route=await buildPrintedRoute(list);
const map=route&&window.SunDeliveryRoute?.routeMedia?`<div class="sun-route-sheet-map" style="margin:14px 0 16px;border:1px solid #d9dde5;border-radius:12px;overflow:hidden;break-inside:avoid"><div style="padding:9px 11px;background:#f7f6fb;display:flex;justify-content:space-between;gap:10px"><b>Маршрут на карте</b><span>${routeSheetDistance(route.distance)} · примерно ${routeSheetTime(route.duration)}</span></div><div class="sun-route-preview" style="height:330px;border:0;border-radius:0">${window.SunDeliveryRoute.routeMedia(route,980,330)}</div><div style="padding:7px 11px;font-size:10px;color:#747b86">A — Телези, 20 · далее точки 1, 2, 3… в порядке маршрутного листа</div></div>`:`<div style="margin:14px 0;padding:12px;border:1px dashed #cfd4dc;border-radius:10px;color:#737b88">Не удалось загрузить схему маршрута. Таблица и ссылка на Яндекс Карты доступны ниже.</div>`;
dialog.innerHTML=`<div class="sun-custom-doc"><div class="sun-custom-head"><div class="sun-custom-brand"><img src="${window.CateriumBranding.logoHTML()}"><div><b>${window.CateriumBranding.nameHTML()}</b><small>Маршрутный лист</small></div></div><div class="sun-custom-title"><h2>${esc($('routeDate').value)}</h2><p>${esc(courierName())}</p></div></div><p><b>Старт:</b> ${esc(b.address)}</p>${map}<table class="route-print-table"><thead><tr><th>№</th><th>Время</th><th>Заказ / клиент</th><th>Адрес</th><th>Телефон</th><th>Комментарий</th><th class="num">Получить</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><td>${esc(o.courierNote||o.note||'')}</td><td class="num">${Math.max(0,Number(o.total||0)-Number(o.prepayment||0)).toLocaleString('ru-RU')} ₽</td></tr>`).join('')}</tbody></table><p style="margin-top:12px"><b>Маршрут:</b> <a href="${esc(yandexUrl(list))}">открыть в Яндекс Картах</a></p><div class="sun-custom-actions"><button class="primary" onclick="window.print()">Печать / PDF</button><button class="outline" onclick="closeModal('preview')">Закрыть</button></div></div>`;modal('preview');
}
window.sunOpenRoutes=()=>button.click();
})();
/* ===== EXCEL (SPREADSHEETML) IMPORT / EXPORT ===== */
(() => {
'use strict';
const esc=window.SunSafe.escapeHTML;
const parse=(raw,d=[])=>{try{return JSON.parse(raw)||d}catch(_){return d}};
const paid=o=>Math.max(0,Number(o.prepayment||0));
const orderTotal=o=>{if(Number.isFinite(Number(o.total)))return Number(o.total||0);let boxes=[];try{boxes=window.boxes||[]}catch(_){try{boxes=globalThis.boxes}catch(__){boxes=[]}}if(!boxes.length)boxes=parse(localStorage.getItem('sunBoxes')||'[]');return(o.lines||[]).reduce((s,l)=>s+Number(boxes.find(x=>String(x.id)===String(l.id))?.price||0)*Number(l.qty||0),0)};
function cell(value,type){if(value===null||value===undefined)value='';if(type==='Number')return `<Cell><Data ss:Type="Number">${Number(value||0)}</Data></Cell>`;if(type==='Boolean')return `<Cell><Data ss:Type="Boolean">${value?1:0}</Data></Cell>`;return `<Cell><Data ss:Type="String">${escXml(value)}</Data></Cell>`;}
function sheet(name,headers,rows,types={}){return `<Worksheet ss:Name="${escXml(name)}"><Table><Row>${headers.map(h=>cell(h)).join('')}</Row>${rows.map(row=>`<Row>${headers.map(h=>cell(row[h],types[h])).join('')}</Row>`).join('')}</Table><WorksheetOptions xmlns="urn:schemas-microsoft-com:office:excel"><FreezePanes/><FrozenNoSplit/><SplitHorizontal>1</SplitHorizontal><TopRowBottomPane>1</TopRowBottomPane><ActivePane>2</ActivePane></WorksheetOptions></Worksheet>`;}
function workbookXml(){let orders=[];try{orders=globalThis.orders||[]}catch(_){orders=parse(localStorage.getItem('sunOrders')||'[]')}if(!orders.length)orders=parse(localStorage.getItem('sunOrders')||'[]');let boxes=[];try{boxes=globalThis.boxes||[]}catch(_){boxes=parse(localStorage.getItem('sunBoxes')||'[]')}if(!boxes.length)boxes=parse(localStorage.getItem('sunBoxes')||'[]');const stock=parse(localStorage.getItem('sunStock')||'[]'),finance=parse(localStorage.getItem('sunFinanceRecordsV2')||'[]'),team=parse(localStorage.getItem('sunEmployees')||'[]'),suppliers=parse(localStorage.getItem('sunSuppliers')||'[]');const clients=new Map();orders.forEach(o=>{const k=String(o.phone||o.contact||'').trim();if(!k)return;if(!clients.has(k))clients.set(k,{Имя:o.contact||'',Телефон:o.phone||'',Адреса:new Set(),Заказов:0,Сумма:0});const c=clients.get(k);if(o.address)c.Адреса.add(o.address);if((o.status||'Новый')!=='Отменён'){c.Заказов++;c.Сумма+=orderTotal(o);}});
const sheets=[];
let headers=['ID','Мероприятие','Дата','Время','Статус','Клиент','Телефон','Адрес','Сумма','Оплачено','Остаток','Гостей','Ответственный','Позиции JSON','Платежи JSON','Расходы JSON','Примечание','Комментарий кухне','Комментарий курьеру','Широта','Долгота'];
sheets.push(sheet('Заказы',headers,orders.map(o=>({'ID':o.id,'Мероприятие':o.event||'','Дата':o.date||'','Время':o.time||'','Статус':o.status||'Новый','Клиент':o.contact||'','Телефон':o.phone||'','Адрес':o.address||'','Сумма':orderTotal(o),'Оплачено':paid(o),'Остаток':Math.max(0,orderTotal(o)-paid(o)),'Гостей':o.guestsCount||'','Ответственный':o.responsible||'','Позиции JSON':JSON.stringify(o.lines||[]),'Платежи JSON':JSON.stringify(o.payments||[]),'Расходы JSON':JSON.stringify(o.costs||{}),'Примечание':o.note||'','Комментарий кухне':o.kitchenNote||'','Комментарий курьеру':o.courierNote||'','Широта':o.geo?.lat||'','Долгота':o.geo?.lon||''})),{'ID':'Number','Сумма':'Number','Оплачено':'Number','Остаток':'Number','Широта':'Number','Долгота':'Number'}));
headers=['ID','Название','Категория','Цена','Цена до скидки','Вес','Раздел','Состав JSON','Описание состава','Фото'];sheets.push(sheet('Каталог',headers,boxes.map(b=>({'ID':b.id,'Название':b.name||'','Категория':Number(b.category||0),'Цена':Number(b.price||0),'Цена до скидки':Number(b.oldPrice||0),'Вес':b.weight||'','Раздел':b.catalogSection||'','Состав JSON':JSON.stringify(b.ingredients||[]),'Описание состава':(b.composition||[]).join(' | '),'Фото':b.photo||''})),{'Категория':'Number','Цена':'Number','Цена до скидки':'Number'}));
headers=['ID','Продукт','Единица','Остаток','Минимум','Закупочная цена','Поставщик ID','Последняя закупка','Последняя цена'];sheets.push(sheet('Склад',headers,stock.map(p=>({'ID':p.id,'Продукт':p.name||'','Единица':p.unit||'','Остаток':Number(p.qty||0),'Минимум':Number(p.min||0),'Закупочная цена':Number(p.cost||0),'Поставщик ID':p.supplierId||'','Последняя закупка':p.lastPurchaseDate||'','Последняя цена':Number(p.lastPurchasePrice||0)})),{'Остаток':'Number','Минимум':'Number','Закупочная цена':'Number','Последняя цена':'Number'}));
headers=['ID','Тип','Дата','Сумма','Категория','Описание','Комментарий'];sheets.push(sheet('Финансы',headers,finance.map(r=>({'ID':r.id,'Тип':r.kind||'','Дата':r.date||'','Сумма':Number(r.amount||0),'Категория':r.category||'','Описание':r.description||'','Комментарий':r.note||''})),{'Сумма':'Number'}));
headers=['ID','Имя','Роль','Телефон','Активен','Комментарий'];sheets.push(sheet('Команда',headers,team.map(e=>({'ID':e.id,'Имя':e.name||'','Роль':e.role||'','Телефон':e.phone||'','Активен':e.active!==false,'Комментарий':e.note||''})),{'Активен':'Boolean'}));
headers=['ID','Название','Категория','Контакт','Телефон','Email','Адрес','Активен','Доставка','Минимальный заказ','Комментарий'];sheets.push(sheet('Поставщики',headers,suppliers.map(s=>({'ID':s.id,'Название':s.name||'','Категория':s.category||'','Контакт':s.contact||'','Телефон':s.phone||'','Email':s.email||'','Адрес':s.address||'','Активен':s.active!==false,'Доставка':s.delivery||s.deliveryTerms||'','Минимальный заказ':Number(s.minOrder||0),'Комментарий':s.note||''})),{'Активен':'Boolean','Минимальный заказ':'Number'}));
headers=['Имя','Телефон','Адреса','Заказов','Сумма'];sheets.push(sheet('Клиенты',headers,[...clients.values()].map(c=>({'Имя':c.Имя,'Телефон':c.Телефон,'Адреса':[...c.Адреса].join(' | '),'Заказов':c.Заказов,'Сумма':c.Сумма})),{'Заказов':'Number','Сумма':'Number'}));
return `<?xml version="1.0" encoding="UTF-8"?><?mso-application progid="Excel.Sheet"?><Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel" xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet"><DocumentProperties xmlns="urn:schemas-microsoft-com:office:office"><Author>${window.CateriumBranding.nameHTML()}</Author><Created>${new Date().toISOString()}</Created></DocumentProperties><Styles><Style ss:ID="Default" ss:Name="Normal"><Alignment ss:Vertical="Bottom"/><Borders/><Font ss:FontName="Arial"/><Interior/><NumberFormat/><Protection/></Style></Styles>${sheets.join('')}</Workbook>`;
}
function download(name,text,type){const blob=new Blob([text],{type}),url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url;a.download=name;document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(url),1000);}
function exportExcel(){download(`sun-catering-${new Date().toISOString().slice(0,10)}.xls`,workbookXml(),'application/vnd.ms-excel;charset=utf-8');window.SunEnterprise?.recordAudit('Экспорт базы','Excel (.xls)');}
function rowsFromWorksheet(ws){const rows=[...ws.getElementsByTagNameNS('*','Row')];if(!rows.length)return[];const values=rows.map(row=>[...row.getElementsByTagNameNS('*','Cell')].map(cell=>cell.getElementsByTagNameNS('*','Data')[0]?.textContent||''));const headers=values.shift()||[];return values.map(vals=>Object.fromEntries(headers.map((h,i)=>[h,vals[i]??''])));}
function sheetByName(doc,name){return [...doc.getElementsByTagNameNS('*','Worksheet')].find(ws=>(ws.getAttribute('ss:Name')||ws.getAttributeNS('urn:schemas-microsoft-com:office:spreadsheet','Name')||ws.getAttribute('Name'))===name);}
const jsonCell=(v,d)=>{try{return JSON.parse(v||'')}catch(_){return d}};
async function importExcel(file){const text=await file.text(),doc=new DOMParser().parseFromString(text,'application/xml');if(doc.querySelector('parsererror'))throw new Error('Файл не является поддерживаемой книгой Excel XML. Импортируйте файл, ранее выгруженный из этого приложения.');const data={};for(const name of ['Заказы','Каталог','Склад','Финансы','Команда','Поставщики']){const ws=sheetByName(doc,name);if(ws)data[name]=rowsFromWorksheet(ws);}if(!Object.keys(data).length)throw new Error('В книге нет поддерживаемых листов.');await window.SunEnterprise?.createBackup('Перед импортом Excel');if(data['Заказы'])localStorage.setItem('sunOrders',JSON.stringify(data['Заказы'].map(r=>({id:Number(r.ID)||r.ID,event:r.Мероприятие,date:r.Дата,time:r.Время,status:r.Статус||'Новый',contact:r.Клиент,phone:r.Телефон,address:r.Адрес,total:Number(r.Сумма||0),prepayment:Number(r.Оплачено||0),guestsCount:r.Гостей,responsible:r.Ответственный,lines:jsonCell(r['Позиции JSON'],[]),payments:jsonCell(r['Платежи JSON'],[]),costs:jsonCell(r['Расходы JSON'],{}),note:r.Примечание,kitchenNote:r['Комментарий кухне'],courierNote:r['Комментарий курьеру'],geo:(r.Широта&&r.Долгота)?{lat:Number(r.Широта),lon:Number(r.Долгота),confirmed:true}:null}))));if(data['Каталог'])localStorage.setItem('sunBoxes',JSON.stringify(data['Каталог'].map(r=>({id:r.ID,name:r.Название,category:Number(r.Категория||0),price:Number(r.Цена||0),oldPrice:Number(r['Цена до скидки']||0)||undefined,weight:r.Вес,guests:r.Гостей,catalogSection:r.Раздел,ingredients:jsonCell(r['Состав JSON'],[]),composition:String(r['Описание состава']||'').split(' | ').filter(Boolean),photo:r.Фото}))));if(data['Склад'])localStorage.setItem('sunStock',JSON.stringify(data['Склад'].map(r=>({id:r.ID,name:r.Продукт,unit:r.Единица,qty:Number(r.Остаток||0),min:Number(r.Минимум||0),cost:Number(r['Закупочная цена']||0),supplierId:r['Поставщик ID'],lastPurchaseDate:r['Последняя закупка'],lastPurchasePrice:Number(r['Последняя цена']||0)}))));if(data['Финансы'])localStorage.setItem('sunFinanceRecordsV2',JSON.stringify(data['Финансы'].map(r=>({id:r.ID,kind:r.Тип,date:r.Дата,amount:Number(r.Сумма||0),category:r.Категория,description:r.Описание,note:r.Комментарий}))));if(data['Команда'])localStorage.setItem('sunEmployees',JSON.stringify(data['Команда'].map(r=>({id:r.ID,name:r.Имя,role:r.Роль,phone:r.Телефон,active:!['0','false','FALSE',''].includes(String(r.Активен)),note:r.Комментарий}))));if(data['Поставщики'])localStorage.setItem('sunSuppliers',JSON.stringify(data['Поставщики'].map(r=>({id:r.ID,name:r.Название,category:r.Категория,contact:r.Контакт,phone:r.Телефон,email:r.Email,address:r.Адрес,active:!['0','false','FALSE',''].includes(String(r.Активен)),delivery:r.Доставка,minOrder:Number(r['Минимальный заказ']||0),note:r.Комментарий}))));window.SunEnterprise?.recordAudit('Импорт базы',`Excel · ${file.name}`);location.reload();}
// Excel tools are intentionally not shown in Settings. JSON backup remains the supported import/export UI.
function inject(){document.getElementById('excelDataCard')?.remove();}
inject();
})();
/* END enterprise-update.js */
</script><script>window.SUN_PORTABLE_HTML=true;</script>
<script>
/* ===== SAFE MODAL BACKDROP v2 =====
Prevents accidental modal closing when a pointer/touch starts inside a dialog
and ends on the dark backdrop, or when the user drags/scrolls on the backdrop.
A deliberate stationary click on the empty backdrop still closes the modal
through the existing application handler. */
(() => {
if (window.__sunSafeModalBackdropV2) return;
window.__sunSafeModalBackdropV2 = true;
let pointerState = null;
const MOVE_LIMIT = 8;
document.addEventListener('pointerdown', event => {
const modal = event.target instanceof Element ? event.target.closest('.modal.on') : null;
if (!modal) {
pointerState = null;
return;
}
pointerState = {
modal,
startedOnBackdrop: event.target === modal,
x: event.clientX,
y: event.clientY,
pointerId: event.pointerId
};
}, true);
document.addEventListener('click', event => {
const target = event.target;
if (!(target instanceof HTMLElement) || !target.classList.contains('modal') || !target.classList.contains('on')) return;
const state = pointerState;
pointerState = null;
// Keyboard-generated click: keep normal behavior.
if (!state) return;
const moved = Math.hypot(event.clientX - state.x, event.clientY - state.y) > MOVE_LIMIT;
const deliberateBackdropClick = state.modal === target && state.startedOnBackdrop && !moved;
// The legacy backdrop handler runs later on document. Stop it unless this was
// a clean click that both started and ended on the empty backdrop.
if (!deliberateBackdropClick) {
event.preventDefault();
event.stopImmediatePropagation();
}
}, true);
document.addEventListener('pointercancel', () => { pointerState = null; }, true);
})();
</script>
<script id="sun-delivery-details-module">
(() => {
const ids=['deliveryStreet','deliveryHouse','deliveryEntrance','deliveryFloor','deliveryApartment','deliveryOffice'];
const $=id=>document.getElementById(id);
const address=$('address');
if(!address || ids.some(id=>!$(id))) return;
const val=id=>$(id).value.trim();
const compose=()=>[
val('deliveryStreet'),
val('deliveryHouse')?`д. ${val('deliveryHouse')}`:'',
val('deliveryEntrance')?`подъезд ${val('deliveryEntrance')}`:'',
val('deliveryFloor')?`этаж ${val('deliveryFloor')}`:'',
val('deliveryApartment')?`кв. ${val('deliveryApartment')}`:'',
val('deliveryOffice')?`офис ${val('deliveryOffice')}`:''
].filter(Boolean).join(', ');
const syncAddress=()=>{
const next=compose();
if(address.value!==next){address.value=next;address.dispatchEvent(new Event('input',{bubbles:true}));}
return next;
};
ids.forEach(id=>$(id).addEventListener('input',syncAddress));
window.sunComposeDeliveryAddress=syncAddress;
const previousSave=window.saveOrder;
if(typeof previousSave==='function') window.saveOrder=function(...args){
syncAddress();
if(typeof draft!=='undefined'&&draft){
draft.deliveryStreet=val('deliveryStreet');
draft.deliveryHouse=val('deliveryHouse');
draft.deliveryEntrance=val('deliveryEntrance');
draft.deliveryFloor=val('deliveryFloor');
draft.deliveryApartment=val('deliveryApartment');
draft.deliveryOffice=val('deliveryOffice');
draft.address=address.value;
}
return previousSave.apply(this,args);
};
const fillFromDraft=()=>{
const d=(typeof draft!=='undefined'&&draft)||{};
const hasParts=['deliveryStreet','deliveryHouse','deliveryEntrance','deliveryFloor','deliveryApartment','deliveryOffice'].some(k=>String(d[k]||'').trim());
$('deliveryStreet').value=hasParts?(d.deliveryStreet||''):(d.address||'');
$('deliveryHouse').value=d.deliveryHouse||'';
$('deliveryEntrance').value=d.deliveryEntrance||'';
$('deliveryFloor').value=d.deliveryFloor||'';
$('deliveryApartment').value=d.deliveryApartment||'';
$('deliveryOffice').value=d.deliveryOffice||'';
if(hasParts) syncAddress(); else {address.value=d.address||'';address.dispatchEvent(new Event('input',{bubbles:true}));}
};
const previousOpen=window.openOrder;
if(typeof previousOpen==='function') window.openOrder=function(...args){const result=previousOpen.apply(this,args);fillFromDraft();return result;};
const previousReset=window.resetDraft;
if(typeof previousReset==='function') window.resetDraft=function(...args){const result=previousReset.apply(this,args);ids.forEach(id=>$(id).value='');address.value='';address.dispatchEvent(new Event('input',{bubbles:true}));return result;};
// New orders begin with empty structured address fields.
if(!(typeof draft!=='undefined'&&draft&&draft.id)) ids.forEach(id=>$(id).value='');
})();
</script>
<style id="sun-youth-sidebar-style">
@media (min-width:901px){
:root{--sun-side:292px!important;--sun-side-collapsed:74px!important;--sy-bg1:#241d45;--sy-bg2:#171d38;--sy-accent:#f2c85f;--sy-violet:#9f8cff;--sy-text:#f8f7ff;--sy-muted:#a9a8c6}
body.sun-enterprise-sidebar{padding-left:var(--sun-side)!important;background:#f5f6fa!important;transition:padding-left .22s cubic-bezier(.2,.8,.2,1)!important}
.sun-enterprise-sidebar header{width:var(--sun-side)!important;background:radial-gradient(circle at 12% 0%,rgba(149,116,255,.32),transparent 30%),radial-gradient(circle at 100% 43%,rgba(73,112,207,.18),transparent 38%),linear-gradient(165deg,#32255d 0%,var(--sy-bg1) 42%,var(--sy-bg2) 100%)!important;padding:16px 14px 14px!important;gap:9px!important;box-shadow:9px 0 34px rgba(33,27,73,.13)!important;border-right:1px solid rgba(255,255,255,.07)!important;overflow-x:visible!important}
.sun-enterprise-sidebar header .brand{min-height:72px!important;padding:5px 7px 13px!important;border-bottom:1px solid rgba(255,255,255,.10)!important;color:#fff!important;font-size:18px!important;display:flex!important;align-items:center!important;letter-spacing:.01em!important}
.sun-enterprise-sidebar header .brand .real-logo{width:56px!important;height:56px!important;margin-right:11px!important;object-fit:contain!important;filter:none!important}
.sun-enterprise-sidebar header nav{gap:3px!important;margin-top:1px!important}
.sun-enterprise-sidebar .sun-nav-home-standalone{margin:3px 3px 7px!important}
.sun-enterprise-sidebar .sun-nav-group{position:relative!important;margin:0 3px!important;padding:30px 0 9px!important;gap:2px!important;border:0!important;border-bottom:1px solid rgba(255,255,255,.09)!important;background:transparent!important;border-radius:0!important}
.sun-enterprise-sidebar .sun-nav-group::before{position:absolute!important;left:11px!important;top:10px!important;color:var(--sy-muted)!important;font-size:9.5px!important;font-weight:800!important;letter-spacing:.12em!important;text-transform:uppercase!important}
.sun-enterprise-sidebar .sun-nav-group::after{display:none!important}
.sun-enterprise-sidebar header nav .sun-nav-button{position:relative!important;width:100%!important;min-height:44px!important;padding:8px 10px!important;justify-content:flex-start!important;border:1px solid transparent!important;border-radius:12px!important;color:#e9e8f6!important;font-size:13.5px!important;font-weight:650!important;letter-spacing:0!important;transition:background .16s ease,border-color .16s ease,color .16s ease,transform .16s ease!important}
.sun-enterprise-sidebar header nav .sun-nav-button::before{content:''!important;width:20px!important;height:20px!important;min-width:20px!important;margin:0 12px 0 0!important;border-radius:0!important;background:currentColor!important;opacity:.86!important;-webkit-mask-image:var(--sun-icon)!important;mask-image:var(--sun-icon)!important;-webkit-mask-repeat:no-repeat!important;mask-repeat:no-repeat!important;-webkit-mask-position:center!important;mask-position:center!important;-webkit-mask-size:20px 20px!important;mask-size:20px 20px!important;box-shadow:none!important}
.sun-enterprise-sidebar header nav .nav-home{--sun-icon:url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%221.9%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M3%2011.5%2012%204l9%207.5%22%2F%3E%3Cpath%20d%3D%22M5%2010.5V20h14v-9.5%22%2F%3E%3Cpath%20d%3D%22M9%2020v-6h6v6%22%2F%3E%3C%2Fsvg%3E")}
.sun-enterprise-sidebar header nav .nav-new{--sun-icon:url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%221.9%22%20stroke-linecap%3D%22round%22%3E%3Crect%20x%3D%224%22%20y%3D%224%22%20width%3D%2216%22%20height%3D%2216%22%20rx%3D%224%22%2F%3E%3Cpath%20d%3D%22M12%208v8M8%2012h8%22%2F%3E%3C%2Fsvg%3E")}
.sun-enterprise-sidebar header nav .nav-orders{--sun-icon:url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%221.9%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Crect%20x%3D%225%22%20y%3D%224%22%20width%3D%2214%22%20height%3D%2217%22%20rx%3D%223%22%2F%3E%3Cpath%20d%3D%22M9%204.5V3h6v1.5M8%2010h8M8%2014h8M8%2018h5%22%2F%3E%3C%2Fsvg%3E")}
.sun-enterprise-sidebar header nav .nav-calendar{--sun-icon:url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%221.9%22%20stroke-linecap%3D%22round%22%3E%3Crect%20x%3D%223.5%22%20y%3D%225%22%20width%3D%2217%22%20height%3D%2216%22%20rx%3D%223%22%2F%3E%3Cpath%20d%3D%22M7.5%203v4M16.5%203v4M3.5%209.5h17M8%2013h.01M12%2013h.01M16%2013h.01M8%2017h.01M12%2017h.01%22%2F%3E%3C%2Fsvg%3E")}
.sun-enterprise-sidebar header nav .nav-map{--sun-icon:url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%221.9%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M20%2010c0%205.2-8%2011-8%2011S4%2015.2%204%2010a8%208%200%201%201%2016%200Z%22%2F%3E%3Ccircle%20cx%3D%2212%22%20cy%3D%2210%22%20r%3D%222.6%22%2F%3E%3C%2Fsvg%3E")}
.sun-enterprise-sidebar header nav .nav-print{--sun-icon:url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%221.9%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M7%208V3h10v5M7%2017H5a2%202%200%200%201-2-2v-4a3%203%200%200%201%203-3h12a3%203%200%200%201%203%203v4a2%202%200%200%201-2%202h-2%22%2F%3E%3Crect%20x%3D%227%22%20y%3D%2214%22%20width%3D%2210%22%20height%3D%227%22%20rx%3D%221.5%22%2F%3E%3C%2Fsvg%3E")}
.sun-enterprise-sidebar header nav .nav-production{--sun-icon:url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%221.9%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M5%2010h14v3a7%207%200%200%201-14%200v-3Z%22%2F%3E%3Cpath%20d%3D%22M3%2010h18M8%206c0-1.4%201-2%201-3M12%206c0-1.4%201-2%201-3M16%206c0-1.4%201-2%201-3%22%2F%3E%3C%2Fsvg%3E")}
.sun-enterprise-sidebar header nav .nav-prep{--sun-icon:url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%221.9%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M4%207.5%2012%204l8%203.5-8%203.5-8-3.5Z%22%2F%3E%3Cpath%20d%3D%22m4%2012%208%203.5%208-3.5M4%2016.5%2012%2020l8-3.5%22%2F%3E%3C%2Fsvg%3E")}
.sun-enterprise-sidebar header nav .nav-shopping{--sun-icon:url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%221.9%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M3%204h2l2.2%2010.2a2%202%200%200%200%202%201.6h7.7a2%202%200%200%200%201.9-1.4L21%208H7%22%2F%3E%3Ccircle%20cx%3D%2210%22%20cy%3D%2220%22%20r%3D%221%22%2F%3E%3Ccircle%20cx%3D%2218%22%20cy%3D%2220%22%20r%3D%221%22%2F%3E%3C%2Fsvg%3E")}
.sun-enterprise-sidebar header nav .nav-stock{--sun-icon:url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%221.9%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22m4%207%208-4%208%204v10l-8%204-8-4V7Z%22%2F%3E%3Cpath%20d%3D%22m4%207%208%204%208-4M12%2011v10%22%2F%3E%3C%2Fsvg%3E")}
.sun-enterprise-sidebar header nav .nav-routes{--sun-icon:url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%221.9%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Ccircle%20cx%3D%226%22%20cy%3D%2218%22%20r%3D%222%22%2F%3E%3Ccircle%20cx%3D%2218%22%20cy%3D%226%22%20r%3D%222%22%2F%3E%3Cpath%20d%3D%22M8%2018h3a3%203%200%200%200%203-3v-6a3%203%200%200%201%203-3h-1%22%2F%3E%3C%2Fsvg%3E")}
.sun-enterprise-sidebar header nav .nav-clients{--sun-icon:url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%221.9%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Ccircle%20cx%3D%2212%22%20cy%3D%228%22%20r%3D%223.5%22%2F%3E%3Cpath%20d%3D%22M5%2020a7%207%200%200%201%2014%200%22%2F%3E%3C%2Fsvg%3E")}
.sun-enterprise-sidebar header nav .nav-money{--sun-icon:url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%221.9%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Crect%20x%3D%223%22%20y%3D%226%22%20width%3D%2218%22%20height%3D%2213%22%20rx%3D%223%22%2F%3E%3Cpath%20d%3D%22M16%2010h5v5h-5a2.5%202.5%200%200%201%200-5ZM7%206V4h10v2%22%2F%3E%3C%2Fsvg%3E")}
.sun-enterprise-sidebar header nav .nav-team{--sun-icon:url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%221.9%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Ccircle%20cx%3D%229%22%20cy%3D%228%22%20r%3D%223%22%2F%3E%3Ccircle%20cx%3D%2217%22%20cy%3D%2210%22%20r%3D%222.4%22%2F%3E%3Cpath%20d%3D%22M3.5%2020a5.5%205.5%200%200%201%2011%200M14.5%2016.2a4.5%204.5%200%200%201%206%203.8%22%2F%3E%3C%2Fsvg%3E")}
.sun-enterprise-sidebar header nav .nav-chat{--sun-icon:url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%221.9%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M4%205h16v11H9l-5%204V5Z%22%2F%3E%3Cpath%20d%3D%22M8%209h8M8%2013h5%22%2F%3E%3C%2Fsvg%3E")}
.sun-enterprise-sidebar header nav .nav-suppliers{--sun-icon:url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%221.9%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M3%206h11v10H3zM14%209h4l3%203v4h-7z%22%2F%3E%3Ccircle%20cx%3D%227%22%20cy%3D%2218%22%20r%3D%222%22%2F%3E%3Ccircle%20cx%3D%2218%22%20cy%3D%2218%22%20r%3D%222%22%2F%3E%3C%2Fsvg%3E")}
.sun-enterprise-sidebar header nav .nav-stats{--sun-icon:url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%221.9%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M4%2020V10M10%2020V4M16%2020v-7M22%2020V7%22%2F%3E%3C%2Fsvg%3E")}
.sun-enterprise-sidebar header nav .nav-settings{--sun-icon:url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%221.9%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M4%207h10M18%207h2M4%2017h2M10%2017h10M4%2012h4M12%2012h8%22%2F%3E%3Ccircle%20cx%3D%2216%22%20cy%3D%227%22%20r%3D%222%22%2F%3E%3Ccircle%20cx%3D%228%22%20cy%3D%2217%22%20r%3D%222%22%2F%3E%3Ccircle%20cx%3D%2210%22%20cy%3D%2212%22%20r%3D%222%22%2F%3E%3C%2Fsvg%3E")}
.sun-enterprise-sidebar header nav .sun-nav-button:hover{background:rgba(255,255,255,.075)!important;border-color:rgba(255,255,255,.06)!important;color:#fff!important;transform:translateX(2px)!important}
.sun-enterprise-sidebar header nav .sun-nav-button.on{background:linear-gradient(90deg,rgba(174,151,255,.22),rgba(255,255,255,.07))!important;border-color:rgba(204,194,255,.12)!important;color:#fff!important;box-shadow:inset 0 1px rgba(255,255,255,.05),0 8px 18px rgba(9,8,31,.10)!important;transform:none!important}
.sun-enterprise-sidebar header nav .sun-nav-button.on::before{background:var(--sy-accent)!important;opacity:1!important}
.sun-enterprise-sidebar header nav .sun-nav-button.on::after{content:''!important;position:absolute!important;left:-3px!important;top:9px!important;bottom:9px!important;width:3px!important;height:auto!important;border-radius:4px!important;background:linear-gradient(#ffe48d,#e4b844)!important;box-shadow:0 0 10px rgba(242,200,95,.35)!important}
.sun-side-tools{display:grid!important;grid-template-columns:minmax(0,1fr) 46px!important;gap:7px!important;width:auto!important;margin:2px 3px 5px!important}
.sun-side-tools .sun-side-action{min-height:44px!important;border:1px solid rgba(255,255,255,.095)!important;background:rgba(255,255,255,.065)!important;color:#f3f0ff!important;border-radius:12px!important;padding:8px 11px!important;box-shadow:inset 0 1px rgba(255,255,255,.03)!important;transition:.16s ease!important}
.sun-side-tools .sun-side-action:hover{background:rgba(255,255,255,.11)!important;border-color:rgba(255,255,255,.15)!important;transform:translateY(-1px)!important}
#sunGlobalSearchBtn>span:first-child,#sunNotificationsBtn>span:first-child{font-size:0!important;display:block!important;width:19px!important;height:19px!important;min-width:19px!important;background:currentColor!important;-webkit-mask-repeat:no-repeat!important;mask-repeat:no-repeat!important;-webkit-mask-position:center!important;mask-position:center!important;-webkit-mask-size:19px 19px!important;mask-size:19px 19px!important}
#sunGlobalSearchBtn>span:first-child{-webkit-mask-image:url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%3E%3Ccircle%20cx%3D%2211%22%20cy%3D%2211%22%20r%3D%227%22%2F%3E%3Cpath%20d%3D%22m20%2020-4-4%22%2F%3E%3C%2Fsvg%3E")!important;mask-image:url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%3E%3Ccircle%20cx%3D%2211%22%20cy%3D%2211%22%20r%3D%227%22%2F%3E%3Cpath%20d%3D%22m20%2020-4-4%22%2F%3E%3C%2Fsvg%3E")!important}
#sunNotificationsBtn{width:46px!important;padding:0!important;justify-content:center!important;position:relative!important}
#sunNotificationsBtn>span:first-child{-webkit-mask-image:url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%221.9%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M18%208a6%206%200%200%200-12%200c0%207-3%207-3%209h18c0-2-3-2-3-9Z%22%2F%3E%3Cpath%20d%3D%22M10%2021h4%22%2F%3E%3C%2Fsvg%3E")!important;mask-image:url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%221.9%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M18%208a6%206%200%200%200-12%200c0%207-3%207-3%209h18c0-2-3-2-3-9Z%22%2F%3E%3Cpath%20d%3D%22M10%2021h4%22%2F%3E%3C%2Fsvg%3E")!important}
#sunNotificationsBtn>span:nth-child(2){display:none!important}
#sunNotificationsBtn .badge{position:absolute!important;right:-4px!important;top:-5px!important;margin:0!important;min-width:20px!important;height:20px!important;padding:0 5px!important;background:#ff6c7c!important;color:#fff!important;border:2px solid #2c2452!important;border-radius:999px!important;font-size:10px!important;font-weight:850!important;box-shadow:0 4px 12px rgba(255,79,103,.28)!important}
#sunGlobalSearchBtn kbd{border:0!important;background:rgba(255,255,255,.08)!important;border-radius:6px!important;padding:3px 5px!important;color:#c9c5dc!important}
.sun-side-footer{margin:4px 3px 0!important;width:auto!important;display:grid!important;gap:5px!important;padding:10px 2px 1px!important;border-top:1px solid rgba(255,255,255,.09)!important}
.sun-current-user{order:1!important;display:flex!important;align-items:center!important;min-height:34px!important;padding:5px 8px!important;background:transparent!important;border:0!important;border-radius:8px!important;color:#9e9db9!important;font-size:10.5px!important;font-weight:650!important}
.sun-current-user::before{content:'';width:15px;height:15px;margin-right:7px;background:currentColor;-webkit-mask:url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%3E%3Ccircle%20cx%3D%2212%22%20cy%3D%228%22%20r%3D%223%22%2F%3E%3Cpath%20d%3D%22M6%2020a6%206%200%200%201%2012%200%22%2F%3E%3C%2Fsvg%3E") center/15px 15px no-repeat;mask:url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%3E%3Ccircle%20cx%3D%2212%22%20cy%3D%228%22%20r%3D%223%22%2F%3E%3Cpath%20d%3D%22M6%2020a6%206%200%200%201%2012%200%22%2F%3E%3C%2Fsvg%3E") center/15px 15px no-repeat;opacity:.8}
#sunLogoutBtn{order:2!important;min-height:34px!important;padding:5px 8px!important;background:transparent!important;border:0!important;color:#aaa8bd!important;font-size:11px!important;font-weight:600!important;border-radius:8px!important}
#sunLogoutBtn:hover{background:rgba(255,255,255,.06)!important;color:#fff!important}
#sunLogoutBtn>span:first-child{font-size:0!important;width:15px;height:15px;background:currentColor;-webkit-mask:url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M10%205H5v14h5M13%208l4%204-4%204M8%2012h9%22%2F%3E%3C%2Fsvg%3E") center/15px 15px no-repeat;mask:url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M10%205H5v14h5M13%208l4%204-4%204M8%2012h9%22%2F%3E%3C%2Fsvg%3E") center/15px 15px no-repeat}
.sun-sidebar-collapse{position:fixed!important;z-index:1200!important;cursor:pointer!important;left:calc(var(--sun-side) - 19px)!important;top:94px!important;width:38px!important;height:38px!important;min-height:38px!important;padding:0!important;border-radius:999px!important;background:rgba(255,255,255,.94)!important;color:#433d68!important;border:1px solid rgba(69,57,113,.15)!important;box-shadow:0 8px 24px rgba(34,27,75,.20)!important;backdrop-filter:blur(8px)!important;display:grid!important;place-items:center!important;font-size:0!important;transition:left .22s cubic-bezier(.2,.8,.2,1),transform .16s ease,box-shadow .16s ease!important}
.sun-sidebar-collapse:hover{transform:scale(1.07)!important;box-shadow:0 10px 28px rgba(34,27,75,.26)!important;background:#fff!important}
.sun-sidebar-collapse span{display:none!important}
.sun-sidebar-collapse::before,.sun-sidebar-collapse::after{content:''!important;position:absolute!important;width:8px!important;height:8px!important;border-left:2px solid currentColor!important;border-bottom:2px solid currentColor!important;transform:rotate(45deg)!important}
.sun-sidebar-collapse::before{margin-left:5px!important}
.sun-sidebar-collapse::after{margin-left:-5px!important}
body.sun-side-collapsed{padding-left:var(--sun-side-collapsed)!important}
.sun-side-collapsed header{width:var(--sun-side-collapsed)!important;padding-inline:9px!important;overflow-x:visible!important}
.sun-side-collapsed .sun-sidebar-collapse{left:calc(var(--sun-side-collapsed) - 19px)!important}
.sun-side-collapsed .sun-sidebar-collapse::before,.sun-side-collapsed .sun-sidebar-collapse::after{transform:rotate(225deg)!important}
.sun-side-collapsed header .brand{justify-content:center!important;min-height:64px!important;padding-inline:0!important;border-bottom:0!important;font-size:0!important}
.sun-side-collapsed header .brand .real-logo{width:46px!important;height:46px!important;margin:0!important}
.sun-side-collapsed .sun-side-tools{grid-template-columns:1fr!important;margin-inline:5px!important}
.sun-side-collapsed #sunGlobalSearchBtn,.sun-side-collapsed #sunNotificationsBtn{width:46px!important;min-width:46px!important;justify-self:center!important;font-size:0!important;padding:0!important}
.sun-side-collapsed #sunGlobalSearchBtn>span:nth-child(2),.sun-side-collapsed #sunGlobalSearchBtn kbd{display:none!important}
.sun-side-collapsed .sun-nav-group{margin:0!important;padding:7px 0 5px!important;border-bottom:0!important}
.sun-side-collapsed .sun-nav-group::before{display:none!important}
.sun-side-collapsed header nav .sun-nav-button{width:48px!important;min-width:48px!important;height:46px!important;min-height:46px!important;margin:1px auto!important;padding:0!important;justify-content:center!important;font-size:0!important;border-radius:13px!important}
.sun-side-collapsed header nav .sun-nav-button::before{width:21px!important;height:21px!important;min-width:21px!important;margin:0!important;-webkit-mask-size:21px 21px!important;mask-size:21px 21px!important}
.sun-side-collapsed header nav .sun-nav-button.on::after{left:-6px!important;top:12px!important;bottom:12px!important}
.sun-side-collapsed .sun-current-user{font-size:0!important;justify-content:center!important;padding:5px!important}
.sun-side-collapsed .sun-current-user::before{margin:0!important;width:17px!important;height:17px!important}
.sun-side-collapsed #sunLogoutBtn{font-size:0!important;justify-content:center!important;padding:6px!important}
}
</style>
<script id="sun-youth-sidebar-finalizer">
(function(){
const init=()=>{
const header=document.querySelector('header');
const footer=document.querySelector('.sun-side-footer');
const current=document.getElementById('sunCurrentUserLabel');
const collapse=document.getElementById('sunSidebarCollapse');
if(!header||!footer||!current||!collapse) return;
// Keep the owner/account indicator at the very bottom and intentionally quiet.
if(current.parentElement!==footer) window.SunSafe.insertBefore(footer,current,footer.firstChild);
// Floating edge control stays visible in both expanded and collapsed states.
if(collapse.parentElement!==document.body) document.body.appendChild(collapse);
const syncCollapse=()=>{
const collapsed=document.body.classList.contains('sun-side-collapsed');
collapse.title=collapsed?'Развернуть меню':'Свернуть меню';
collapse.setAttribute('aria-label',collapsed?'Развернуть меню':'Свернуть меню');
};
syncCollapse();
collapse.addEventListener('click',()=>setTimeout(syncCollapse,0));
document.querySelectorAll('header nav button').forEach(btn=>{
const label=(btn.dataset.navLabel||btn.textContent||'').trim();
if(label) { btn.title=label; btn.setAttribute('aria-label',label); }
});
const n=document.getElementById('sunNotificationsBtn');
if(n){n.title='Уведомления';n.setAttribute('aria-label','Уведомления');}
const s=document.getElementById('sunGlobalSearchBtn');
if(s){s.title='Поиск';s.setAttribute('aria-label','Поиск');}
};
if(document.readyState==='loading') document.addEventListener('DOMContentLoaded',()=>setTimeout(init,0));
else setTimeout(init,0);
})();
</script>
<style id="sun-catalog-density-style">
/* Catalog density modes + branded no-photo placeholder */
.sun-catalog-actions{display:flex;align-items:center;gap:9px;flex-wrap:wrap;justify-content:flex-end}
.sun-catalog-view-switch{display:inline-flex;align-items:center;gap:3px;padding:3px;border:1px solid #e2e4eb;background:#f7f8fb;border-radius:12px;box-shadow:inset 0 1px 0 #fff}
.sun-catalog-view-switch button{height:31px;border:0;border-radius:9px;background:transparent;color:#6f7484;padding:0 9px;font-size:11.5px;font-weight:750;letter-spacing:.01em;display:inline-flex;align-items:center;gap:6px;white-space:nowrap;transition:.15s ease}
.sun-catalog-view-switch button:hover{background:#fff;color:#30284f}
.sun-catalog-view-switch button.on{background:#fff;color:#30284f;box-shadow:0 2px 8px rgba(40,34,75,.12)}
.sun-catalog-view-switch .view-glyph{width:14px;height:14px;display:inline-grid;place-items:center;position:relative;color:currentColor}
.sun-catalog-view-switch .view-glyph::before,.sun-catalog-view-switch .view-glyph::after{content:'';position:absolute;border:1.5px solid currentColor;border-radius:2px}
.sun-catalog-view-switch [data-mode="photo"] .view-glyph::before{inset:1px}
.sun-catalog-view-switch [data-mode="photo"] .view-glyph::after{width:4px;height:4px;right:2px;top:2px;border-radius:50%;background:currentColor}
.sun-catalog-view-switch [data-mode="compact"] .view-glyph::before{width:5px;height:5px;left:1px;top:1px;box-shadow:7px 0 0 -1.5px currentColor,0 7px 0 -1.5px currentColor,7px 7px 0 -1.5px currentColor}
.sun-catalog-view-switch [data-mode="compact"] .view-glyph::after{display:none}
.sun-catalog-view-switch [data-mode="list"] .view-glyph::before{left:1px;right:1px;top:2px;height:1px;border-width:1px;box-shadow:0 4px 0 currentColor,0 8px 0 currentColor}
.sun-catalog-view-switch [data-mode="list"] .view-glyph::after{display:none}
.sun-catalog-view-switch [data-mode="dense"] .view-glyph::before{left:1px;right:1px;top:3px;height:2px;border-width:1px;box-shadow:0 4px 0 currentColor,0 8px 0 currentColor}
.sun-catalog-view-switch [data-mode="dense"] .view-glyph::after{display:none}
.tile .ph{overflow:hidden;background:linear-gradient(145deg,#f4f1e9,#e7ebf0)!important;place-items:center!important}
.tile .ph .sun-ph-logo,.manager .ph .sun-ph-logo{width:56%!important;height:56%!important;max-width:62px!important;max-height:62px!important;object-fit:contain!important;border-radius:0!important;filter:drop-shadow(0 2px 3px rgba(0,0,0,.08));opacity:.92}
.manager .ph{display:grid!important;place-items:center!important;background:linear-gradient(145deg,#f4f1e9,#e7ebf0)!important;overflow:hidden!important}
.manager .ph .sun-ph-logo{width:66%!important;height:66%!important}
/* Large photo cards — current comfortable view */
#tiles.sun-view-photo{grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:10px}
#tiles.sun-view-photo .tile{border-radius:10px;overflow:hidden}
/* Compact cards — more positions on screen */
#tiles.sun-view-compact{grid-template-columns:repeat(auto-fill,minmax(108px,1fr));gap:7px}
#tiles.sun-view-compact .tile{min-height:116px;padding:6px;border-radius:9px;font-size:12px;line-height:1.18}
#tiles.sun-view-compact .tile img,#tiles.sun-view-compact .tile>.ph{height:58px}
#tiles.sun-view-compact .tile>span{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;margin-top:4px}
#tiles.sun-view-compact .tile-section,#tiles.sun-view-compact .tile-meta{display:none!important}
#tiles.sun-view-compact .tile-price{display:block;margin-top:4px;font-size:11px;color:#57606c}
#tiles.sun-view-compact .tile.add{font-size:12px;background:linear-gradient(145deg,#fffaf0,#fff)}
/* Ultra-dense rectangles — no photos, maximum positions on screen */
#tiles.sun-view-dense{grid-template-columns:repeat(auto-fill,minmax(132px,1fr));gap:5px}
#tiles.sun-view-dense .tile:not(.add){min-height:46px;height:46px;padding:6px 8px;border-radius:8px;display:grid;grid-template-columns:minmax(0,1fr) auto;grid-template-rows:1fr;gap:6px;align-items:center;text-align:left;background:#f8f9fb}
#tiles.sun-view-dense .tile:not(.add)>img,#tiles.sun-view-dense .tile:not(.add)>.ph,#tiles.sun-view-dense .tile-section,#tiles.sun-view-dense .tile-meta{display:none!important}
#tiles.sun-view-dense .tile:not(.add)>span{grid-column:1;grid-row:1;font-size:11.5px;line-height:1.12;margin:0;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
#tiles.sun-view-dense .tile-price{grid-column:2;grid-row:1;font-size:10.5px;line-height:1;color:#645c7d;white-space:nowrap;margin:0}
#tiles.sun-view-dense .tile.add{grid-column:1/-1;min-height:40px;height:40px;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:11.5px;padding:5px;background:linear-gradient(145deg,#fffaf0,#fff)}
/* Dense list — fastest when adding many items */
#tiles.sun-view-list{grid-template-columns:repeat(auto-fill,minmax(285px,1fr));gap:6px}
#tiles.sun-view-list .tile:not(.add){min-height:62px;padding:7px 10px;border-radius:10px;display:grid;grid-template-columns:48px minmax(0,1fr) auto;grid-template-rows:auto auto;column-gap:9px;row-gap:1px;align-items:center;text-align:left;background:#f8f9fb}
#tiles.sun-view-list .tile:not(.add)>img,#tiles.sun-view-list .tile:not(.add)>.ph{grid-column:1;grid-row:1/3;width:46px;height:46px;margin:0;border-radius:8px}
#tiles.sun-view-list .tile:not(.add)>span{grid-column:2;grid-row:1;font-size:12.5px;line-height:1.18;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;align-self:end}
#tiles.sun-view-list .tile-section,#tiles.sun-view-list .tile-meta{grid-column:2;grid-row:2;font-size:10.5px!important;line-height:1.15;color:#838999;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;align-self:start}
#tiles.sun-view-list .tile-meta{display:none!important}
#tiles.sun-view-list .tile-price{grid-column:3;grid-row:1/3;align-self:center;font-size:12px;color:#30284f;white-space:nowrap;margin-left:6px}
#tiles.sun-view-list .tile.add{grid-column:1/-1;min-height:48px;border-radius:10px;display:flex;align-items:center;justify-content:center;gap:5px;font-size:12.5px;background:linear-gradient(145deg,#fffaf0,#fff)}
@media(max-width:1180px) and (min-width:901px){
.sun-catalog-view-switch button{padding:0 7px;font-size:0}.sun-catalog-view-switch .view-glyph{margin:0}
#tiles.sun-view-list{grid-template-columns:1fr}
}
@media(max-width:900px){
.catalog-head{align-items:flex-start;gap:8px;flex-wrap:wrap}.sun-catalog-actions{width:100%;justify-content:space-between}.sun-catalog-view-switch{max-width:100%;overflow:auto}
#tiles.sun-view-compact{grid-template-columns:repeat(auto-fill,minmax(96px,1fr))}
#tiles.sun-view-list{grid-template-columns:1fr}
}
</style>
<script id="sun-catalog-density-script">
(() => {
const VIEW_KEY='sunCatalogViewModeV1';
const AUTOSAVE_KEY_LOCAL='sunOrderDraftAutosaveV1';
const modes=new Set(['photo','compact','list','dense']);
const getMode=()=>{const value=localStorage.getItem(VIEW_KEY)||'photo';return modes.has(value)?value:'photo';};
const tiles=document.getElementById('tiles');
const catalogHead=document.querySelector('#new .catalog .catalog-head');
const configureButton=catalogHead ? [...catalogHead.querySelectorAll('button')].find(b=>b.textContent.trim()==='Настроить') : null;
function setMode(mode){
if(!modes.has(mode)) mode='photo';
localStorage.setItem(VIEW_KEY,mode);
if(tiles){tiles.classList.remove('sun-view-photo','sun-view-compact','sun-view-list','sun-view-dense');tiles.classList.add(`sun-view-${mode}`);}
document.querySelectorAll('.sun-catalog-view-switch button').forEach(btn=>btn.classList.toggle('on',btn.dataset.mode===mode));
}
if(catalogHead && configureButton && !document.querySelector('.sun-catalog-view-switch')){
const actions=document.createElement('div'); actions.className='sun-catalog-actions';
const switcher=document.createElement('div'); switcher.className='sun-catalog-view-switch'; switcher.setAttribute('role','group'); switcher.setAttribute('aria-label','Вид каталога');
switcher.innerHTML=`
<button type="button" data-mode="photo" title="Фото — крупные карточки"><span class="view-glyph"></span><span>Фото</span></button>
<button type="button" data-mode="compact" title="Компактно — больше позиций на экране"><span class="view-glyph"></span><span>Компактно</span></button>
<button type="button" data-mode="list" title="Список — плотный вид с маленькой фотографией"><span class="view-glyph"></span><span>Список</span></button>
<button type="button" data-mode="dense" title="Мини — без фотографий, максимум позиций"><span class="view-glyph"></span><span>Мини</span></button>`;
switcher.addEventListener('click',event=>{const btn=event.target.closest('button[data-mode]');if(btn)setMode(btn.dataset.mode);});
window.SunSafe.insertBefore(catalogHead,actions,configureButton);
actions.appendChild(switcher); actions.appendChild(configureButton);
}
setMode(getMode());
// Universal editor wording for every catalog tab.
const priceCaption=document.getElementById('boxPriceCaption');
const categorySelect=document.getElementById('itemCategory');
const saveBoxButton=document.getElementById('saveBoxButton');
function updateEditorWording(){
if(priceCaption) priceCaption.textContent='Цена, ₽';
if(saveBoxButton) saveBoxButton.textContent='Сохранить';
}
if(typeof window.editBox==='function'){
const previousEditBox=window.editBox;
window.editBox=function(...args){const result=previousEditBox.apply(this,args);updateEditorWording(categorySelect?.value||0);return result;};
}
if(categorySelect){
const previousCategoryChange=categorySelect.onchange;
categorySelect.onchange=function(event){const result=previousCategoryChange?.call(this,event);updateEditorWording(this.value);return result;};
}
updateEditorWording(categorySelect?.value||0);
// Clicking “Новый заказ” after working with a saved order always opens a clean draft.
// Opening an order through “Заказы → Изменить” still loads that saved order normally.
document.addEventListener('click',event=>{
const button=event.target.closest('header nav button');
if(!button || button.textContent.trim()!=='Новый заказ') return;
try{
if(typeof draft!=='undefined' && draft && Number(draft.id||0)>0){
localStorage.removeItem(AUTOSAVE_KEY_LOCAL);
if(typeof window.resetDraft==='function') window.resetDraft();
if(typeof details==='function') details(false);
const orderTab=document.getElementById('orderTab');
orderTab?.scrollIntoView({behavior:'smooth',block:'nearest'});
}
}catch(_){/* keep navigation usable even if a legacy draft is malformed */}
},true);
})();
</script>
<style id="sun-order-details-clean-style">
/* Simplified order details requested: only client/delivery data + order economics. */
#details .event-card-section,#details .staff-assignment,#openEventCardBtn{display:none!important}
#details .sun-guests-details-field{grid-column:1/-1;max-width:280px;margin:0 0 2px}
#details .sun-guests-details-field span{display:block}
#details .sun-guests-details-field input{width:100%}
@media(max-width:680px){#details .sun-guests-details-field{max-width:none}}
#orderEconomyPanel>.order-finance-panel:first-child,#order-payment-modal{display:none!important}
</style>
<style id="sun-dashboard-route-v3-style">
#orderGeoBox{display:none!important}
/* Dashboard v3 */
#dashboard .dash3-head{display:flex;justify-content:space-between;align-items:flex-start;gap:16px;flex-wrap:wrap;margin-bottom:16px}
#dashboard .dash3-head h1{margin:0;color:var(--n);font-size:26px}#dashboard .dash3-head p{margin:5px 0 0;color:#717887}
#dashboard .dash3-actions{display:flex;gap:8px;flex-wrap:wrap}
#dashboard .dash3-grid{display:grid;grid-template-columns:minmax(0,1.25fr) minmax(320px,.75fr);gap:14px}
#dashboard .dash3-panel{border:1px solid #e1e4e9;background:#fff;border-radius:16px;overflow:hidden;box-shadow:0 8px 28px rgba(24,30,55,.055)}
#dashboard .dash3-panel-head{display:flex;justify-content:space-between;align-items:center;gap:10px;padding:14px 16px;border-bottom:1px solid #eceef2}
#dashboard .dash3-panel-head h2{margin:0;color:#27324c;font-size:16px}#dashboard .dash3-panel-head small{color:#8a91a0}
#dashboard .dash3-next{padding:18px;background:linear-gradient(145deg,#28204f 0%,#3d2d74 55%,#25375e 100%);color:#fff;min-height:220px;position:relative;overflow:hidden}
#dashboard .dash3-next:after{content:"";position:absolute;width:230px;height:230px;border-radius:50%;right:-80px;top:-110px;background:rgba(255,255,255,.08)}
#dashboard .dash3-next-top{display:flex;justify-content:space-between;gap:10px;position:relative;z-index:1}.dash3-kicker{font-size:11px;text-transform:uppercase;letter-spacing:.12em;color:#c9c4e4}
#dashboard .dash3-next h2{font-size:24px;margin:12px 0 7px;color:#fff}.dash3-next p{margin:4px 0;color:#dedbef;line-height:1.35}
#dashboard .dash3-next-bottom{display:flex;align-items:flex-end;justify-content:space-between;gap:12px;margin-top:22px;position:relative;z-index:1}.dash3-next-money{font-size:22px;font-weight:800;color:#f3d27a}
#dashboard .dash3-next .dash3-open{border:1px solid rgba(255,255,255,.28);background:rgba(255,255,255,.12);color:#fff;border-radius:10px;padding:9px 13px;font-weight:700}
#dashboard .dash3-status{display:inline-flex;padding:5px 9px;border-radius:999px;background:rgba(255,255,255,.12);font-size:11px;color:#fff}
#dashboard .dash3-orders{padding:2px 14px}.dash3-order{display:grid;grid-template-columns:74px 1fr auto;gap:10px;align-items:center;padding:12px 0;border-bottom:1px solid #eceef2}.dash3-order:last-child{border-bottom:0}
#dashboard .dash3-order-date{font-weight:800;color:#342c66}.dash3-order-date small{display:block;font-weight:500;color:#8a91a0;margin-top:3px}.dash3-order-main b{display:block;color:#27324c}.dash3-order-main span{display:block;color:#7a8290;font-size:12px;margin-top:3px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
#dashboard .dash3-order-price{font-weight:800;color:#27324c;text-align:right}.dash3-order-price button{display:block;margin:5px 0 0 auto}
#dashboard .dash3-row{display:grid;grid-template-columns:minmax(0,1.15fr) minmax(300px,.85fr);gap:14px;margin-top:14px}
#dashboard .dash3-chart{padding:16px}.dash3-bars{height:180px;display:flex;align-items:flex-end;gap:6px;border-bottom:1px solid #e7e9ee;padding:8px 2px 0}
#dashboard .dash3-barcol{flex:1;min-width:0;height:100%;display:flex;align-items:flex-end;justify-content:flex-end;flex-direction:column;gap:5px}.dash3-bar{width:100%;min-height:3px;border-radius:7px 7px 2px 2px;background:linear-gradient(180deg,#8269d8,#4b5796);opacity:.92}.dash3-barcol small{width:100%;text-align:center;color:#8a91a0;font-size:9px}
#dashboard .dash3-chart-note{display:flex;justify-content:space-between;gap:8px;margin-top:10px;color:#7b8290;font-size:11px}
#dashboard .dash3-donut-wrap{display:grid;grid-template-columns:150px 1fr;align-items:center;gap:18px;padding:16px}.dash3-donut{width:138px;height:138px;border-radius:50%;position:relative;margin:auto}.dash3-donut:after{content:"";position:absolute;inset:28px;background:#fff;border-radius:50%;box-shadow:inset 0 0 0 1px #edf0f3}.dash3-donut-center{position:absolute;inset:0;display:grid;place-items:center;z-index:1;text-align:center;font-weight:800;color:#27324c}.dash3-donut-center small{display:block;font-weight:500;color:#8a91a0;font-size:10px}
#dashboard .dash3-legend{display:grid;gap:7px}.dash3-legend-row{display:grid;grid-template-columns:10px 1fr auto;gap:7px;align-items:center;font-size:11px;color:#68717a}.dash3-legend-row i{width:9px;height:9px;border-radius:50%}.dash3-legend-row b{color:#27324c}
#dashboard .dash3-popular{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:10px;padding:14px}.dash3-dish{border:1px solid #e5e8ed;border-radius:13px;overflow:hidden;background:#fafbfc;min-width:0}.dash3-dish img{display:block;width:100%;height:92px;object-fit:cover;background:#eef0f4}.dash3-dish-body{padding:9px}.dash3-dish b{display:block;color:#27324c;font-size:12px;line-height:1.25}.dash3-dish span{display:block;color:#7d8492;font-size:11px;margin-top:5px}
#dashboard .dash3-empty{padding:24px 16px;color:#8a91a0;text-align:center}
@media(max-width:1100px){#dashboard .dash3-grid,#dashboard .dash3-row{grid-template-columns:1fr}#dashboard .dash3-popular{grid-template-columns:repeat(3,minmax(0,1fr))}}
@media(max-width:650px){#dashboard .dash3-popular{grid-template-columns:repeat(2,minmax(0,1fr))}#dashboard .dash3-donut-wrap{grid-template-columns:1fr}.dash3-order{grid-template-columns:62px 1fr}.dash3-order-price{grid-column:2}.dash3-next-bottom{align-items:flex-start;flex-direction:column}}
/* Order double click */
#ordersList .row{cursor:pointer}
#ordersList .row:hover{box-shadow:0 4px 14px rgba(38,42,70,.06)}
#ordersList .row.sun-order-open-flash{outline:2px solid rgba(111,91,208,.25)}
/* Route preview */
#orderGeoBox{display:none!important}
.sun-route-box{margin:10px 0 14px;border:1px solid #dedfea;border-radius:15px;background:linear-gradient(145deg,#fff,#f8f8fc);padding:14px}
.sun-route-head{display:flex;justify-content:space-between;gap:12px;align-items:flex-start}.sun-route-head b{color:#29244e}.sun-route-head small{color:#7a8190}
.sun-route-status{display:flex;align-items:center;gap:7px;font-size:12px;color:#747b89;margin-top:5px}.sun-route-dot{width:9px;height:9px;border-radius:50%;background:#b7bdc6}.sun-route-status.ok .sun-route-dot{background:#55a46c}.sun-route-status.warn .sun-route-dot{background:#d4a343}
.sun-route-actions{display:flex;gap:7px;flex-wrap:wrap;margin:11px 0}.sun-route-actions button{font-size:12px;padding:7px 10px}
.sun-route-meta{display:flex;gap:8px;flex-wrap:wrap;margin:9px 0}.sun-route-chip{font-size:11px;color:#566071;background:#f0f1f6;border-radius:999px;padding:5px 8px}
.sun-route-preview{height:245px;border-radius:12px;overflow:hidden;background:#edf0f3;border:1px solid #dfe3e7;position:relative}
.sun-route-placeholder{height:100%;display:grid;place-items:center;text-align:center;padding:20px;color:#7b8290}.sun-route-placeholder img{width:68px;height:50px;object-fit:contain;display:block;margin:0 auto 9px}
.route-map-canvas{position:relative;width:100%;height:100%;overflow:hidden;background:#e9edf0}.route-map-canvas .osm-tile{position:absolute;width:256px;height:256px;max-width:none;user-select:none;pointer-events:none}.route-map-svg{position:absolute;inset:0;width:100%;height:100%;z-index:2;pointer-events:none}
.route-map-marker{position:absolute;z-index:4;width:28px;height:28px;border-radius:50%;display:grid;place-items:center;font-weight:800;font-size:11px;color:#fff;border:3px solid #fff;box-shadow:0 3px 10px rgba(30,35,50,.25);transform:translate(-50%,-50%)}.route-map-marker.start{background:#303c61}.route-map-marker.finish{background:#6f5bd0}
.route-map-credit{position:absolute;right:5px;bottom:4px;z-index:5;background:rgba(255,255,255,.82);padding:2px 5px;border-radius:4px;font-size:9px;color:#68717a}
.route-page-head{display:flex;justify-content:space-between;gap:12px;align-items:flex-start;flex-wrap:wrap}.route-page-head p{margin:5px 0 0;color:#707887;max-width:780px}
.route-toolbar{display:grid;grid-template-columns:170px 190px minmax(220px,1fr);gap:9px;margin:14px 0}.route-cards{display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:13px}
.route-card{border:1px solid #e0e3e8;border-radius:15px;background:#fff;overflow:hidden;box-shadow:0 6px 20px rgba(25,30,55,.045)}.route-card .sun-route-preview{border:0;border-radius:0;height:210px}
.route-card-body{padding:12px}.route-card-head{display:flex;justify-content:space-between;gap:10px}.route-card h3{margin:0;color:#28324c;font-size:15px}.route-card p{margin:5px 0;color:#6d7583;font-size:12px;line-height:1.35}.route-card-actions{display:flex;gap:6px;flex-wrap:wrap;margin-top:9px}.route-card-actions button{font-size:11px;padding:6px 8px}
.route-badge{font-size:10px;font-weight:800;padding:4px 8px;border-radius:999px;background:#f0eefb;color:#5a49a8;white-space:nowrap}
.sun-doc-route{margin-top:18px;border:1px solid #d9dde0;border-radius:10px;overflow:hidden;break-inside:avoid}.sun-doc-route-head{padding:9px 11px;background:#f8f6f0;display:flex;justify-content:space-between;gap:10px}.sun-doc-route .sun-route-preview{height:250px;border:0;border-radius:0}.sun-doc-route-foot{padding:8px 11px;color:#68717a;font-size:11px}
@media(max-width:700px){.route-toolbar{grid-template-columns:1fr}.route-cards{grid-template-columns:1fr}.sun-route-preview{height:210px}}
@media print{.sun-doc-route .sun-route-preview{height:220px}.route-map-credit{font-size:7px}}
</style>
<script id="sun-dashboard-route-v3">
(() => {
const q = s => document.querySelector(s);
const qa = s => [...document.querySelectorAll(s)];
const esc=window.SunSafe.escapeHTML;
const fmt = v => typeof window.money === 'function' ? window.money(v) : `${Math.round(Number(v||0)).toLocaleString('ru-RU')}`;
const orderTotal = o => typeof window.orderTotalValue === 'function' ? Number(window.orderTotalValue(o)||0) : Number(o?.total||0);
const statusColors = {'Новый':'#6f9fc1','В работе':'#9074c0','Собран':'#e0b242','Передан курьеру':'#df8c43','Отдан заказчику':'#62a77a','Отменён':'#c96b6b'};
const todayKey = () => { const d=new Date(); return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`; };
const addDays = (iso,n) => { const d=new Date(`${iso}T12:00:00`); d.setDate(d.getDate()+n); return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`; };
const stamp = o => `${o?.date||'9999-99-99'}T${o?.time||'23:59'}`;
const dateLabel = raw => { if(!raw)return'—'; const d=new Date(`${raw}T12:00:00`); return Number.isNaN(d.getTime())?raw:d.toLocaleDateString('ru-RU',{day:'2-digit',month:'2-digit'}); };
/* ---------- Dashboard v3 ---------- */
const dashboard = q('#dashboard');
const nav = q('header nav');
const dashboardButton = nav ? qa('header nav button').find(b => b.textContent.trim()==='Главная') : null;
function openDashboardOrder(id){
if(typeof window.openOrder==='function') window.openOrder(Number(id));
}
window.sunDashboardOpenOrder = openDashboardOrder;
function dashboardGo(label){
const b = qa('header nav button').find(x=>x.textContent.trim()===label);
if(b) b.click();
}
window.sunDashboardGo = dashboardGo;
function dashboardBars(valid){
const start=todayKey(), days=Array.from({length:14},(_,i)=>{
const date=addDays(start,i), list=valid.filter(o=>o.date===date);
return {date,count:list.length,total:list.reduce((s,o)=>s+orderTotal(o),0)};
});
const max=Math.max(1,...days.map(x=>x.count));
return `<div class="dash3-bars">${days.map((d,i)=>`<div class="dash3-barcol" title="${esc(dateLabel(d.date))}: ${d.count} заказов"><div class="dash3-bar" style="height:${d.count?Math.max(7,d.count/max*100):3}%"></div><small>${i%2===0?esc(dateLabel(d.date)):''}</small></div>`).join('')}</div><div class="dash3-chart-note"><span>Ближайшие 14 дней</span><b>${days.reduce((s,d)=>s+d.count,0)} заказов</b></div>`;
}
function dashboardDonut(valid){
const names=['Новый','В работе','Собран','Передан курьеру','Отдан заказчику'];
const rows=names.map(name=>({name,count:valid.filter(o=>(o.status||'Новый')===name).length,color:statusColors[name]}));
const total=rows.reduce((s,r)=>s+r.count,0);
let acc=0,parts=[];
rows.forEach(r=>{ if(!r.count||!total)return; const a=acc/total*360; acc+=r.count; const b=acc/total*360; parts.push(`${r.color} ${a}deg ${b}deg`); });
const bg=parts.length?`conic-gradient(${parts.join(',')})`:'#eef0f4';
return `<div class="dash3-donut-wrap"><div class="dash3-donut" style="background:${bg}"><div class="dash3-donut-center"><div>${total}<small>заказов</small></div></div></div><div class="dash3-legend">${rows.map(r=>`<div class="dash3-legend-row"><i style="background:${r.color}"></i><span>${r.name}</span><b>${r.count}</b></div>`).join('')}</div></div>`;
}
function dashboardPopular(valid){
const end=todayKey(),start=addDays(end,-29);
const recent=valid.filter(o=>String(o.date||'')>=start&&String(o.date||'')<=end);
const map=new Map();
recent.forEach(o=>(o.lines||[]).forEach(l=>{
const item=(typeof boxes!=='undefined'?boxes:[]).find(b=>String(b.id)===String(l.id));
if(!item)return;
const qty=Number(l.qty||0),sales=qty*Number(item.price||0);
const prev=map.get(String(item.id))||{item,qty:0,sales:0};
prev.qty+=qty;prev.sales+=sales;map.set(String(item.id),prev);
}));
return [...map.values()].sort((a,b)=>b.sales-a.sales||b.qty-a.qty).slice(0,6);
}
function dashboardRevenue(valid){
const end=todayKey(),days=Array.from({length:30},(_,i)=>{
const date=addDays(end,i-29),list=valid.filter(o=>o.date===date);
return {date,total:list.reduce((sum,o)=>sum+orderTotal(o),0)};
});
const max=Math.max(1,...days.map(x=>x.total)),sum=days.reduce((a,x)=>a+x.total,0);
return `<div class="dash3-bars">${days.map((d,i)=>`<div class="dash3-barcol" title="${esc(dateLabel(d.date))}: ${esc(fmt(d.total))}"><div class="dash3-bar" style="height:${d.total?Math.max(5,d.total/max*100):3}%"></div><small>${i%5===0||i===29?esc(dateLabel(d.date)):''}</small></div>`).join('')}</div><div class="dash3-chart-note"><span>Последние 30 дней</span><b>${esc(fmt(sum))}</b></div>`;
}
function renderDashboardV3(){
if(!dashboard || typeof orders==='undefined') return;
const valid=orders.filter(o=>(o.status||'Новый')!=='Отменён');
const now=new Date();
const upcoming=valid.filter(o=>{
if(['Отдан заказчику'].includes(o.status||'Новый')) return false;
const d=new Date(`${o.date||'1900-01-01'}T${o.time||'00:00'}:00`);
return !Number.isNaN(d.getTime()) && d.getTime()>=now.getTime()-30*60*1000;
}).sort((a,b)=>stamp(a).localeCompare(stamp(b)));
const nearest=upcoming[0]||null;
const following=upcoming.slice(1,6);
const popular=dashboardPopular(valid);
const nearestHtml=nearest?`<div class="dash3-next">
<div class="dash3-next-top"><div><div class="dash3-kicker">Ближайший заказ</div><h2>№${esc(nearest.id)} · ${esc(nearest.event||'Заказ')}</h2><p>${esc(dateLabel(nearest.date))} · ${esc(nearest.time||'—')} · ${esc(nearest.contact||'Клиент не указан')}</p><p>${esc(nearest.address||'Адрес не указан')}</p></div><span class="dash3-status">${esc(nearest.status||'Новый')}</span></div>
<div class="dash3-next-bottom"><div><div class="dash3-next-money">${esc(fmt(orderTotal(nearest)))}</div><small style="color:#c9c4e4">Следующий по времени активный заказ</small></div><button type="button" class="dash3-open" onclick="sunDashboardOpenOrder('${esc(nearest.id)}')">Открыть заказ</button></div>
</div>`:`<div class="dash3-next"><div class="dash3-kicker">Ближайший заказ</div><h2>Активных заказов пока нет</h2><p>Когда появится новый заказ, он автоматически будет показан здесь.</p></div>`;
const followingHtml=following.length?following.map(o=>`<div class="dash3-order"><div class="dash3-order-date">${esc(dateLabel(o.date))}<small>${esc(o.time||'')}</small></div><div class="dash3-order-main"><b>№${esc(o.id)} · ${esc(o.event||'Заказ')}</b><span>${esc(o.contact||'Клиент не указан')} · ${esc(o.address||'Без адреса')}</span></div><div class="dash3-order-price">${esc(fmt(orderTotal(o)))}<button type="button" class="mini" onclick="sunDashboardOpenOrder('${esc(o.id)}')">Открыть</button></div></div>`).join(''):`<div class="dash3-empty">Следующих заказов пока нет.</div>`;
const popularHtml=popular.length?popular.map((x,i)=>`<div class="dash3-dish"><img src="${esc(x.item.photo||(window.CateriumBranding.documentImage()))}" onerror="this.src='${window.CateriumBranding.logoHTML()}'" alt=""><div class="dash3-dish-body"><b>${i+1}. ${esc(x.item.name||'Позиция')}</b><span>${x.qty} шт. · ${esc(fmt(x.sales))}</span></div></div>`).join(''):`<div class="dash3-empty" style="grid-column:1/-1">За последние 30 дней продаж по позициям пока нет.</div>`;
dashboard.innerHTML=`<div class="dash3-head"><div><h1>Главная</h1><p>Ближайшие заказы и короткая картина по работе кейтеринга.</p></div><div class="dash3-actions"><button class="primary" type="button" onclick="sunDashboardGo('Новый заказ')"> Новый заказ</button><button class="outline" type="button" onclick="sunDashboardGo('Заказы')">Все заказы</button></div></div>
<div class="dash3-grid">
<section class="dash3-panel">${nearestHtml}</section>
<section class="dash3-panel"><div class="dash3-panel-head"><h2>Следующие заказы</h2><small>${following.length}</small></div><div class="dash3-orders">${followingHtml}</div></section>
</div>
<div class="dash3-row">
<section class="dash3-panel"><div class="dash3-panel-head"><h2>Загрузка по дням</h2><small>14 дней</small></div><div class="dash3-chart">${dashboardBars(valid)}</div></section>
</div>
<section class="dash3-panel" style="margin-top:14px"><div class="dash3-panel-head"><h2>Лидеры продаж</h2><small>последние 30 дней</small></div><div class="dash3-popular">${popularHtml}</div></section>`;
}
window.renderDashboardV3=renderDashboardV3;
if(dashboardButton){
dashboardButton.onclick=()=>{
qa('.view').forEach(v=>v.classList.remove('on')); dashboard.classList.add('on');
qa('header nav button').forEach(b=>b.classList.remove('on')); dashboardButton.classList.add('on');
renderDashboardV3();
};
}
if(dashboard?.classList.contains('on')) renderDashboardV3();
/* ---------- Double click orders ---------- */
const ordersList=q('#ordersList');
if(ordersList && !ordersList.dataset.dblOpen){
ordersList.dataset.dblOpen='1';
ordersList.addEventListener('dblclick',event=>{
if(event.target.closest('button,select,input,textarea,a,label')) return;
const row=event.target.closest('.row'); if(!row) return;
const first=row.querySelector('span'); const m=String(first?.textContent||'').match(/(\d+)/); if(!m)return;
row.classList.add('sun-order-open-flash');
setTimeout(()=>openDashboardOrder(Number(m[1])),70);
});
}
const ordersHint=q('#orders .hint');
if(ordersHint) ordersHint.textContent='Двойной щелчок по заказу открывает его. Также можно использовать кнопку «Изменить».';
/* ---------- Fixed-origin route previews ---------- */
const ORIGIN_ADDRESS='Ленинградская область, Ломоносовский район, деревня Телези, 20';
const ORIGIN_LABEL='Телези, 20';
const ORIGIN_COORDS={lat:59.701058,lon:29.977269};
const ROUTE_PROVIDER='OSRM / OpenStreetMap';
function currentDraftAddress(){
if(window.SunStaticMap?.orderMapAddress) return window.SunStaticMap.orderMapAddress(draft);
return String(draft?.address||'').trim();
}
function orderAddress(order){
if(window.SunStaticMap?.orderMapAddress) return window.SunStaticMap.orderMapAddress(order);
return String(order?.address||'').trim();
}
function geoOK(g){ return g && Number.isFinite(Number(g.lat)) && Number.isFinite(Number(g.lon)); }
function routeOK(r){ return r && Array.isArray(r.coords) && r.coords.length>1 && geoOK(r.origin) && geoOK(r.destination); }
function routeMatches(r,geo,address){
const originCorrect=routeOK(r)&&Number(r.routeVersion||0)===3&&Math.abs(Number(r.origin.lat)-ORIGIN_COORDS.lat)<0.00002&&Math.abs(Number(r.origin.lon)-ORIGIN_COORDS.lon)<0.00002;
return originCorrect && geoOK(geo) && String(r.sourceAddress||'')===String(address||'') && Math.abs(Number(r.destination.lat)-Number(geo.lat))<0.00002 && Math.abs(Number(r.destination.lon)-Number(geo.lon))<0.00002;
}
function km(v){return `${(Number(v||0)/1000).toLocaleString('ru-RU',{maximumFractionDigits:1})} км`;}
function mins(v){const n=Math.max(1,Math.round(Number(v||0)/60));return n>=60?`${Math.floor(n/60)} ч ${n%60?`${n%60} мин`:''}`:`${n} мин`;}
async function geocodeOrigin(){
return {lat:ORIGIN_COORDS.lat,lon:ORIGIN_COORDS.lon,formatted:ORIGIN_ADDRESS};
}
function simplifyCoords(coords,max=220){
if(!Array.isArray(coords)||coords.length<=max)return coords||[];
const step=Math.ceil(coords.length/max),out=[];
for(let i=0;i<coords.length;i+=step)out.push(coords[i]);
const last=coords[coords.length-1];
if(out[out.length-1]!==last)out.push(last);
return out;
}
async function fetchRoute(origin,destination,sourceAddress){
const url=`https://router.project-osrm.org/route/v1/driving/${Number(origin.lon)},${Number(origin.lat)};${Number(destination.lon)},${Number(destination.lat)}?overview=full&geometries=geojson&steps=false`;
const r=await sunFetchWithTimeout(url,{mode:'cors',credentials:'omit'},8000);
if(!r.ok)throw new Error('Сервис маршрута временно недоступен.');
const d=await r.json(),route=d?.routes?.[0],coords=route?.geometry?.coordinates;
if(!route||!Array.isArray(coords)||coords.length<2)throw new Error('Не удалось построить автомобильный маршрут.');
return {
origin:{lat:Number(origin.lat),lon:Number(origin.lon),address:ORIGIN_ADDRESS},
destination:{lat:Number(destination.lat),lon:Number(destination.lon),address:sourceAddress},
coords:simplifyCoords(coords.map(c=>[Number(c[0]),Number(c[1])])),
distance:Number(route.distance||0),duration:Number(route.duration||0),
sourceAddress:String(sourceAddress||''),builtAt:new Date().toISOString(),provider:ROUTE_PROVIDER,routeVersion:3
};
}
function merc(lon,lat){
const x=(Number(lon)+180)/360;
const s=Math.sin(Math.max(-85.05112878,Math.min(85.05112878,Number(lat)))*Math.PI/180);
const y=0.5-Math.log((1+s)/(1-s))/(4*Math.PI);
return [x,y];
}
function routeMedia(route,width=760,height=245){
if(!routeOK(route)) return `<div class="sun-route-placeholder"><div><img src="${window.CateriumBranding.logoHTML()}" alt="${window.CateriumBranding.nameHTML()}"><b>Маршрут ещё не построен</b><div style="margin-top:5px">Старт всегда: ${esc(ORIGIN_LABEL)}</div></div></div>`;
const pts=route.coords.map(c=>merc(c[0],c[1]));
let minX=Math.min(...pts.map(p=>p[0])),maxX=Math.max(...pts.map(p=>p[0])),minY=Math.min(...pts.map(p=>p[1])),maxY=Math.max(...pts.map(p=>p[1]));
if(maxX-minX<1e-6){minX-=1e-6;maxX+=1e-6} if(maxY-minY<1e-6){minY-=1e-6;maxY+=1e-6}
let z=18;
for(;z>3;z--){const world=256*Math.pow(2,z);if((maxX-minX)*world<=width-76 && (maxY-minY)*world<=height-62)break;}
const world=256*Math.pow(2,z),cx=(minX+maxX)/2,cy=(minY+maxY)/2,left=cx*world-width/2,top=cy*world-height/2;
const tx0=Math.floor(left/256),tx1=Math.floor((left+width)/256),ty0=Math.floor(top/256),ty1=Math.floor((top+height)/256),limit=Math.pow(2,z);
let tileSvg='';
for(let ty=ty0;ty<=ty1;ty++)for(let tx=tx0;tx<=tx1;tx++){
const wx=((tx%limit)+limit)%limit;if(ty<0||ty>=limit)continue;
tileSvg+=`<image href="https://tile.openstreetmap.org/${z}/${wx}/${ty}.png" x="${Math.round(tx*256-left)}" y="${Math.round(ty*256-top)}" width="256" height="256" preserveAspectRatio="none"/>`;
}
const xy=pts.map(p=>[p[0]*world-left,p[1]*world-top]);
const poly=xy.map(p=>`${p[0].toFixed(1)},${p[1].toFixed(1)}`).join(' ');
const markerSource=Array.isArray(route.stops)&&route.stops.length?
[{lat:route.origin.lat,lon:route.origin.lon,label:'A',kind:'start'},...route.stops.map((x,i)=>({lat:x.lat,lon:x.lon,label:String(i+1),kind:'finish'}))]:
[{lat:route.origin.lat,lon:route.origin.lon,label:'A',kind:'start'},{lat:route.destination.lat,lon:route.destination.lon,label:'B',kind:'finish'}];
const markerSvg=markerSource.map(m=>{const p=merc(m.lon,m.lat),x=p[0]*world-left,y=p[1]*world-top,fill=m.kind==='start'?'#303c61':'#6f5bd0';return `<g transform="translate(${x.toFixed(1)} ${y.toFixed(1)})"><circle r="13" fill="${fill}" stroke="#fff" stroke-width="3"/><text x="0" y="4" text-anchor="middle" font-size="10" font-weight="800" fill="#fff">${esc(m.label)}</text></g>`;}).join('');
return `<div class="route-map-canvas"><svg class="route-map-svg" viewBox="0 0 ${width} ${height}" preserveAspectRatio="none">${tileSvg}<polyline points="${poly}" fill="none" stroke="rgba(255,255,255,.96)" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/><polyline points="${poly}" fill="none" stroke="#6f5bd0" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>${markerSvg}</svg><div class="route-map-credit">© OpenStreetMap contributors</div></div>`;
}
function yandexRouteUrl(route,address){
if(routeOK(route)){
const a=route.origin,b=route.destination;
return `https://yandex.ru/maps/?rtext=${encodeURIComponent(`${a.lat},${a.lon}~${b.lat},${b.lon}`)}&rtt=auto`;
}
return `https://yandex.ru/maps/?rtext=${encodeURIComponent(`${ORIGIN_ADDRESS}~${address||''}`)}&rtt=auto`;
}
async function ensureDraftRoute({silent=false,force=false}={}){
const address=currentDraftAddress();
if(!address){if(!silent)alert('Сначала укажите улицу и дом.');return null;}
let destination=null;
try{destination=await window.SunStaticMap?.ensureDraftGeo?.({silent:true,force});}catch(_){}
if(!geoOK(destination)){if(!silent)alert('Не удалось определить адрес доставки. Проверьте улицу, дом и интернет.');return null;}
if(!force&&routeMatches(draft.routePreview,destination,address))return draft.routePreview;
try{
setDraftRouteStatus('Строим маршрут…','warn');
const origin=await geocodeOrigin(),route=await fetchRoute(origin,destination,address);
draft.routePreview=route;
if(draft.id){
const i=orders.findIndex(o=>String(o.id)===String(draft.id));
if(i>=0){orders[i].routePreview=structuredClone(route);persist();}
}
renderDraftRoute(); return route;
}catch(e){renderDraftRoute();if(!silent)alert(e?.message||'Не удалось построить маршрут.');return null;}
}
async function ensureOrderRoute(order,{silent=false,force=false}={}){
if(!order)return null;
const address=orderAddress(order);if(!address){if(!silent)alert('В заказе не указан адрес.');return null;}
let destination=null;try{destination=await window.SunStaticMap?.ensureOrderGeo?.(order,{silent:true,force});}catch(_){}
if(!geoOK(destination)){if(!silent)alert('Не удалось определить адрес доставки.');return null;}
if(!force&&routeMatches(order.routePreview,destination,address))return order.routePreview;
try{
const origin=await geocodeOrigin(),route=await fetchRoute(origin,destination,address);
order.routePreview=route;persist();
if(draft?.id&&String(draft.id)===String(order.id))draft.routePreview=structuredClone(route);
return route;
}catch(e){if(!silent)alert(e?.message||'Не удалось построить маршрут.');return null;}
}
function setDraftRouteStatus(text,kind=''){
const st=q('#sunRouteStatus');if(!st)return;st.className=`sun-route-status ${kind}`;const span=st.querySelector('span');if(span)span.textContent=text;
}
const details=q('#details');
let routeBox=q('#sunRouteBox');
if(false && details && !routeBox){
routeBox=document.createElement('div');routeBox.id='sunRouteBox';routeBox.className='sun-route-box';
const old=q('#orderGeoBox');
routeBox.innerHTML=`<div class="sun-route-head"><div><b>Маршрут доставки</b><div class="sun-route-status" id="sunRouteStatus"><i class="sun-route-dot"></i><span>Маршрут ещё не построен</span></div></div><small>Старт: ${esc(ORIGIN_LABEL)}</small></div><div class="sun-route-actions"><button type="button" class="primary" id="sunRouteBuild">Построить маршрут</button><button type="button" class="outline" id="sunRouteOpen">Открыть маршрут</button></div><div id="sunRouteMeta" class="sun-route-meta"></div><div id="sunRoutePreview" class="sun-route-preview"></div>`;
if(old)old.insertAdjacentElement('afterend',routeBox);else details.appendChild(routeBox);
}
function renderDraftRoute(){
if(!routeBox)return;
const address=currentDraftAddress(),geo=draft?.geo,route=draft?.routePreview,valid=routeMatches(route,geo,address);
const preview=q('#sunRoutePreview'),meta=q('#sunRouteMeta'),build=q('#sunRouteBuild');
if(!address){
setDraftRouteStatus('Укажите улицу и дом','');
meta.innerHTML=`<span class="sun-route-chip">Старт: ${esc(ORIGIN_LABEL)}</span>`;
preview.innerHTML=routeMedia(null);build.textContent='Построить маршрут';return;
}
if(!valid){
setDraftRouteStatus('Нужно построить маршрут','warn');
meta.innerHTML=`<span class="sun-route-chip">${esc(ORIGIN_LABEL)}${esc(address)}</span>`;
preview.innerHTML=routeMedia(null);build.textContent='Построить маршрут';return;
}
setDraftRouteStatus('Маршрут готов','ok');
meta.innerHTML=`<span class="sun-route-chip">${esc(ORIGIN_LABEL)}${esc(address)}</span><span class="sun-route-chip">${esc(km(route.distance))}</span><span class="sun-route-chip">≈ ${esc(mins(route.duration))}</span>`;
preview.innerHTML=routeMedia(route);build.textContent='Обновить маршрут';
}
q('#sunRouteBuild')?.addEventListener('click',async()=>{await ensureDraftRoute({force:true});renderRoutePage();});
q('#sunRouteOpen')?.addEventListener('click',()=>{
const address=currentDraftAddress();
window.open(yandexRouteUrl(draft?.routePreview,address),'_blank','noopener');
});
['deliveryStreet','deliveryHouse','address'].forEach(id=>q(`#${id}`)?.addEventListener('input',()=>setTimeout(renderDraftRoute,0)));
/* Replace the old map page with route cards. */
const mapView=q('#order-map-view');
const mapButton=qa('header nav button').find(b=>b.classList.contains('nav-map')||b.textContent.trim()==='Карта');
function routePageShell(){
if(!mapView)return;
mapView.innerHTML=`<div class="route-page-head"><div><h1>Маршруты доставки</h1><p>На каждом превью показан автомобильный маршрут от постоянной точки <b>${esc(ORIGIN_LABEL)}</b> до адреса заказа. Для навигации курьера маршрут открывается в Яндекс Картах.</p></div></div>
<div class="route-toolbar"><label>Период<select id="routePeriod"><option value="today">Сегодня</option><option value="tomorrow">Завтра</option><option value="week" selected>7 дней</option><option value="month">30 дней</option><option value="all">Все заказы</option></select></label><label>Статус<select id="routeStatus"><option value="all">Все статусы</option>${Object.keys(statusColors).map(s=>`<option>${esc(s)}</option>`).join('')}</select></label><label>Поиск<input id="routeSearch" placeholder="Заказ, клиент, адрес..."></label></div><div id="routeCards" class="route-cards"></div>`;
q('#routePeriod')?.addEventListener('change',renderRoutePage);
q('#routeStatus')?.addEventListener('change',renderRoutePage);
q('#routeSearch')?.addEventListener('input',renderRoutePage);
}
function filteredRouteOrders(){
const period=q('#routePeriod')?.value||'week',status=q('#routeStatus')?.value||'all',search=(q('#routeSearch')?.value||'').trim().toLowerCase(),today=todayKey();
return orders.slice().filter(o=>{
if((o.status||'Новый')==='Отменён')return false;
if(status!=='all'&&(o.status||'Новый')!==status)return false;
if(period==='today'&&o.date!==today)return false;
if(period==='tomorrow'&&o.date!==addDays(today,1))return false;
if(period==='week'&&((o.date||'')<today||(o.date||'')>addDays(today,6)))return false;
if(period==='month'&&((o.date||'')<today||(o.date||'')>addDays(today,29)))return false;
if(search&&!`${o.id} ${o.event||''} ${o.contact||''} ${o.address||''}`.toLowerCase().includes(search))return false;
return true;
}).sort((a,b)=>stamp(a).localeCompare(stamp(b)));
}
function renderRoutePage(){
const wrap=q('#routeCards');if(!wrap)return;
const list=filteredRouteOrders();
wrap.innerHTML=list.length?list.map(o=>{
const address=orderAddress(o),valid=routeMatches(o.routePreview,o.geo,address),r=o.routePreview;
return `<article class="route-card"><div class="sun-route-preview">${valid?routeMedia(r,760,210):routeMedia(null,760,210)}</div><div class="route-card-body"><div class="route-card-head"><div><h3>№${esc(o.id)} · ${esc(o.event||'Заказ')}</h3><p>${esc(dateLabel(o.date))} · ${esc(o.time||'—')} · ${esc(o.contact||'Клиент не указан')}</p></div><span class="route-badge">${valid?`${esc(km(r.distance))} · ${esc(mins(r.duration))}`:'маршрут не построен'}</span></div><p><b>${esc(ORIGIN_LABEL)}</b> → ${esc(address||'Адрес не указан')}</p><div class="route-card-actions"><button class="outline" type="button" onclick="sunRouteOpenOrder('${esc(o.id)}')">Заказ</button>${address?`<button class="${valid?'outline':'primary'}" type="button" onclick="sunRouteBuildOrder('${esc(o.id)}')">${valid?'Обновить':'Построить маршрут'}</button><button class="outline" type="button" onclick="sunRouteOpenYandex('${esc(o.id)}')">Открыть маршрут</button>`:''}</div></div></article>`;
}).join(''):`<div class="dash3-empty" style="grid-column:1/-1">Заказов по выбранным фильтрам нет.</div>`;
}
window.sunRouteOpenOrder=id=>openDashboardOrder(id);
window.sunRouteBuildOrder=async id=>{const o=orders.find(x=>String(x.id)===String(id));if(!o)return;const btn=document.activeElement;if(btn?.tagName==='BUTTON'){btn.disabled=true;btn.textContent='Строим…';}await ensureOrderRoute(o,{force:true});if(btn?.tagName==='BUTTON')btn.disabled=false;renderRoutePage();};
window.sunRouteOpenYandex=id=>{const o=orders.find(x=>String(x.id)===String(id));if(!o)return;const address=orderAddress(o);window.open(yandexRouteUrl(o.routePreview,address),'_blank','noopener');};
if(mapView&&mapButton){
routePageShell();
mapButton.onclick=()=>{
qa('.view').forEach(v=>v.classList.remove('on'));mapView.classList.add('on');
qa('header nav button').forEach(b=>b.classList.remove('on'));mapButton.classList.add('on');
renderRoutePage();
};
}
/* Keep route panel in sync when an order is opened/reset. */
if(typeof window.openOrder==='function'){
const prev=window.openOrder;
window.openOrder=id=>{const result=prev(id);setTimeout(renderDraftRoute,0);return result;};
}
if(typeof window.resetDraft==='function'){
const prev=window.resetDraft;
window.resetDraft=()=>{const result=prev();setTimeout(renderDraftRoute,0);return result;};
}
/* Replace point map in printable order documents with route preview when available. */
function injectRouteIntoPrint(){
const doc=q('#preview .sun-doc');if(!doc)return;
doc.querySelectorAll('.sun-doc-map,.sun-doc-route').forEach(x=>x.remove());
const address=currentDraftAddress(),r=draft?.routePreview;
if(!routeMatches(r,draft?.geo,address))return;
const block=document.createElement('div');block.className='sun-doc-route';
block.innerHTML=`<div class="sun-doc-route-head"><b>Маршрут доставки</b><span>${esc(ORIGIN_LABEL)}${esc(address)}</span></div><div class="sun-route-preview">${routeMedia(r,760,250)}</div><div class="sun-doc-route-foot">${esc(km(r.distance))} · примерно ${esc(mins(r.duration))} · © OpenStreetMap contributors</div>`;
const footer=doc.querySelector('.sun-doc-footer');if(footer)footer.insertAdjacentElement('beforebegin',block);else doc.appendChild(block);
}
document.addEventListener('click',e=>{
const b=e.target.closest('button');if(!b)return;
if(b.textContent.trim()==='Бланк заказа') setTimeout(injectRouteIntoPrint,40);
},true);
const previewModal=q('#preview');
if(previewModal){
new MutationObserver(()=>{
if(q('#preview .sun-doc-map'))setTimeout(injectRouteIntoPrint,0);
}).observe(previewModal,{childList:true,subtree:true});
}
window.SunDeliveryRoute={originAddress:ORIGIN_ADDRESS,buildDraft:ensureDraftRoute,buildOrder:ensureOrderRoute,routeMedia,yandexRouteUrl};
renderDraftRoute();
})();
</script>
<script id="sun-final-cleanup-v6">
(()=>{
const ROUTE_FIX_KEY='sunRouteUnifiedFixV3';
try{
if(localStorage.getItem(ROUTE_FIX_KEY)!=='1'){
let list=[];try{list=JSON.parse(localStorage.getItem('sunOrders')||'[]')||[]}catch(_){list=[]}
let changed=false;
if(Array.isArray(list)) list.forEach(o=>{if(o&&('routePreview' in o||'geo' in o)){delete o.routePreview;delete o.geo;changed=true;}});
if(changed)localStorage.setItem('sunOrders',JSON.stringify(list));
localStorage.removeItem('sunStaticMapGeocodeCacheV2');
localStorage.setItem(ROUTE_FIX_KEY,'1');
try{if(typeof orders!=='undefined'&&Array.isArray(orders)){orders.forEach(o=>{if(o){delete o.routePreview;delete o.geo;}});}}catch(_){}
try{if(typeof draft!=='undefined'&&draft){delete draft.routePreview;delete draft.geo;}}catch(_){}
}
}catch(_){}
const removeProduction=()=>{
document.querySelectorAll('header nav button').forEach(b=>{if((b.dataset.navLabel||b.textContent||'').trim()==='Производство')b.remove();});
document.getElementById('production')?.remove();
};
removeProduction();
try{localStorage.removeItem('sunRouteBaseV1');localStorage.removeItem('sunRouteOriginTelezi20V1');}catch(_){}
setTimeout(removeProduction,0);setTimeout(removeProduction,250);
})();
</script>
<style id="sun-print-clean-layout-v1">
/* Clean order actions: stable, equal-sized buttons. */
#new .actions{
display:grid!important;
grid-template-columns:repeat(2,minmax(0,1fr))!important;
gap:8px!important;
align-items:stretch!important;
}
#new .actions>button{
width:100%!important;
min-width:0!important;
min-height:42px!important;
margin:0!important;
display:flex!important;
align-items:center!important;
justify-content:center!important;
text-align:center!important;
line-height:1.15!important;
}
#new .actions>#deleteOrderBtn{grid-column:1/-1!important;}
#dashboard .dash3-row{grid-template-columns:1fr!important;}
/* Route/map should not occupy space inside New order -> Details. */
#details #orderGeoBox,#details #sunRouteBox,.sun-route-box{display:none!important;}
/* One calm preview size for order form, receipt and preparations. */
#preview{
padding:22px!important;
align-items:flex-start!important;
overflow:auto!important;
}
#preview .dialog{
width:min(980px,calc(100vw - 44px))!important;
max-width:980px!important;
min-height:min(680px,calc(100vh - 44px))!important;
max-height:calc(100vh - 44px)!important;
padding:24px!important;
border-radius:16px!important;
overflow:auto!important;
margin:0 auto!important;
}
#preview .sun-doc,
#preview .sun-receipt,
#preview .prep-doc,
#preview .sun-custom-doc{
width:100%!important;
max-width:900px!important;
margin-left:auto!important;
margin-right:auto!important;
}
#preview .sun-receipt{max-width:900px!important;}
#preview .sun-doc-actions,
#preview .prep-doc-actions,
#preview .sun-custom-actions{
position:sticky!important;
bottom:-24px!important;
z-index:20!important;
background:rgba(255,255,255,.97)!important;
border-top:1px solid #eceef1!important;
padding:12px 0 2px!important;
margin-top:20px!important;
}
@media(max-width:620px){
#new .actions{grid-template-columns:1fr!important;}
#new .actions>#deleteOrderBtn{grid-column:auto!important;}
#preview{padding:10px!important;}
#preview .dialog{
width:calc(100vw - 20px)!important;
min-height:calc(100vh - 20px)!important;
max-height:calc(100vh - 20px)!important;
padding:15px!important;
border-radius:12px!important;
}
}
@media print{
#preview{padding:0!important;overflow:visible!important;}
#preview .dialog{width:100%!important;max-width:none!important;min-height:0!important;max-height:none!important;padding:8mm!important;border-radius:0!important;overflow:visible!important;}
#preview .sun-doc,#preview .sun-receipt,#preview .prep-doc,#preview .sun-custom-doc{max-width:none!important;}
}
</style>
<script id="sun-print-clean-behavior-v1">
(() => {
// Keep the main order buttons in a stable visual order.
const orderActions=[...document.querySelectorAll('#new .actions')].find(a=>[...a.querySelectorAll('button')].some(b=>b.textContent.trim()==='Сохранить'));
const deleteButton=document.getElementById('deleteOrderBtn');
if(orderActions&&deleteButton) orderActions.appendChild(deleteButton);
// Defensive cleanup: receipts must never inherit a delivery map/route from another preview.
const observer = new MutationObserver(() => {
const doc=document.querySelector('#preview .sun-doc');
if(!doc)return;
const title=doc.querySelector('.sun-doc-title h2')?.textContent||'';
if(/Товарный чек/i.test(title)) doc.querySelectorAll('.sun-doc-map,.sun-doc-route').forEach(x=>x.remove());
});
const preview=document.getElementById('preview');
if(preview) observer.observe(preview,{childList:true,subtree:true});
})();
</script>
<style id="sun-orders-card-view-style">
#orders .sun-orders-viewbar{display:flex;align-items:center;justify-content:space-between;gap:14px;margin:0 0 14px}
#orders .sun-orders-viewbar h1{margin:0}
#orders .sun-orders-view-switch{display:inline-flex;gap:3px;padding:4px;background:#eef0f5;border:1px solid #e0e3ea;border-radius:12px}
#orders .sun-orders-view-switch button{border:0;background:transparent;color:#72798a;border-radius:9px;padding:8px 11px;font-size:12px;font-weight:700;display:inline-flex;align-items:center;gap:6px}
#orders .sun-orders-view-switch button.on{background:#fff;color:#312a65;box-shadow:0 2px 7px rgba(35,31,71,.08)}
#orders .sun-orders-view-switch svg{width:15px;height:15px;stroke:currentColor;fill:none;stroke-width:1.9;stroke-linecap:round;stroke-linejoin:round}
#orders.sun-orders-cards .head,#orders.sun-orders-cards #ordersList{display:none!important}
#orders:not(.sun-orders-cards) #sunOrderCards{display:none!important}
#sunOrderCards{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px}
.sun-order-card{position:relative;background:#fff;border:1px solid #e4e6ec;border-radius:16px;padding:14px;box-shadow:0 2px 8px rgba(28,33,55,.035);transition:.16s ease;cursor:pointer;min-width:0}
.sun-order-card:hover{border-color:#d8d4e9;box-shadow:0 7px 22px rgba(35,31,71,.08);transform:translateY(-1px)}
.sun-order-card.sun-order-open-flash{outline:2px solid rgba(111,91,208,.25)}
.sun-order-card-top{display:grid;grid-template-columns:78px minmax(0,1fr);gap:12px;align-items:start}
.sun-order-card-date{border-radius:13px;background:linear-gradient(145deg,#f1eff9,#f7f7fb);border:1px solid #e4e0f0;text-align:center;padding:9px 6px;color:#3d356e}
.sun-order-card-date strong{display:block;font-size:25px;line-height:1;font-weight:850;letter-spacing:-.04em}
.sun-order-card-date span{display:block;margin-top:5px;font-size:11px;font-weight:800;text-transform:uppercase;letter-spacing:.06em}
.sun-order-card-date small{display:block;margin-top:5px;color:#747b8b;font-size:11px;font-weight:700}
.sun-order-card-title{min-width:0;padding-top:2px}
.sun-order-card-title .sun-order-number{font-size:10px;color:#969cab;font-weight:800;letter-spacing:.07em;text-transform:uppercase;margin-bottom:4px}
.sun-order-card-title h3{margin:0;color:#27314b;font-size:15px;line-height:1.28;overflow-wrap:anywhere}
.sun-order-card-client{margin-top:7px;color:#626b7a;font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.sun-order-card-address{margin-top:10px;padding:9px 10px;background:#f8f9fb;border-radius:10px;color:#697180;font-size:11.5px;min-height:38px;line-height:1.35;overflow-wrap:anywhere}
.sun-order-card-bottom{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:11px;padding-top:11px;border-top:1px solid #eceef2}
.sun-order-card-total strong{display:block;color:#27314b;font-size:16px}.sun-order-card-total small{display:block;color:#969cab;font-size:10px;margin-top:2px}
.sun-order-card-actions{display:flex;gap:6px}.sun-order-card-actions button{padding:6px 8px;font-size:11px}
.sun-order-card-status{margin-top:9px;width:100%;border:1px solid #e1e4e9;background:#fff;border-radius:9px;padding:7px 9px;font-size:11px;color:#4f5868}
.sun-order-card-empty{grid-column:1/-1;padding:34px 18px;text-align:center;color:#8c93a0;border:1px dashed #d9dde5;border-radius:14px;background:#fafbfc}
@media(max-width:1180px){#sunOrderCards{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media(max-width:720px){#orders .sun-orders-viewbar{align-items:flex-start;flex-direction:column}#sunOrderCards{grid-template-columns:1fr}.sun-order-card-top{grid-template-columns:72px minmax(0,1fr)}}
</style>
<script id="sun-orders-card-view-script">
(()=>{
const section=document.getElementById('orders');
const list=document.getElementById('ordersList');
if(!section||!list||document.getElementById('sunOrderCards')) return;
const h1=section.querySelector(':scope > h1');
const bar=document.createElement('div'); bar.className='sun-orders-viewbar';
const title=document.createElement('h1'); title.textContent='Заказы';
const toggle=document.createElement('div'); toggle.className='sun-orders-view-switch'; toggle.setAttribute('aria-label','Вид заказов');
toggle.innerHTML=`
<button type="button" data-orders-view="list" title="Список"><svg viewBox="0 0 24 24"><path d="M8 6h12M8 12h12M8 18h12M4 6h.01M4 12h.01M4 18h.01"/></svg>Список</button>
<button type="button" data-orders-view="cards" title="Карточки"><svg viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="7" rx="1.5"/><rect x="14" y="3" width="7" height="7" rx="1.5"/><rect x="3" y="14" width="7" height="7" rx="1.5"/><rect x="14" y="14" width="7" height="7" rx="1.5"/></svg>Карточки</button>`;
bar.append(title,toggle);
if(h1) h1.replaceWith(bar); else section.prepend(bar);
const cards=document.createElement('div'); cards.id='sunOrderCards';
list.insertAdjacentElement('afterend',cards);
const esc=window.SunSafe.escapeHTML;
const fmt=v=>typeof window.money==='function'?window.money(v):`${Math.round(Number(v||0)).toLocaleString('ru-RU')}`;
const total=o=>typeof window.orderTotalValue==='function'?Number(window.orderTotalValue(o)||0):Number(o?.total||0);
const monthName=d=>d.toLocaleDateString('ru-RU',{month:'short'}).replace('.','');
const dateParts=raw=>{const d=raw?new Date(`${raw}T12:00:00`):null;if(!d||Number.isNaN(d.getTime()))return{day:'—',month:'',weekday:'Дата'};return{day:String(d.getDate()).padStart(2,'0'),month:monthName(d),weekday:d.toLocaleDateString('ru-RU',{weekday:'short'}).replace('.','')}};
function currentOrders(){try{return Array.isArray(orders)?orders:JSON.parse(localStorage.getItem('sunOrders')||'[]')}catch(_){return[]}}
function renderCards(){
const arr=currentOrders();
cards.innerHTML=arr.length?arr.map(o=>{
const d=dateParts(o.date), phone=o.phone?` · ${esc(o.phone)}`:'';
const statuses=['Новый','В работе','Собран','Передан курьеру','Отдан заказчику','Отменён'];
return `<article class="sun-order-card" data-order-id="${esc(o.id)}" tabindex="0" title="Двойной щелчок — открыть заказ">
<div class="sun-order-card-top">
<div class="sun-order-card-date"><strong>${esc(d.day)}</strong><span>${esc(d.month)}</span><small>${esc(o.time||d.weekday)}</small></div>
<div class="sun-order-card-title"><div class="sun-order-number">Заказ № ${esc(o.id)}</div><h3>${esc(o.event||'Заказ')}</h3><div class="sun-order-card-client">${esc(o.contact||'Клиент не указан')}${phone}</div></div>
</div>
<div class="sun-order-card-address">${esc(o.address||'Адрес не указан')}</div>
<select class="sun-order-card-status" data-card-status="${esc(o.id)}" aria-label="Статус заказа">${statuses.map(s=>`<option ${s===(o.status||'Новый')?'selected':''}>${s}</option>`).join('')}</select>
<div class="sun-order-card-bottom"><div class="sun-order-card-total"><strong>${esc(fmt(total(o)))}</strong><small>сумма заказа</small></div><div class="sun-order-card-actions"><button class="mini" type="button" data-card-open="${esc(o.id)}">Открыть</button><button class="mini danger" type="button" data-card-delete="${esc(o.id)}">Удалить</button></div></div>
</article>`;
}).join(''):'<div class="sun-order-card-empty">Сохранённых заказов пока нет.</div>';
}
function setView(view){
const isCards=view==='cards'; section.classList.toggle('sun-orders-cards',isCards);
toggle.querySelectorAll('button').forEach(b=>b.classList.toggle('on',b.dataset.ordersView===view));
try{localStorage.setItem('sunOrdersView',view)}catch(_){}
if(isCards)renderCards();
}
toggle.addEventListener('click',e=>{const b=e.target.closest('[data-orders-view]');if(b)setView(b.dataset.ordersView)});
cards.addEventListener('change',e=>{const s=e.target.closest('[data-card-status]');if(!s)return;if(typeof window.status==='function')window.status(Number(s.dataset.cardStatus),s.value);setTimeout(renderCards,0)});
cards.addEventListener('click',e=>{
const open=e.target.closest('[data-card-open]'); if(open){e.stopPropagation();window.openOrder?.(Number(open.dataset.cardOpen));return;}
const del=e.target.closest('[data-card-delete]'); if(del){e.stopPropagation();window.deleteOrderById?.(Number(del.dataset.cardDelete));setTimeout(renderCards,0);return;}
});
cards.addEventListener('dblclick',e=>{if(e.target.closest('button,select,input,textarea,a,label'))return;const card=e.target.closest('.sun-order-card');if(!card)return;card.classList.add('sun-order-open-flash');setTimeout(()=>window.openOrder?.(Number(card.dataset.orderId)),70)});
cards.addEventListener('keydown',e=>{if(e.key!=='Enter'||e.target.closest('button,select,input,textarea,a,label'))return;const card=e.target.closest('.sun-order-card');if(card)window.openOrder?.(Number(card.dataset.orderId))});
const prior=window.renderOrders;
if(typeof prior==='function') window.renderOrders=()=>{prior();if(section.classList.contains('sun-orders-cards'))renderCards();};
window.sunRenderOrderCards=renderCards;
let saved='list';try{saved=localStorage.getItem('sunOrdersView')||'list'}catch(_){}
setView(saved==='cards'?'cards':'list');
const hint=section.querySelector(':scope > .hint');if(hint)hint.textContent='Выберите удобный вид. Двойной щелчок по строке или карточке открывает заказ.';
})();
</script>
<style id="sun-orders-compact-toolbar-style">
/* Compact order cards: date remains the strongest visual anchor. */
#sunOrderCards{grid-template-columns:repeat(auto-fill,minmax(215px,1fr));gap:9px!important}
.sun-order-card{padding:10px!important;border-radius:13px!important;border-color:#e7e7ef!important;background:linear-gradient(145deg,#ffffff 0%,#f8f7fd 100%)!important;box-shadow:0 4px 14px rgba(37,42,72,.045)!important}
.sun-order-card:nth-child(4n+2){background:linear-gradient(145deg,#ffffff 0%,#f4f9fd 100%)!important}
.sun-order-card:nth-child(4n+3){background:linear-gradient(145deg,#ffffff 0%,#f4faf7 100%)!important}
.sun-order-card:nth-child(4n+4){background:linear-gradient(145deg,#ffffff 0%,#fff9f0 100%)!important}
.sun-order-card:hover{border-color:#d9d6e9!important;box-shadow:0 8px 22px rgba(43,39,83,.085)!important}
.sun-order-card-top{grid-template-columns:58px minmax(0,1fr)!important;gap:9px!important}
.sun-order-card-date{border-radius:11px!important;padding:7px 4px!important;background:rgba(255,255,255,.78)!important;box-shadow:inset 0 0 0 1px rgba(111,91,208,.08)}
.sun-order-card-date strong{font-size:21px!important}.sun-order-card-date span{font-size:9.5px!important;margin-top:3px!important}.sun-order-card-date small{font-size:9.5px!important;margin-top:3px!important}
.sun-order-card-title{padding-top:0!important}.sun-order-card-title .sun-order-number{font-size:8.5px!important;margin-bottom:2px!important}.sun-order-card-title h3{font-size:13px!important;line-height:1.2!important;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.sun-order-card-client{margin-top:4px!important;font-size:10.5px!important}
.sun-order-card-address{margin-top:7px!important;padding:6px 8px!important;border-radius:8px!important;font-size:10px!important;min-height:29px!important;line-height:1.25!important;background:rgba(255,255,255,.62)!important;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.sun-order-card-status{margin-top:6px!important;border-radius:8px!important;padding:5px 7px!important;font-size:10px!important;background:rgba(255,255,255,.78)!important}
.sun-order-card-bottom{gap:7px!important;margin-top:7px!important;padding-top:7px!important}.sun-order-card-total strong{font-size:13px!important}.sun-order-card-total small{font-size:8.5px!important;margin-top:1px!important}.sun-order-card-actions{gap:4px!important}.sun-order-card-actions button{padding:4px 6px!important;font-size:9.5px!important;border-radius:7px!important}
/* Move catalog categories into the top catalog toolbar and free the former middle column. */
#new .work{grid-template-columns:minmax(0,1.72fr) minmax(350px,.88fr)!important;gap:12px!important}
#new .catalog{min-width:0}
#new .sun-catalog-top-toolbar{display:flex;align-items:center;justify-content:space-between;gap:10px;margin:10px 0 5px;padding:7px;background:#f7f8fb;border:1px solid #eaebf0;border-radius:13px;min-width:0}
#new .sun-catalog-top-toolbar .cats{display:flex!important;align-items:center;gap:5px;flex-wrap:wrap;flex:1 1 auto;padding:0!important;margin:0!important;background:transparent!important;border-radius:0!important;min-width:0}
#new .sun-catalog-top-toolbar .cats p{display:none!important}
#new .sun-catalog-top-toolbar .cats button{width:auto!important;min-width:0!important;margin:0!important;padding:7px 10px!important;border-radius:9px!important;font-size:11px!important;line-height:1!important;white-space:nowrap;box-shadow:none!important;transform:none!important;outline-width:1px!important;outline-offset:1px!important}
#new .sun-catalog-top-toolbar .sun-catalog-view-switch{flex:0 0 auto;margin:0!important;background:#eceef4!important;border-radius:10px!important;padding:3px!important}
#new .sun-catalog-top-toolbar .sun-catalog-view-switch button{height:30px!important;padding:0 8px!important;font-size:10.5px!important;border-radius:8px!important}
#new .catalog-head{gap:10px}
#new .catalog-head .sun-catalog-actions{margin-left:auto}
#new .catalog-head .sun-catalog-actions>.sun-catalog-view-switch{display:none!important}
#new .tiles{margin-top:8px!important}
@media(max-width:1180px){
#new .work{grid-template-columns:1fr!important}
#new .work>.catalog,#new .work>section.card{grid-column:1!important}
#new .sun-catalog-top-toolbar{align-items:flex-start;flex-direction:column}
#new .sun-catalog-top-toolbar .sun-catalog-view-switch{max-width:100%;overflow:auto}
}
@media(max-width:720px){
#sunOrderCards{grid-template-columns:repeat(auto-fill,minmax(190px,1fr))!important}
.sun-order-card-top{grid-template-columns:54px minmax(0,1fr)!important}
#new .sun-catalog-top-toolbar .cats{flex-wrap:wrap!important;overflow:visible;width:100%;padding-bottom:2px!important}
#new .sun-catalog-top-toolbar .cats button{flex:0 0 auto!important}
}
</style>
<script id="sun-orders-compact-toolbar-script">
(()=>{
const catalog=document.querySelector('#new .catalog');
const head=catalog?.querySelector('.catalog-head');
const cats=document.querySelector('#new .work > .cats') || document.querySelector('#new .cats');
const switcher=document.querySelector('#new .sun-catalog-view-switch');
if(catalog && head && cats && switcher && !catalog.querySelector('.sun-catalog-top-toolbar')){
const toolbar=document.createElement('div');
toolbar.className='sun-catalog-top-toolbar';
head.insertAdjacentElement('afterend',toolbar);
toolbar.appendChild(cats);
toolbar.appendChild(switcher);
}
})();
</script>
<!-- ===== ORDER VIEWER + DASHBOARD V4 ===== -->
<style id="sun-order-viewer-dashboard-v4-style">
/* Keep operational/event blocks out of the order details screen. */
#details .event-card-section,#details .staff-assignment,#openEventCardBtn{display:none!important}
/* Dedicated read-only order viewer. */
#sun-order-viewer{z-index:12000}
#sun-order-viewer .dialog{width:min(980px,96vw);max-height:92vh;padding:0;border-radius:20px;overflow:auto;background:#f6f7fb;box-shadow:0 28px 80px rgba(24,29,55,.28)}
.sun-order-view-head{position:sticky;top:0;z-index:2;display:flex;justify-content:space-between;align-items:flex-start;gap:14px;padding:18px 20px;background:rgba(255,255,255,.96);backdrop-filter:blur(14px);border-bottom:1px solid #e8eaf0}
.sun-order-view-title small{display:block;color:#8d94a3;font-size:11px;font-weight:800;letter-spacing:.08em;text-transform:uppercase;margin-bottom:5px}.sun-order-view-title h2{margin:0;color:#27314b;font-size:24px;line-height:1.15}.sun-order-view-title p{margin:6px 0 0;color:#71798a;font-size:13px}
.sun-order-view-close{width:38px;height:38px;border:0;border-radius:11px;background:#f1f2f6;color:#4b5363;font-size:24px;line-height:1;display:grid;place-items:center}
.sun-order-view-body{padding:16px 18px 20px}.sun-order-view-hero{display:grid;grid-template-columns:minmax(0,1.35fr) minmax(260px,.65fr);gap:12px}.sun-order-view-card{background:#fff;border:1px solid #e5e7ed;border-radius:16px;padding:15px;box-shadow:0 5px 20px rgba(32,38,67,.035)}
.sun-order-view-main{background:linear-gradient(145deg,#2d2855,#3b3970 58%,#2b4a68);color:#fff;border:0;position:relative;overflow:hidden}.sun-order-view-main:after{content:"";position:absolute;width:220px;height:220px;border-radius:50%;right:-90px;top:-120px;background:rgba(255,255,255,.07)}
.sun-order-view-main .sun-order-view-date{font-size:13px;color:#d9d6ec;position:relative;z-index:1}.sun-order-view-main h3{font-size:25px;margin:9px 0 10px;color:#fff;position:relative;z-index:1}.sun-order-view-main p{margin:5px 0;color:#e3e1f1;line-height:1.4;position:relative;z-index:1}.sun-order-view-status{display:inline-flex;margin-top:10px;padding:5px 9px;border-radius:999px;background:rgba(255,255,255,.13);font-size:11px;font-weight:800;position:relative;z-index:1}
.sun-order-view-money{display:grid;grid-template-columns:repeat(3,1fr);gap:8px}.sun-order-view-money div{padding:11px;border-radius:12px;background:#f7f8fb}.sun-order-view-money small{display:block;color:#8a91a0;font-size:10px}.sun-order-view-money b{display:block;color:#29324b;font-size:16px;margin-top:4px}.sun-order-view-money .accent{background:#f3effd}.sun-order-view-money .accent b{color:#5b48a6}
.sun-order-view-info{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin-top:12px}.sun-order-view-info h3,.sun-order-view-items h3{margin:0 0 10px;color:#30394f;font-size:14px}.sun-order-view-list{display:grid;gap:8px}.sun-order-view-list div{display:grid;grid-template-columns:95px 1fr;gap:8px;font-size:12px;line-height:1.35}.sun-order-view-list span{color:#9399a6}.sun-order-view-list b{color:#4a5260;font-weight:700;overflow-wrap:anywhere}.sun-order-view-note{white-space:pre-wrap;color:#59616f;font-size:12px;line-height:1.45;background:#fafbfc;border-radius:11px;padding:10px;min-height:42px}
.sun-order-view-items{margin-top:12px}.sun-order-view-table-wrap{overflow:auto;border:1px solid #e7e9ee;border-radius:12px}.sun-order-view-table{width:100%;border-collapse:collapse;min-width:560px}.sun-order-view-table th,.sun-order-view-table td{padding:9px 10px;border-bottom:1px solid #eceef2;text-align:left;font-size:12px}.sun-order-view-table th{color:#858c9a;background:#f8f9fb;font-size:10px;text-transform:uppercase;letter-spacing:.04em}.sun-order-view-table td{color:#454d5c}.sun-order-view-table tr:last-child td{border-bottom:0}.sun-order-view-table .num{text-align:right;white-space:nowrap}
.sun-order-view-actions{position:sticky;bottom:0;z-index:2;display:flex;gap:7px;flex-wrap:wrap;padding:13px 18px;background:rgba(255,255,255,.97);backdrop-filter:blur(12px);border-top:1px solid #e7e9ee}.sun-order-view-actions button{min-height:38px;border-radius:10px;padding:8px 12px}.sun-order-view-actions .spacer{flex:1}
/* Dashboard V4 */
#dashboard .dash4{max-width:1500px;margin:0 auto}.dash4-head{display:flex;justify-content:space-between;align-items:flex-start;gap:16px;flex-wrap:wrap;margin-bottom:12px}.dash4-head h1{margin:0;color:#27314b;font-size:27px}.dash4-head p{margin:5px 0 0;color:#7b8290}
.dash4-quick{display:grid;grid-template-columns:repeat(6,minmax(110px,1fr));gap:8px;margin-bottom:12px}.dash4-qbtn{display:flex;align-items:center;gap:9px;min-height:54px;padding:9px 11px;border:1px solid #e4e6ec;border-radius:14px;background:linear-gradient(145deg,#fff,#f7f6fc);color:#394159;text-align:left;box-shadow:0 4px 16px rgba(35,40,68,.035)}.dash4-qbtn:hover{border-color:#d8d2ee;box-shadow:0 8px 22px rgba(45,40,86,.07);transform:translateY(-1px)}.dash4-qicon{width:31px;height:31px;border-radius:10px;display:grid;place-items:center;background:#f0edfb;color:#6553b6;flex:0 0 auto}.dash4-qicon svg{width:17px;height:17px;stroke:currentColor;fill:none;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}.dash4-qbtn:nth-child(2n) .dash4-qicon{background:#edf6fb;color:#4f87a8}.dash4-qbtn:nth-child(3n) .dash4-qicon{background:#eef8f2;color:#4e8d69}.dash4-qbtn b{font-size:12px}.dash4-qbtn small{display:block;color:#9298a4;font-size:9.5px;margin-top:2px}
.dash4-kpis{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px;margin-bottom:12px}.dash4-kpi{background:#fff;border:1px solid #e6e8ed;border-radius:13px;padding:10px 12px;display:flex;justify-content:space-between;align-items:center;gap:8px}.dash4-kpi small{color:#9198a5;font-size:10px}.dash4-kpi b{display:block;color:#29324c;font-size:19px;margin-top:3px}.dash4-kpi i{font-style:normal;width:31px;height:31px;border-radius:10px;display:grid;place-items:center;background:#f4f2fb;color:#6655b5;font-weight:900}
.dash4-main{display:grid;grid-template-columns:minmax(0,1.12fr) minmax(360px,.88fr);gap:12px}.dash4-panel{background:#fff;border:1px solid #e4e6eb;border-radius:16px;overflow:hidden;box-shadow:0 6px 22px rgba(34,39,66,.04)}.dash4-panel-head{display:flex;justify-content:space-between;align-items:center;gap:8px;padding:11px 13px;border-bottom:1px solid #eceef2}.dash4-panel-head h2{margin:0;color:#30394f;font-size:14px}.dash4-panel-head small{color:#9298a4;font-size:10px}
.dash4-next{padding:15px;background:linear-gradient(145deg,#29254f,#403875 55%,#2d5067);color:#fff;min-height:175px;position:relative;overflow:hidden}.dash4-next:after{content:"";position:absolute;width:190px;height:190px;border-radius:50%;right:-70px;top:-95px;background:rgba(255,255,255,.07)}.dash4-next-top{display:flex;justify-content:space-between;gap:8px;position:relative;z-index:1}.dash4-next-kicker{font-size:9px;text-transform:uppercase;letter-spacing:.13em;color:#c9c5e2}.dash4-next h2{font-size:20px;color:#fff;margin:8px 0 5px}.dash4-next p{margin:3px 0;color:#dedbec;font-size:12px;line-height:1.35}.dash4-next-status{font-size:9px;font-weight:800;padding:4px 7px;border-radius:999px;background:rgba(255,255,255,.12);height:max-content}.dash4-next-foot{display:flex;justify-content:space-between;align-items:flex-end;gap:9px;margin-top:16px;position:relative;z-index:1}.dash4-next-foot b{font-size:18px;color:#f0d280}.dash4-next-foot button{border:1px solid rgba(255,255,255,.25);background:rgba(255,255,255,.1);color:#fff;border-radius:9px;padding:7px 10px;font-weight:700;font-size:11px}
.dash4-orders{padding:0 12px}.dash4-order{display:grid;grid-template-columns:62px minmax(0,1fr) auto;gap:8px;align-items:center;padding:9px 0;border-bottom:1px solid #eef0f3}.dash4-order:last-child{border-bottom:0}.dash4-order-date{font-weight:850;color:#493d86;font-size:12px}.dash4-order-date small{display:block;color:#969ca8;font-size:9px;margin-top:2px}.dash4-order-main{min-width:0}.dash4-order-main b{display:block;color:#343c50;font-size:11.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dash4-order-main span{display:block;color:#8a919f;font-size:9.5px;margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dash4-order button{padding:5px 7px;font-size:9.5px}
.dash4-bottom{display:grid;grid-template-columns:minmax(260px,.62fr) minmax(0,1.38fr);gap:12px;margin-top:12px}.dash4-load{padding:11px 13px}.dash4-load-bars{height:76px;display:flex;align-items:flex-end;gap:4px;border-bottom:1px solid #eceef2;padding-top:4px}.dash4-load-col{flex:1;min-width:0;height:100%;display:flex;flex-direction:column;justify-content:flex-end;align-items:center;gap:3px}.dash4-load-bar{width:100%;min-height:2px;border-radius:4px 4px 1px 1px;background:linear-gradient(180deg,#8070d1,#5769a6)}.dash4-load-col small{font-size:7.5px;color:#9ba1ac}.dash4-load-note{display:flex;justify-content:space-between;gap:8px;margin-top:7px;font-size:9.5px;color:#8a919e}.dash4-load-note b{color:#4b5363}
.dash4-leaders{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:7px;padding:10px}.dash4-leader{display:grid;grid-template-columns:36px minmax(0,1fr);gap:7px;align-items:center;padding:7px;border:1px solid #eceef2;border-radius:11px;background:#fafbfc;min-width:0}.dash4-leader img{width:36px;height:36px;border-radius:9px;object-fit:cover;background:#eef0f3}.dash4-leader b{display:block;color:#3c4558;font-size:9.5px;line-height:1.2;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.dash4-leader span{display:block;color:#9298a4;font-size:8.5px;margin-top:3px}.dash4-empty{padding:20px;color:#9298a4;text-align:center;font-size:11px}
@media(max-width:1100px){.dash4-quick{grid-template-columns:repeat(3,1fr)}.dash4-main,.dash4-bottom{grid-template-columns:1fr}.dash4-leaders{grid-template-columns:repeat(3,1fr)}}
@media(max-width:700px){#sun-order-viewer .dialog{width:96vw}.sun-order-view-hero,.sun-order-view-info{grid-template-columns:1fr}.sun-order-view-money{grid-template-columns:1fr 1fr}.dash4-quick{grid-template-columns:repeat(2,1fr)}.dash4-kpis{grid-template-columns:1fr 1fr}.dash4-leaders{grid-template-columns:repeat(2,1fr)}.dash4-order{grid-template-columns:55px minmax(0,1fr)}.dash4-order>button{grid-column:2;justify-self:end}}
</style>
<script id="sun-order-viewer-dashboard-v4-script">
(()=>{
const q=s=>document.querySelector(s), qa=s=>[...document.querySelectorAll(s)];
const esc=window.SunSafe.escapeHTML;
const fmt=v=>typeof window.money==='function'?window.money(v):`${Math.round(Number(v||0)).toLocaleString('ru-RU')}`;
const getOrders=()=>{try{return Array.isArray(orders)?orders:JSON.parse(localStorage.getItem('sunOrders')||'[]')}catch(_){return[]}};
const getBoxes=()=>{try{return Array.isArray(boxes)?boxes:JSON.parse(localStorage.getItem('sunBoxes')||'[]')}catch(_){return[]}};
const total=o=>typeof window.orderTotalValue==='function'?Number(window.orderTotalValue(o)||0):Number(o?.total||0);
const paid=o=>Math.max(0,Number(o?.prepayment||0));
const dateLong=raw=>{if(!raw)return'Дата не указана';const d=new Date(`${raw}T12:00:00`);return Number.isNaN(d.getTime())?raw:d.toLocaleDateString('ru-RU',{weekday:'long',day:'numeric',month:'long',year:'numeric'})};
const dateShort=raw=>{if(!raw)return'—';const d=new Date(`${raw}T12:00:00`);return Number.isNaN(d.getTime())?raw:d.toLocaleDateString('ru-RU',{day:'2-digit',month:'2-digit'})};
const dayKey=()=>{const d=new Date();return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`};
const addDays=(iso,n)=>{const d=new Date(`${iso}T12:00:00`);d.setDate(d.getDate()+n);return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`};
/* Restore the order blank in the New order action bar, while keeping the event card hidden. */
function restoreOrderBlank(){
const actions=qa('#new .actions').find(a=>[...a.querySelectorAll('button')].some(b=>b.textContent.trim()==='Сохранить')); if(!actions)return;
const allButtons=qa('#new button');
const blank=allButtons.find(b=>b.textContent.trim()==='Бланк заказа');
if(blank){
blank.hidden=false;blank.removeAttribute('aria-hidden');blank.tabIndex=0;blank.style.removeProperty('display');
const receipt=[...actions.querySelectorAll('button')].find(b=>b.textContent.trim()==='Товарный чек');
if(receipt&&receipt.parentNode===actions){
if(blank.parentNode!==actions||blank.nextSibling!==receipt)window.SunSafe.insertBefore(actions,blank,receipt);
}else if(blank.parentNode!==actions){
actions.appendChild(blank);
}
}
const eventBtn=q('#openEventCardBtn');if(eventBtn)eventBtn.style.setProperty('display','none','important');
}
restoreOrderBlank(); setTimeout(restoreOrderBlank,100);
/* Read-only order viewer modal. */
let viewer=q('#sun-order-viewer');
if(!viewer){viewer=document.createElement('div');viewer.id='sun-order-viewer';viewer.className='modal';viewer.innerHTML='<div class="dialog"></div>';document.body.appendChild(viewer);}
function itemPrice(line,item){const direct=Number(line?.price);return Number.isFinite(direct)&&direct>=0?direct:Number(item?.price||0)}
function viewerHtml(o){
const boxList=getBoxes();
const t=total(o),p=paid(o),balance=Math.max(0,t-p);
const deliveryMeta=[['Подъезд',o.deliveryEntrance],['Этаж',o.deliveryFloor],['Квартира',o.deliveryApartment],['Офис',o.deliveryOffice]].filter(x=>String(x[1]??'').trim());
const rows=(o.lines||[]).map(line=>{const item=boxList.find(x=>String(x.id)===String(line.id));const price=itemPrice(line,item),qty=Math.max(0,Number(line.qty||0));return `<tr><td><b>${esc(item?.name||line.name||`Позиция ${line.id}`)}</b></td><td class="num">${esc(qty)}</td><td class="num">${esc(fmt(price))}</td><td class="num"><b>${esc(fmt(price*qty))}</b></td></tr>`}).join('');
return `<div class="sun-order-view-head"><div class="sun-order-view-title"><small>Просмотр заказа</small><h2>Заказ № ${esc(o.id)}</h2><p>${esc(o.event||'Заказ')}</p></div><button class="sun-order-view-close" type="button" data-viewer-close>×</button></div>
<div class="sun-order-view-body">
<div class="sun-order-view-hero">
<section class="sun-order-view-card sun-order-view-main"><div class="sun-order-view-date">${esc(dateLong(o.date))} · ${esc(o.time||'время не указано')}</div><h3>${esc(o.event||'Заказ')}</h3><p>${esc(o.contact||'Клиент не указан')}${o.phone?` · ${esc(o.phone)}`:''}</p><p>${esc(o.address||'Адрес доставки не указан')}</p><span class="sun-order-view-status">${esc(o.status||'Новый')}</span></section>
<section class="sun-order-view-card"><h3 style="margin:0 0 11px;color:#30394f;font-size:14px">Оплата</h3><div class="sun-order-view-money"><div><small>Сумма</small><b>${esc(fmt(t))}</b></div><div><small>Предоплата</small><b>${esc(fmt(p))}</b></div><div class="accent"><small>К оплате</small><b>${esc(fmt(balance))}</b></div></div></section>
</div>
<div class="sun-order-view-info">
<section class="sun-order-view-card"><h3>Клиент и доставка</h3><div class="sun-order-view-list"><div><span>Клиент</span><b>${esc(o.contact||'—')}</b></div><div><span>Телефон</span><b>${esc(o.phone||'—')}</b></div><div><span>Адрес</span><b>${esc(o.address||'—')}</b></div>${deliveryMeta.map(([k,v])=>`<div><span>${esc(k)}</span><b>${esc(v)}</b></div>`).join('')}</div></section>
<section class="sun-order-view-card"><h3>Примечание</h3><div class="sun-order-view-note">${esc(o.note||'Примечаний нет.')}</div></section>
</div>
<section class="sun-order-view-card sun-order-view-items"><h3>Состав заказа · ${(o.lines||[]).length} поз.</h3><div class="sun-order-view-table-wrap"><table class="sun-order-view-table"><thead><tr><th>Позиция</th><th class="num">Кол-во</th><th class="num">Цена</th><th class="num">Сумма</th></tr></thead><tbody>${rows||'<tr><td colspan="4">В заказе нет позиций.</td></tr>'}</tbody></table></div></section>
</div>
<div class="sun-order-view-actions"><button class="primary" type="button" data-viewer-action="edit" data-id="${esc(o.id)}">Редактировать</button><button class="outline" type="button" data-viewer-action="blank" data-id="${esc(o.id)}">Бланк заказа</button><button class="outline" type="button" data-viewer-action="receipt" data-id="${esc(o.id)}">Товарный чек</button><button class="outline" type="button" data-viewer-action="prep" data-id="${esc(o.id)}">Заготовки</button><span class="spacer"></span><button class="outline" type="button" data-viewer-close>Закрыть</button></div>`;
}
function openViewer(id){const o=getOrders().find(x=>String(x.id)===String(id));if(!o)return;viewer.querySelector('.dialog').innerHTML=viewerHtml(o);if(typeof window.modal==='function')window.modal('sun-order-viewer');else viewer.classList.add('on');}
function closeViewer(){if(typeof window.closeModal==='function')window.closeModal('sun-order-viewer');else viewer.classList.remove('on')}
window.sunOrderViewerOpen=openViewer;
viewer.addEventListener('click',e=>{
if(e.target.closest('[data-viewer-close]')){closeViewer();return;}
const b=e.target.closest('[data-viewer-action]');if(!b)return;const id=Number(b.dataset.id),action=b.dataset.viewerAction;closeViewer();
if(action==='edit'){window.openOrder?.(id);return;}
window.openOrder?.(id);
setTimeout(()=>{
if(action==='prep'){
if(typeof window.openPreview==='function'){window.__sunOrderViewerReturnId=id;window.__sunPrintPreviewReturn=false;window.openPreview();}
return;
}
const label=action==='blank'?'Бланк заказа':'Товарный чек';
const target=qa('#new .actions button').find(x=>x.textContent.trim()===label);
if(target){window.__sunOrderViewerReturnId=id;window.__sunPrintPreviewReturn=false;target.click();}
},120);
});
/* Double click in Orders now opens the viewer instead of navigating to New order. */
document.addEventListener('dblclick',e=>{
if(!e.target.closest('#orders')||e.target.closest('button,select,input,textarea,a,label'))return;
const card=e.target.closest('.sun-order-card');const row=e.target.closest('#ordersList .row');if(!card&&!row)return;
let id=card?.dataset.orderId;if(!id&&row){const m=String(row.querySelector('span')?.textContent||'').match(/(\d+)/);id=m?.[1];}
if(!id)return;e.preventDefault();e.stopImmediatePropagation();window.openOrder?.(Number(id));
},true);
/* Dashboard V4: compact, informative and action-focused. */
const dashboard=q('#dashboard'), nav=q('header nav'), dashBtn=nav?qa('header nav button').find(b=>b.textContent.trim()==='Главная'):null;
const icon=name=>({
plus:'<svg viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"/></svg>',
orders:'<svg viewBox="0 0 24 24"><path d="M7 4h10M7 9h10M7 14h10M7 19h7M4 4h.01M4 9h.01M4 14h.01M4 19h.01"/></svg>',
print:'<svg viewBox="0 0 24 24"><path d="M7 8V4h10v4M7 17H5a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2M7 14h10v6H7z"/></svg>',
calendar:'<svg viewBox="0 0 24 24"><rect x="3" y="5" width="18" height="16" rx="2"/><path d="M8 3v4M16 3v4M3 10h18"/></svg>',
clients:'<svg viewBox="0 0 24 24"><circle cx="9" cy="8" r="3"/><path d="M3 20c0-3 2.5-5 6-5s6 2 6 5M16 7a3 3 0 0 1 0 6M17 15c2.4.4 4 2 4 5"/></svg>',
route:'<svg viewBox="0 0 24 24"><circle cx="5" cy="6" r="2"/><circle cx="19" cy="18" r="2"/><path d="M7 6h4a3 3 0 0 1 3 3v6a3 3 0 0 0 3 3"/></svg>'
}[name]||'');
function go(label){if(typeof window.sunDashboardGo==='function')window.sunDashboardGo(label);else qa('header nav button').find(b=>b.textContent.trim()===label)?.click()}
window.sunDashboardV4Go=go;
function renderDash(){
if(!dashboard)return;const arr=getOrders(),today=dayKey();const active=arr.filter(o=>(o.status||'Новый')!=='Отменён');
const upcoming=active.filter(o=>`${o.date||'9999-99-99'}T${o.time||'23:59'}`>=`${today}T00:00`).sort((a,b)=>`${a.date||''}T${a.time||''}`.localeCompare(`${b.date||''}T${b.time||''}`));
const nearest=upcoming[0], following=upcoming.slice(1,6);const end7=addDays(today,6);const next7=active.filter(o=>o.date>=today&&o.date<=end7);
const start30=addDays(today,-29), last30=active.filter(o=>o.date>=start30&&o.date<=today);const clients30=new Set(last30.map(o=>String(o.phone||o.contact||'').trim().toLowerCase()).filter(Boolean)).size;const units30=last30.reduce((s,o)=>s+(o.lines||[]).reduce((n,l)=>n+Number(l.qty||0),0),0);const todayCount=active.filter(o=>o.date===today).length;
const days=Array.from({length:14},(_,i)=>{const date=addDays(today,i);return{date,count:active.filter(o=>o.date===date).length}});const maxLoad=Math.max(1,...days.map(x=>x.count));
const boxList=getBoxes(),leaderMap=new Map();last30.forEach(o=>(o.lines||[]).forEach(l=>{const item=boxList.find(x=>String(x.id)===String(l.id));if(!item)return;const qty=Number(l.qty||0),price=itemPrice(l,item),prev=leaderMap.get(String(item.id))||{item,qty:0,sales:0};prev.qty+=qty;prev.sales+=qty*price;leaderMap.set(String(item.id),prev)}));const leaders=[...leaderMap.values()].sort((a,b)=>b.sales-a.sales||b.qty-a.qty).slice(0,5);
const quick=[['plus','Новый заказ','Создать заказ'],['orders','Заказы','Все заказы'],['print','Печать','Документы'],['calendar','Календарь','Расписание'],['clients','Клиенты','База клиентов'],['route','Маршруты','Курьеры']];
const nearestHtml=nearest?`<div class="dash4-next"><div class="dash4-next-top"><div><div class="dash4-next-kicker">Ближайший заказ</div><h2>№${esc(nearest.id)} · ${esc(nearest.event||'Заказ')}</h2><p>${esc(dateLong(nearest.date))} · ${esc(nearest.time||'—')}</p><p>${esc(nearest.contact||'Клиент не указан')} · ${esc(nearest.address||'Адрес не указан')}</p></div><span class="dash4-next-status">${esc(nearest.status||'Новый')}</span></div><div class="dash4-next-foot"><b>${esc(fmt(total(nearest)))}</b><button type="button" onclick="sunOrderViewerOpen('${esc(nearest.id)}')">Посмотреть</button></div></div>`:`<div class="dash4-next"><div class="dash4-next-kicker">Ближайший заказ</div><h2>Заказов пока нет</h2><p>Ближайший активный заказ появится здесь автоматически.</p></div>`;
const followHtml=following.length?following.map(o=>`<div class="dash4-order"><div class="dash4-order-date">${esc(dateShort(o.date))}<small>${esc(o.time||'')}</small></div><div class="dash4-order-main"><b>№${esc(o.id)} · ${esc(o.event||'Заказ')}</b><span>${esc(o.contact||'Клиент')} · ${esc(o.address||'Без адреса')}</span></div><button class="mini" type="button" onclick="sunOrderViewerOpen('${esc(o.id)}')">Открыть</button></div>`).join(''):'<div class="dash4-empty">Следующих заказов пока нет.</div>';
const loadHtml=`<div class="dash4-load-bars">${days.map((d,i)=>`<div class="dash4-load-col" title="${esc(dateShort(d.date))}: ${d.count}"><div class="dash4-load-bar" style="height:${d.count?Math.max(8,d.count/maxLoad*100):3}%"></div><small>${i%2===0?esc(dateShort(d.date)):''}</small></div>`).join('')}</div><div class="dash4-load-note"><span>14 дней</span><b>${days.reduce((s,d)=>s+d.count,0)} заказов</b></div>`;
const leaderHtml=leaders.length?leaders.map((x,i)=>`<div class="dash4-leader"><img src="${esc(x.item.photo||(window.CateriumBranding.documentImage()))}" onerror="this.src='${window.CateriumBranding.logoHTML()}'" alt=""><div><b>${i+1}. ${esc(x.item.name||'Позиция')}</b><span>${x.qty} шт. · ${esc(fmt(x.sales))}</span></div></div>`).join(''):'<div class="dash4-empty" style="grid-column:1/-1">За последние 30 дней продаж пока нет.</div>';
dashboard.innerHTML=`<div class="dash4"><div class="dash4-head"><div><h1>Главная</h1><p>Ближайшие заказы, быстрые действия и короткая сводка.</p></div></div><div class="dash4-quick">${quick.map(([ic,label,sub])=>`<button class="dash4-qbtn" type="button" onclick="sunDashboardV4Go('${label}')"><span class="dash4-qicon">${icon(ic)}</span><span><b>${label}</b><small>${sub}</small></span></button>`).join('')}</div><div class="dash4-kpis"><div class="dash4-kpi"><div><small>Заказов сегодня</small><b>${todayCount}</b></div><i>С</i></div><div class="dash4-kpi"><div><small>Ближайшие 7 дней</small><b>${next7.length}</b></div><i>7</i></div><div class="dash4-kpi"><div><small>Клиентов за 30 дней</small><b>${clients30}</b></div><i>К</i></div><div class="dash4-kpi"><div><small>Позиций за 30 дней</small><b>${units30}</b></div><i>×</i></div></div><div class="dash4-main"><section class="dash4-panel">${nearestHtml}</section><section class="dash4-panel"><div class="dash4-panel-head"><h2>Следующие заказы</h2><small>${following.length}</small></div><div class="dash4-orders">${followHtml}</div></section></div><div class="dash4-bottom"><section class="dash4-panel"><div class="dash4-panel-head"><h2>Загрузка</h2><small>короткий график</small></div><div class="dash4-load">${loadHtml}</div></section><section class="dash4-panel"><div class="dash4-panel-head"><h2>Лидеры продаж</h2><small>последние 30 дней</small></div><div class="dash4-leaders">${leaderHtml}</div></section></div></div>`;
}
window.renderDashboardV4=renderDash;
if(dashBtn){dashBtn.onclick=()=>{qa('.view').forEach(v=>v.classList.remove('on'));dashboard.classList.add('on');qa('header nav button').forEach(b=>b.classList.remove('on'));dashBtn.classList.add('on');renderDash();};}
if(dashboard?.classList.contains('on'))renderDash();
})();
</script>
<!-- ===== ORDER TABLE REFERENCE + RESIZABLE WORKSPACE v1 ===== -->
<style id="sun-order-reference-resize-style">
/* Order table closer to the supplied reference: readable names and tactile quantity controls. */
#orderForm .sun-order-lines-table{border-radius:10px!important;border-color:#dfe3e6!important;box-shadow:none!important}
#orderForm .sun-order-line-head,
#orderForm .sun-order-line-row{grid-template-columns:minmax(260px,1fr) 175px 105px 125px!important;gap:12px!important}
#orderForm .sun-order-line-head{padding:11px 14px!important;background:#fff!important;color:#141b22!important;font-size:15px!important;font-weight:800!important;text-transform:none!important;letter-spacing:0!important;border-bottom:1px solid #dfe3e6!important}
#orderForm .sun-order-line-head span:first-child{text-align:left!important}
#orderForm .sun-order-line-head span:nth-child(n+2){text-align:center!important}
#orderForm .sun-order-line-head span:last-child{text-align:right!important}
#orderForm .sun-order-line-row{min-height:61px!important;padding:7px 14px!important;border-bottom:1px solid #e3e6e8!important;background:#fff!important}
#orderForm .sun-order-line-row:hover{background:#fbfcfd!important}
#orderForm .sun-order-line-name{font-size:17px!important;font-weight:500!important;color:#0f1720!important;line-height:1.2!important;padding-right:6px!important}
#orderForm .sun-order-line-qty{display:grid!important;grid-template-columns:38px 58px 38px!important;justify-content:center!important;gap:5px!important;align-items:center!important}
#orderForm .sun-order-line-qty button{width:38px!important;height:40px!important;padding:0!important;border:0!important;background:transparent!important;color:#76aee7!important;font-size:36px!important;font-weight:400!important;line-height:36px!important;border-radius:6px!important;box-shadow:none!important}
#orderForm .sun-order-line-qty button:hover{background:#eef6ff!important;color:#4e97df!important}
#orderForm .sun-order-line-qty input{width:58px!important;height:40px!important;padding:3px 4px!important;border:1px solid #cbd0d4!important;border-radius:7px!important;background:#fff!important;color:#1d2730!important;text-align:center!important;font-size:17px!important;font-weight:500!important;box-shadow:inset 0 1px 2px rgba(0,0,0,.035)!important}
#orderForm .sun-order-line-price{font-size:16px!important;color:#1e2932!important;text-align:center!important;white-space:nowrap!important}
#orderForm .sun-order-line-sum{font-size:16px!important;color:#111b24!important;text-align:right!important;white-space:nowrap!important}
#orderForm .sun-order-line-sum b{font-weight:500!important}
#orderForm .sun-order-line-total{grid-template-columns:minmax(260px,1fr) 175px 105px 125px!important;gap:12px!important;padding:12px 14px!important;background:#fff!important;border-top:1px solid #dfe3e6!important}
#orderForm .sun-order-line-total span{grid-column:1/4!important;text-align:left!important;color:#0f1720!important;font-size:17px!important;font-weight:800!important}
#orderForm .sun-order-line-total b{grid-column:4!important;text-align:right!important;color:#0f1720!important;font-size:17px!important;font-weight:800!important}
/* Desktop splitter between catalog and order pane. */
#new .work.sun-resizable-work{position:relative!important;align-items:stretch!important;grid-template-columns:minmax(0,2fr) 10px minmax(0,1fr)!important}
#new .work.sun-resizable-work>.catalog{grid-column:1!important;min-width:0!important}
#new .work.sun-resizable-work>.sun-work-splitter{grid-column:2!important}
#new .work.sun-resizable-work>.sun-order-pane{grid-column:3!important;min-width:0!important}
.sun-work-splitter{width:10px;min-width:10px;align-self:stretch;cursor:col-resize;position:relative;border-radius:8px;background:transparent;touch-action:none;user-select:none}
.sun-work-splitter:before{content:"";position:absolute;left:4px;top:12px;bottom:12px;width:2px;border-radius:3px;background:#d9dde4;transition:background .15s,width .15s,left .15s}
.sun-work-splitter:after{content:"⋮";position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);width:18px;height:34px;border:1px solid #e2e5ea;background:#fff;border-radius:9px;display:grid;place-items:center;color:#a1a8b2;font-size:18px;line-height:1;box-shadow:0 2px 8px rgba(23,32,47,.06);transition:border-color .15s,box-shadow .15s,color .15s}
.sun-work-splitter:hover:before,.sun-work-splitter.sun-dragging:before{left:3px;width:4px;background:#9c8fd0}
.sun-work-splitter:hover:after,.sun-work-splitter.sun-dragging:after{border-color:#c9c0e8;color:#7565b5;box-shadow:0 4px 12px rgba(78,64,137,.12)}
body.sun-splitting{cursor:col-resize!important;user-select:none!important}
body.sun-splitting *{cursor:col-resize!important}
@media(max-width:1180px){
#new .work.sun-resizable-work{grid-template-columns:1fr!important}
#new .work.sun-resizable-work>.catalog,#new .work.sun-resizable-work>.sun-order-pane{grid-column:1!important}
#new .work.sun-resizable-work>.sun-work-splitter{display:none!important}
}
@media(max-width:760px){
#orderForm .sun-order-line-head{display:none!important}
#orderForm .sun-order-line-row{grid-template-columns:minmax(0,1fr) 125px!important;gap:7px 8px!important;padding:10px!important}
#orderForm .sun-order-line-name{grid-column:1/-1!important;font-size:15px!important}
#orderForm .sun-order-line-qty{justify-content:start!important;grid-template-columns:34px 54px 34px!important}
#orderForm .sun-order-line-qty button{width:34px!important;height:34px!important;font-size:31px!important;line-height:31px!important}
#orderForm .sun-order-line-qty input{width:54px!important;height:34px!important;font-size:15px!important}
#orderForm .sun-order-line-price{font-size:13px!important;text-align:right!important}
#orderForm .sun-order-line-sum{grid-column:2!important;font-size:14px!important}
#orderForm .sun-order-line-total{grid-template-columns:1fr auto!important;padding:11px!important}
#orderForm .sun-order-line-total span{grid-column:1!important;font-size:15px!important}
#orderForm .sun-order-line-total b{grid-column:2!important;font-size:16px!important}
}
</style>
<script id="sun-order-reference-resize-script">
(()=>{
const work=document.querySelector('#new .work');
const catalog=work?.querySelector(':scope > .catalog');
if(!work||!catalog)return;
const orderPane=[...work.children].find(el=>el!==catalog&&!el.classList.contains('sun-work-splitter')&&el.matches('section.card'));
if(!orderPane)return;
orderPane.classList.add('sun-order-pane');
work.classList.add('sun-resizable-work');
let splitter=work.querySelector(':scope > .sun-work-splitter');
if(!splitter){
splitter=document.createElement('div');
splitter.className='sun-work-splitter';
splitter.setAttribute('role','separator');
splitter.setAttribute('aria-orientation','vertical');
splitter.setAttribute('aria-label','Изменить ширину каталога и заказа');
splitter.title='Потяните, чтобы изменить ширину. Двойной щелчок — сбросить.';
catalog.insertAdjacentElement('afterend',splitter);
}
const STORAGE_KEY='sunNewOrderSplitRatioV5';
const defaultRatio=2/3;
const clamp=(v,min,max)=>Math.max(min,Math.min(max,v));
function savedRatio(){
try{const n=Number(localStorage.getItem(STORAGE_KEY));return Number.isFinite(n)&&n>.2&&n<.8?n:defaultRatio}catch(_){return defaultRatio}
}
function applyRatio(ratio,save=false){
if(matchMedia('(max-width:1180px)').matches){work.style.removeProperty('grid-template-columns');return;}
const rect=work.getBoundingClientRect();
const splitterW=10;
const minLeft=480,minRight=340;
const usable=Math.max(1,rect.width-splitterW);
const left=clamp(Math.round(usable*ratio),minLeft,Math.max(minLeft,usable-minRight));
const actual=left/usable;
work.style.setProperty('grid-template-columns',`${left}px ${splitterW}px minmax(${minRight}px,1fr)`,'important');
splitter.setAttribute('aria-valuenow',String(Math.round(actual*100)));
if(save){try{localStorage.setItem(STORAGE_KEY,String(actual))}catch(_){}}
}
requestAnimationFrame(()=>requestAnimationFrame(()=>applyRatio(savedRatio())));
let dragging=false;
function move(clientX){
const rect=work.getBoundingClientRect();
const usable=Math.max(1,rect.width-10);
const ratio=(clientX-rect.left)/usable;
applyRatio(ratio,true);
}
splitter.addEventListener('pointerdown',e=>{
if(e.button!==0)return;
dragging=true;splitter.classList.add('sun-dragging');document.body.classList.add('sun-splitting');
try{splitter.setPointerCapture(e.pointerId)}catch(_){}
e.preventDefault();
});
splitter.addEventListener('pointermove',e=>{if(dragging)move(e.clientX)});
const stop=()=>{if(!dragging)return;dragging=false;splitter.classList.remove('sun-dragging');document.body.classList.remove('sun-splitting')};
splitter.addEventListener('pointerup',stop);splitter.addEventListener('pointercancel',stop);
splitter.addEventListener('dblclick',()=>{try{localStorage.removeItem(STORAGE_KEY)}catch(_){}applyRatio(defaultRatio,true)});
let resizeFrame=0;
const reflow=()=>{cancelAnimationFrame(resizeFrame);resizeFrame=requestAnimationFrame(()=>{if(!dragging)applyRatio(savedRatio());});};
window.addEventListener('resize',reflow,{passive:true});
if('ResizeObserver' in window){
const observer=new ResizeObserver(reflow);observer.observe(work);
}
setTimeout(reflow,80);setTimeout(reflow,350);
})();
</script>
<script id="sun-order-viewer-preview-return-fix">
(()=>{
if(window.__sunOrderViewerPreviewReturnPatched)return;
window.__sunOrderViewerPreviewReturnPatched=true;
const previousCloseModal=window.closeModal;
if(typeof previousCloseModal!=='function')return;
function restoreOrderViewer(id){
const orderId=Number(id);
if(!Number.isFinite(orderId))return;
const ordersButton=[...document.querySelectorAll('header nav button')].find(button=>String(button.dataset.navLabel||button.textContent||'').trim()==='Заказы');
if(ordersButton){ordersButton.click();}
else if(typeof window.show==='function'){
try{window.show('orders',document.querySelector('header nav button:nth-of-type(2)'));}catch(_){}
}
setTimeout(()=>window.sunOrderViewerOpen?.(orderId),40);
}
window.closeModal=function(id,...args){
const returnId=id==='preview'?window.__sunOrderViewerReturnId:null;
if(id==='preview'&&returnId){
window.__sunOrderViewerReturnId=null;
window.__sunPrintPreviewReturn=false;
}
const result=previousCloseModal.call(this,id,...args);
if(id==='preview'&&returnId)restoreOrderViewer(returnId);
return result;
};
})();
</script>
<!-- ===== COMPACT ORDER NAMES + LOYALTY CARD IN DETAILS v1 ===== -->
<style id="sun-order-compact-loyalty-style">
/* Keep product names compact and on a single line. */
#orderForm .sun-order-line-head,
#orderForm .sun-order-line-row{grid-template-columns:minmax(220px,1fr) 150px 92px 115px!important;gap:10px!important}
#orderForm .sun-order-line-row{min-height:50px!important;padding:6px 12px!important}
#orderForm .sun-order-line-name{font-size:14px!important;font-weight:500!important;line-height:1.15!important;white-space:nowrap!important;overflow:hidden!important;text-overflow:ellipsis!important;padding-right:8px!important}
#orderForm .sun-order-line-price,#orderForm .sun-order-line-sum{font-size:14px!important}
#orderForm .sun-order-line-total{grid-template-columns:minmax(220px,1fr) 150px 92px 115px!important;gap:10px!important;padding:10px 12px!important}
#orderForm .sun-order-line-total span,#orderForm .sun-order-line-total b{font-size:15px!important}
#details .sun-loyalty-order-field input{letter-spacing:.02em}
@media(max-width:760px){
#orderForm .sun-order-line-row{grid-template-columns:minmax(0,1fr) 122px!important;min-height:0!important}
#orderForm .sun-order-line-name{font-size:13px!important;grid-column:1/-1!important}
#orderForm .sun-order-line-total{grid-template-columns:1fr auto!important}
}
</style>
<script id="sun-order-loyalty-details-script">
(()=>{
const detailsGrid=document.querySelector('#details .sun-delivery-details');
const phoneInput=document.getElementById('phone');
const contactInput=document.getElementById('contact');
if(!detailsGrid||!phoneInput||!contactInput)return;
let field=document.getElementById('orderLoyaltyCard');
if(!field){
const label=document.createElement('label');
label.className='sun-loyalty-order-field';
label.innerHTML='<span>Номер карты лояльности</span><input id="orderLoyaltyCard" autocomplete="off" placeholder="">';
const phoneLabel=phoneInput.closest('label');
phoneLabel?.insertAdjacentElement('afterend',label);
field=label.querySelector('input');
}
const STORE_KEY='sunClientLoyaltyV1';
const defaults=()=>({enabled:false,cardNumber:'',mode:'fixed',fixedDiscount:0,baseDiscount:0,stepAmount:50000,stepPercent:1,maxDiscount:10});
const normalizePhone=value=>String(value||'').replace(/\D/g,'');
const keyFromValues=(phone,name)=>{const p=normalizePhone(phone);if(p)return `p:${p}`;const n=String(name||'').trim().toLowerCase();return n?`n:${n}`:'';};
const readStore=()=>{try{const v=JSON.parse(localStorage.getItem(STORE_KEY)||'{}');return v&&typeof v==='object'?v:{}}catch(_){return {}}};
const writeStore=value=>{try{localStorage.setItem(STORE_KEY,JSON.stringify(value||{}))}catch(_){}};
const currentDraft=()=>{try{return draft}catch(_){return null}};
function cardFromClient(){
const key=keyFromValues(phoneInput.value,contactInput.value);
if(!key)return '';
return String(readStore()[key]?.cardNumber||'').trim();
}
function loadField(){
const d=currentDraft();
const saved=String(d?.loyaltyCardNumber||d?.loyaltyCard||'').trim();
field.value=saved||cardFromClient();
}
function syncDraft(){
const d=currentDraft();
if(d)d.loyaltyCardNumber=String(field.value||'').trim();
}
function syncClientStore(){
const card=String(field.value||'').trim();
const key=keyFromValues(phoneInput.value,contactInput.value);
if(!key||!card)return;
const store=readStore();
store[key]={...defaults(),...(store[key]||{}),cardNumber:card};
writeStore(store);
}
function maybePrefill(){
if(String(field.value||'').trim())return;
const card=cardFromClient();
if(card)field.value=card;
}
field.addEventListener('input',syncDraft);
phoneInput.addEventListener('blur',maybePrefill);
contactInput.addEventListener('blur',maybePrefill);
if(typeof window.saveOrder==='function'){
const previous=window.saveOrder;
window.saveOrder=function(...args){
syncDraft();
syncClientStore();
return previous.apply(this,args);
};
}
if(typeof window.openOrder==='function'){
const previous=window.openOrder;
window.openOrder=function(...args){
const result=previous.apply(this,args);
setTimeout(loadField,0);
return result;
};
}
if(typeof window.resetDraft==='function'){
const previous=window.resetDraft;
window.resetDraft=function(...args){
const result=previous.apply(this,args);
field.value='';
const d=currentDraft();if(d){d.loyaltyCardNumber='';delete d.loyaltyCard;}
return result;
};
}
function decorateNames(){
document.querySelectorAll('#orderForm .sun-order-line-name').forEach(el=>{el.title=el.textContent.trim();});
}
if(typeof window.render==='function'){
const previous=window.render;
window.render=function(...args){const result=previous.apply(this,args);decorateNames();return result;};
}
decorateNames();
loadField();
})();
</script>
<!-- ===== ULTRA COMPACT SINGLE-LINE ORDER TABLE v2 ===== -->
<style id="sun-order-ultracompact-style">
/* Four columns always stay on one line on desktop, even when the order pane is about one third of the workspace. */
#orderForm .sun-order-lines-table{border-radius:9px!important}
#orderForm .sun-order-line-head,
#orderForm .sun-order-line-row,
#orderForm .sun-order-line-total{grid-template-columns:minmax(104px,1fr) 82px 62px 76px!important;gap:4px!important}
#orderForm .sun-order-line-head{padding:7px 8px!important;font-size:10.5px!important;line-height:1!important;white-space:nowrap!important}
#orderForm .sun-order-line-row{min-height:42px!important;padding:5px 8px!important}
#orderForm .sun-order-line-name{font-size:12.5px!important;font-weight:500!important;line-height:1.1!important;white-space:nowrap!important;overflow:hidden!important;text-overflow:ellipsis!important;padding-right:4px!important}
#orderForm .sun-order-line-qty{grid-template-columns:20px 34px 20px!important;gap:4px!important;justify-content:center!important}
#orderForm .sun-order-line-qty button{width:20px!important;height:28px!important;font-size:25px!important;line-height:25px!important;border-radius:5px!important}
#orderForm .sun-order-line-qty input{width:34px!important;height:28px!important;padding:2px!important;font-size:13px!important;border-radius:6px!important}
#orderForm .sun-order-line-price,#orderForm .sun-order-line-sum{font-size:12.5px!important;line-height:1!important;white-space:nowrap!important}
#orderForm .sun-order-line-price{text-align:center!important}
#orderForm .sun-order-line-sum{text-align:right!important}
#orderForm .sun-order-line-total{padding:8px!important}
#orderForm .sun-order-line-total span{grid-column:1/4!important;text-align:left!important;font-size:13px!important}
#orderForm .sun-order-line-total b{grid-column:4!important;text-align:right!important;font-size:13.5px!important}
@media(max-width:1180px){
#orderForm .sun-order-line-head,#orderForm .sun-order-line-row,#orderForm .sun-order-line-total{grid-template-columns:minmax(150px,1fr) 92px 72px 88px!important}
}
@media(max-width:760px){
#orderForm .sun-order-line-head{display:none!important}
#orderForm .sun-order-line-row{grid-template-columns:minmax(0,1fr) 112px!important;gap:6px 8px!important;padding:8px!important}
#orderForm .sun-order-line-name{grid-column:1/-1!important;font-size:13px!important}
#orderForm .sun-order-line-qty{grid-template-columns:26px 44px 26px!important;justify-content:start!important}
#orderForm .sun-order-line-qty button{width:26px!important;height:30px!important;font-size:27px!important}
#orderForm .sun-order-line-qty input{width:44px!important;height:30px!important}
#orderForm .sun-order-line-price{font-size:12px!important;text-align:right!important}
#orderForm .sun-order-line-sum{grid-column:2!important;font-size:13px!important}
#orderForm .sun-order-line-total{grid-template-columns:1fr auto!important}
#orderForm .sun-order-line-total span{grid-column:1!important}
#orderForm .sun-order-line-total b{grid-column:2!important}
}
</style>
<!-- ===== REMOVE DASHBOARD + NEW ORDER DEFAULT 2:1 LAYOUT ===== -->
<script id="sun-remove-dashboard-default-layout">
(()=>{
const dashboard=document.getElementById('dashboard');
const homeButton=[...document.querySelectorAll('header nav button')].find(b=>b.textContent.trim()==='Главная');
const wasDashboardActive=Boolean(dashboard?.classList.contains('on')||homeButton?.classList.contains('on'));
if(homeButton) homeButton.remove();
if(dashboard) dashboard.remove();
if(wasDashboardActive){
const newView=document.getElementById('new');
const newButton=[...document.querySelectorAll('header nav button')].find(b=>b.textContent.trim()==='Новый заказ');
document.querySelectorAll('.view').forEach(v=>v.classList.remove('on'));
newView?.classList.add('on');
document.querySelectorAll('header nav button').forEach(b=>b.classList.remove('on'));
newButton?.classList.add('on');
}
})();
</script>
<!-- ===== STABILITY + MOTION POLISH v1 ===== -->
<style id="sun-stability-motion-style">
@media screen{
html{scroll-behavior:smooth}
body{overflow-x:hidden}
body.sun-modal-open{overflow:hidden!important}
.view.on{animation:sun-view-enter .18s cubic-bezier(.2,.75,.25,1)}
@keyframes sun-view-enter{from{opacity:.35;transform:translateY(5px)}to{opacity:1;transform:none}}
/* Modals remain in the layout as invisible fixed layers so both opening and
closing can animate, even when older modules remove the .on class directly. */
.modal{
display:grid!important;opacity:0;visibility:hidden;pointer-events:none;
transition:opacity .18s ease,visibility 0s linear .18s;
backdrop-filter:blur(0);-webkit-backdrop-filter:blur(0);
}
.modal>.dialog{
opacity:0;transform:translateY(12px) scale(.985);
transition:transform .2s cubic-bezier(.2,.8,.25,1),opacity .16s ease;
scrollbar-gutter:stable;overscroll-behavior:contain;
}
.modal.on{
opacity:1;visibility:visible;pointer-events:auto;transition-delay:0s;
backdrop-filter:blur(3px);-webkit-backdrop-filter:blur(3px);
}
.modal.on>.dialog{opacity:1;transform:translateY(0) scale(1)}
.modal:not(.on)>.dialog{pointer-events:none}
button,.mini,.primary,.outline,.danger{transition:transform .14s ease,box-shadow .14s ease,background-color .14s ease,border-color .14s ease,opacity .14s ease}
button:not(:disabled):active{transform:translateY(1px) scale(.99)}
button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible,[tabindex]:focus-visible{
outline:3px solid rgba(111,91,208,.28)!important;outline-offset:2px!important
}
/* Prevent the catalog category buttons and the view selector from ever
overlapping when the window or the draggable columns are narrow. */
#new .sun-catalog-top-toolbar{flex-wrap:wrap!important;align-items:center!important;overflow:visible!important}
#new .sun-catalog-top-toolbar .cats{flex:1 1 430px!important;min-width:280px!important;width:auto!important;overflow:visible!important}
#new .sun-catalog-top-toolbar .cats button{flex:0 0 auto!important;min-width:max-content!important;width:auto!important;position:relative;z-index:1}
#new .sun-catalog-top-toolbar .sun-catalog-view-switch{position:relative!important;z-index:4!important;flex:0 0 auto!important;max-width:100%;overflow:auto}
#new .sun-catalog-top-toolbar .sun-catalog-view-switch button{flex:0 0 auto}
#tiles .tile{content-visibility:auto;contain-intrinsic-size:150px}
#tiles.sun-view-dense .tile{contain-intrinsic-size:46px}
#tiles.sun-view-list .tile{contain-intrinsic-size:62px}
}
@media screen and (max-width:1450px){
#new .sun-catalog-top-toolbar .cats{flex-basis:100%!important}
#new .sun-catalog-top-toolbar .sun-catalog-view-switch{margin-left:0!important}
}
@media (prefers-reduced-motion:reduce){
.view.on,.modal,.modal>.dialog,button,.mini,.primary,.outline,.danger{animation:none!important;transition:none!important;scroll-behavior:auto!important}
}
@media print{
#preview.modal{opacity:1!important;visibility:visible!important;pointer-events:auto!important;backdrop-filter:none!important}
#preview.modal>.dialog{opacity:1!important;transform:none!important}
}
</style>
<script id="sun-stability-motion-script">
(()=>{
'use strict';
if(window.__sunStabilityMotionV1)return;window.__sunStabilityMotionV1=true;
const modalState=new WeakMap();
let layer=1200;
let backdropPress=null;
const MOVE_LIMIT=9;
const openModals=()=>[...document.querySelectorAll('.modal.on')];
const topModal=()=>openModals().sort((a,b)=>(Number(getComputedStyle(a).zIndex)||0)-(Number(getComputedStyle(b).zIndex)||0)).pop();
const syncBody=()=>document.body.classList.toggle('sun-modal-open',openModals().length>0);
function onOpened(modal){
if(!modalState.has(modal))modalState.set(modal,{opener:document.activeElement});
modal.setAttribute('role','dialog');modal.setAttribute('aria-modal','true');modal.setAttribute('aria-hidden','false');
modal.style.zIndex=String(++layer);syncBody();
requestAnimationFrame(()=>{
const active=document.activeElement;
if(active&&modal.contains(active))return;
const focusable=modal.querySelector('[autofocus],input:not([type="hidden"]):not([disabled]),select:not([disabled]),textarea:not([disabled]),button:not([disabled]),[tabindex]:not([tabindex="-1"])');
focusable?.focus({preventScroll:true});
});
}
function onClosed(modal){
modal.setAttribute('aria-hidden','true');syncBody();
const state=modalState.get(modal);modalState.delete(modal);
const opener=state?.opener;
if(opener&&opener.isConnected)setTimeout(()=>{if(!openModals().length||!document.activeElement||document.activeElement===document.body)opener.focus?.({preventScroll:true});},190);
}
document.querySelectorAll('.modal').forEach(m=>{m.setAttribute('aria-hidden',m.classList.contains('on')?'false':'true');if(m.classList.contains('on'))onOpened(m);});
const observer=new MutationObserver(records=>{
records.forEach(record=>{
const modal=record.target;if(!(modal instanceof HTMLElement)||!modal.classList.contains('modal'))return;
const was=modal.dataset.sunObservedOpen==='1',now=modal.classList.contains('on');
if(now&&!was){modal.dataset.sunObservedOpen='1';onOpened(modal);}
else if(!now&&was){modal.dataset.sunObservedOpen='0';onClosed(modal);}
});
});
document.querySelectorAll('.modal').forEach(m=>{m.dataset.sunObservedOpen=m.classList.contains('on')?'1':'0';observer.observe(m,{attributes:true,attributeFilter:['class']});});
const bodyObserver=new MutationObserver(records=>{
records.forEach(record=>{
record.addedNodes.forEach(node=>{
if(!(node instanceof HTMLElement))return;
const nested=[...node.querySelectorAll('.modal')];
const modals=node.matches('.modal')?[node,...nested]:nested;
modals.forEach(m=>{m.dataset.sunObservedOpen=m.classList.contains('on')?'1':'0';m.setAttribute('aria-hidden',m.classList.contains('on')?'false':'true');observer.observe(m,{attributes:true,attributeFilter:['class']});if(m.classList.contains('on'))onOpened(m);});
node.querySelectorAll('img').forEach(enhanceImage);
});
});
});
bodyObserver.observe(document.body,{childList:true,subtree:true});
/* Capture backdrop clicks before the older generic handler. Only a stationary
press that starts and ends on the same backdrop is allowed to close. */
document.addEventListener('pointerdown',event=>{
const modal=event.target instanceof Element?event.target.closest('.modal.on'):null;
backdropPress=modal&&event.target===modal?{modal,x:event.clientX,y:event.clientY,id:event.pointerId,moved:false}:null;
},true);
document.addEventListener('pointermove',event=>{
if(!backdropPress||backdropPress.id!==event.pointerId)return;
if(Math.hypot(event.clientX-backdropPress.x,event.clientY-backdropPress.y)>MOVE_LIMIT)backdropPress.moved=true;
},true);
document.addEventListener('click',event=>{
const modal=event.target instanceof Element?event.target.closest('.modal.on'):null;
if(!modal||event.target!==modal)return;
const state=backdropPress;backdropPress=null;
event.preventDefault();event.stopImmediatePropagation();
if(!state||state.modal!==modal||state.moved)return;
window.closeModal?.(modal.id);
},true);
document.addEventListener('pointercancel',()=>{backdropPress=null;},true);
document.addEventListener('keydown',event=>{
if(event.key!=='Escape'||event.isComposing)return;
const modal=topModal();if(!modal?.id)return;
event.preventDefault();event.stopImmediatePropagation();window.closeModal?.(modal.id);
},true);
function enhanceImage(img){
if(!(img instanceof HTMLImageElement))return;
if(!img.loading)img.loading='lazy';img.decoding='async';
}
document.querySelectorAll('img').forEach(enhanceImage);
document.addEventListener('error',event=>{
const img=event.target;if(!(img instanceof HTMLImageElement)||img.dataset.sunFallback==='1')return;
if(String(img.getAttribute('src')||'').includes((window.CateriumBranding.documentImage())))return;
img.dataset.sunFallback='1';img.src=(window.CateriumBranding.documentImage());img.alt=img.alt||(window.CateriumBranding.documentName());
},true);
/* Expose a small health snapshot for troubleshooting without changing the UI. */
window.SunAppHealth={
snapshot:()=>({
activeView:document.querySelector('.view.on')?.id||'',
openModals:openModals().map(m=>m.id),
duplicateIds:(()=>{const ids=new Map();document.querySelectorAll('[id]').forEach(e=>ids.set(e.id,(ids.get(e.id)||0)+1));return [...ids].filter(([,n])=>n>1).map(([id,n])=>({id,n}));})(),
orders:(()=>{try{return JSON.parse(localStorage.getItem('sunOrders')||'[]').length}catch(_){return 0}})(),
catalog:(()=>{try{return JSON.parse(localStorage.getItem('sunBoxes')||'[]').length}catch(_){return 0}})()
})
};
})();
</script>
<!-- ===== OPTIONAL ORDER STATUS + PRICE/BALANCE FIX v2 ===== -->
<style id="sun-order-status-optional-style-v2">
#details .sun-order-status-field{grid-column:1/-1!important;display:grid!important;grid-template-columns:minmax(0,1fr) auto!important;gap:7px!important;align-items:end!important}
#details .sun-order-status-field>span{grid-column:1/-1;color:#68717a;font-size:13px}
#details .sun-order-status-field select{min-width:0;width:100%}
#details .sun-order-status-clear{height:35px;min-width:74px;padding:6px 10px;border:1px solid #d9dde0;border-radius:7px;background:#fff;color:#7a5560;font-weight:700}
#details .sun-order-status-clear:hover{background:#fff7f8;border-color:#e5c8ce}
.sun-order-view-status.sun-status-empty{display:none!important}
.sun-doc-badge.sun-status-empty{display:none!important}
#new .sun-order-pane{container-type:inline-size}
#new .sun-order-pane .money-form{grid-template-columns:repeat(3,minmax(0,1fr))!important;min-width:0!important}
#new .sun-order-pane .money-form label,#new .sun-order-pane .money-form input{min-width:0!important;width:100%!important}
#new .sun-order-pane .order-summary{min-width:0!important}
#new .sun-order-pane .order-summary p{gap:10px!important;min-width:0!important}
#new .sun-order-pane .order-summary p b{white-space:nowrap!important;flex:0 0 auto!important}
#orderForm .sun-order-line-price{display:flex!important;align-items:center!important;justify-content:center!important}
#orderForm .sun-order-line-price input{width:100%!important;max-width:92px!important;min-width:0!important;height:28px!important;padding:2px 5px!important;border:1px solid #cbd0d4!important;border-radius:6px!important;background:#fff!important;color:#1d2730!important;text-align:center!important;font-size:12.5px!important;font-weight:600!important}
#orderForm .sun-order-line-price input:focus{outline:none!important;border-color:#7d70c7!important;box-shadow:0 0 0 3px rgba(111,91,208,.12)!important}
@container (max-width:680px){#new .sun-order-pane .money-form{grid-template-columns:repeat(2,minmax(0,1fr))!important}#new .sun-order-pane .money-form label:last-child{grid-column:1/-1!important}}
@container (max-width:520px){#new .sun-order-pane .money-form{grid-template-columns:1fr!important}#new .sun-order-pane .money-form label:last-child{grid-column:auto!important}}
@media(max-width:680px){#details .sun-order-status-field{grid-template-columns:minmax(0,1fr) auto!important}}
</style>
<script id="sun-order-status-optional-script-v2">
(()=>{
'use strict';
if(window.__sunOrderStatusOptionalV2)return;window.__sunOrderStatusOptionalV2=true;
const STATUS_VALUES=['Новый','В работе','Собран','Передан курьеру','Отдан заказчику','Отменён'];
const NONE='';
const NONE_SENTINEL='__sun_no_status__';
const moneyFmt=v=>typeof money==='function'?money(v):`${Math.round(Number(v||0)).toLocaleString('ru-RU')}`;
const itemById=id=>boxes.find(x=>String(x.id)===String(id));
const unitPrice=line=>{
const direct=Number(line?.price);
return Number.isFinite(direct)&&direct>=0?direct:Math.max(0,Number(itemById(line?.id)?.price||0));
};
const currentOrder=()=>orders.find(o=>String(o.id)===String(draft?.id));
const pane=document.querySelector('#new .work > section.card');
pane?.classList.add('sun-order-pane');
// Keep the shared pricing functions: they already preserve per-order prices,
// include delivery once, and apply the saved manual/promo discounts.
// Add optional status to Details.
const detailsGrid=document.querySelector('#details .sun-delivery-details');
let statusSelect=document.getElementById('orderStatusOptional');
if(detailsGrid&&!statusSelect){
const wrap=document.createElement('label');
wrap.className='sun-order-status-field';
wrap.innerHTML=`<span>Статус заказа</span><select id="orderStatusOptional" aria-label="Статус заказа"><option value="">Без статуса</option>${STATUS_VALUES.map(s=>`<option value="${s}">${s}</option>`).join('')}</select><button class="sun-order-status-clear" id="orderStatusClear" type="button">Убрать</button>`;
detailsGrid.appendChild(wrap);statusSelect=wrap.querySelector('select');
wrap.querySelector('#orderStatusClear').onclick=()=>{statusSelect.value='';draft.status='';};
statusSelect.onchange=()=>{draft.status=statusSelect.value;};
}
const syncStatusField=(fresh=false)=>{
if(!statusSelect)return;
const value=fresh?'Новый':String(draft?.status||'');
statusSelect.value=STATUS_VALUES.includes(value)?value:'';
};
// Turn price text into editable input after the existing renderer finishes.
function decorateLinePrices(){
const rows=[...document.querySelectorAll('#orderForm .sun-order-line-row')];
rows.forEach((row,index)=>{
const line=draft.lines?.[index];if(!line)return;
if(!Object.prototype.hasOwnProperty.call(line,'price')||!Number.isFinite(Number(line.price)))line.price=Math.max(0,Number(itemById(line.id)?.price||0));
const price=unitPrice(line),qty=Math.max(0,Number(line.qty||0));
const priceCell=row.querySelector('.sun-order-line-price');
const sumCell=row.querySelector('.sun-order-line-sum');
if(priceCell){
priceCell.innerHTML=`<input type="text" inputmode="numeric" pattern="[0-9]*" autocomplete="off" value="${Math.round(price)}" aria-label="Цена позиции">`;
const input=priceCell.querySelector('input');
input.oninput=()=>{const clean=String(input.value||'').replace(/\D+/g,'').slice(0,9);if(input.value!==clean)input.value=clean;};
input.onchange=()=>{
// Do not rebuild the whole order table from the blur/change event of
// the focused price input: replacing its parent DOM during blur can
// trigger Chromium NotFoundError and make the interface feel frozen.
line.price=Math.max(0,Math.round(Number(String(input.value||'').replace(/\D+/g,'')||0)));
const next=unitPrice(line),nextQty=Math.max(0,Number(line.qty||0));
if(sumCell)sumCell.innerHTML=`<b>${moneyFmt(next*nextQty)}</b>`;
const totalEl=document.querySelector('#orderForm .sun-order-line-total b');
if(totalEl)totalEl.textContent=moneyFmt(calcOrderTotal());
updateOrderSummary();
};
}
if(sumCell)sumCell.innerHTML=`<b>${moneyFmt(price*qty)}</b>`;
});
const totalEl=document.querySelector('#orderForm .sun-order-line-total b');
if(totalEl)totalEl.textContent=moneyFmt(calcOrderTotal());
updateOrderSummary();
}
const priorRender=window.render;
if(typeof priorRender==='function')window.render=function(...args){const r=priorRender.apply(this,args);decorateLinePrices();return r;};
// New positions remember their catalog price at the moment they are added.
const priorAdd=window.add;
if(typeof priorAdd==='function')window.add=function(id,...args){
const before=(draft.lines||[]).find(x=>String(x.id)===String(id));
const r=priorAdd.call(this,id,...args);
const line=(draft.lines||[]).find(x=>String(x.id)===String(id));
if(line&&!before&&!Object.prototype.hasOwnProperty.call(line,'price'))line.price=Math.max(0,Number(itemById(id)?.price||0));
decorateLinePrices();return r;
};
function normalizeNoStatus(value){return value===NONE_SENTINEL?'':String(value||'')}
function finalizeSavedStatus(wanted){
draft.status=normalizeNoStatus(wanted);
const saved=currentOrder();if(saved)saved.status=draft.status;
persist();window.renderOrders?.();decorateOrderStatusUI();
}
const priorSave=window.saveOrder;
if(typeof priorSave==='function')window.saveOrder=function(...args){
(draft.lines||[]).forEach(line=>{if(!Object.prototype.hasOwnProperty.call(line,'price'))line.price=Math.max(0,Number(itemById(line.id)?.price||0));});
const wanted=statusSelect?statusSelect.value:String(draft.status||'Новый');
draft.status=wanted||NONE_SENTINEL; // prevents old core from forcing "Новый"
const result=priorSave.apply(this,args);
if(result&&typeof result.then==='function')return result.then(value=>{finalizeSavedStatus(wanted);return value;},err=>{draft.status=normalizeNoStatus(wanted);throw err;});
finalizeSavedStatus(wanted);return result;
};
const priorOpen=window.openOrder;
if(typeof priorOpen==='function')window.openOrder=function(...args){const r=priorOpen.apply(this,args);setTimeout(()=>{syncStatusField(false);decorateLinePrices();decorateOrderStatusUI();},0);return r;};
const priorReset=window.resetDraft;
if(typeof priorReset==='function')window.resetDraft=function(...args){const r=priorReset.apply(this,args);draft.status='Новый';setTimeout(()=>{syncStatusField(true);decorateLinePrices();},0);return r;};
const priorStatus=window.status;
window.status=function(id,value){
const order=orders.find(o=>String(o.id)===String(id));if(order)order.status=String(value||'');
if(String(draft?.id)===String(id)){draft.status=String(value||'');syncStatusField(false);}
persist();window.renderOrders?.();decorateOrderStatusUI();
try{if(typeof priorStatus==='function'&&value){} }catch(_){}
};
function ensureBlankOption(select){
if(!select)return;
if(![...select.options].some(o=>o.value===''))select.insertAdjacentHTML('afterbegin','<option value="">Без статуса</option>');
}
function decorateOrderStatusUI(){
document.querySelectorAll('#ordersList .row').forEach(row=>{
const id=String(row.querySelector('span')?.textContent||'').replace(/\D/g,'');
const order=orders.find(o=>String(o.id)===id),select=row.querySelector('select');if(!order||!select)return;
ensureBlankOption(select);select.value=String(order.status||'');
});
document.querySelectorAll('#sunOrderCards [data-card-status]').forEach(select=>{
const order=orders.find(o=>String(o.id)===String(select.dataset.cardStatus));if(!order)return;
ensureBlankOption(select);select.value=String(order.status||'');
});
}
const priorRenderOrders=window.renderOrders;
if(typeof priorRenderOrders==='function')window.renderOrders=function(...args){const r=priorRenderOrders.apply(this,args);setTimeout(decorateOrderStatusUI,0);return r;};
// Keep cards/list/viewer and printed order blank consistent when status was removed.
let statusDecorateQueued=false;
function decorateDynamicStatus(){
statusDecorateQueued=false;
decorateOrderStatusUI();
const viewer=document.getElementById('sun-order-viewer');
if(viewer?.classList.contains('on')){
const id=(viewer.querySelector('.sun-order-view-title h2')?.textContent||'').replace(/\D/g,'');
const order=orders.find(o=>String(o.id)===id),chip=viewer.querySelector('.sun-order-view-status');
if(chip&&order){
const next=String(order.status||'');
if(chip.textContent!==next)chip.textContent=next;
chip.classList.toggle('sun-status-empty',!next);
}
}
const badge=document.querySelector('#preview .sun-doc-badge');
if(badge){
const next=String(draft.status||'');
if(badge.textContent!==next)badge.textContent=next;
badge.classList.toggle('sun-status-empty',!next);
}
}
const observer=new MutationObserver(()=>{
if(statusDecorateQueued)return;
statusDecorateQueued=true;
requestAnimationFrame(decorateDynamicStatus);
});
observer.observe(document.body,{childList:true,subtree:true});
// Existing stored orders keep their current status. New drafts default to "Новый".
if(!draft.id&&!draft.status)draft.status='Новый';
syncStatusField(!draft.id);
setTimeout(()=>{window.render?.();decorateOrderStatusUI();},0);
})();
</script>
<style id="sun-orders-edit-date-color-fix-v1">
/* Orders: make the date tile more visible and keep editing as the primary action. */
#sunOrderCards .sun-order-card-date{
background:linear-gradient(145deg,#6f5bd0,#8b78df)!important;
border-color:#6f5bd0!important;
color:#fff!important;
box-shadow:0 5px 14px rgba(91,73,184,.22)!important;
}
#sunOrderCards .sun-order-card-date strong,
#sunOrderCards .sun-order-card-date span,
#sunOrderCards .sun-order-card-date small{color:#fff!important}
#sunOrderCards .sun-order-card-date small{opacity:.88}
#ordersList .row{cursor:pointer!important}
</style>
<!-- ===== ORDER DETAILS: SCHEDULE + PRECISE ADDRESS + MAP PRINT OPTION v1 ===== -->
<style id="sun-order-details-precise-address-style-v1">
#orderForm>.form{grid-template-columns:minmax(0,1fr)!important}
#details .sun-order-schedule{grid-column:1/-1;display:grid;grid-template-columns:minmax(0,1fr) minmax(110px,.45fr);gap:10px 12px;padding:10px 0 12px;margin-bottom:2px;border-bottom:1px solid #eceef2}
#details .sun-order-schedule label{margin:0}
#details .sun-address-precision-title{grid-column:1/-1;margin:5px 0 -1px;color:#30394f;font-size:12px;font-weight:800;letter-spacing:.02em}
#details .sun-delivery-region{grid-column:auto}
#details .sun-delivery-locality{grid-column:auto}
#details .sun-delivery-district{grid-column:1/-1}
#details .sun-print-map-option{grid-column:1/-1;display:flex!important;flex-direction:row!important;align-items:center!important;gap:9px!important;margin-top:3px;padding:9px 10px;border:1px solid #e2e5ea;border-radius:9px;background:#fafbfc;color:#364152!important;font-size:12.5px!important}
#details .sun-print-map-option input{width:17px!important;height:17px!important;flex:0 0 auto;margin:0}
#details .sun-print-map-option small{display:block;color:#7b8490;font-size:11px;margin-top:2px}
#details #openEventCardBtn,#details .event-card-section,.event-card-section,#openEventCardBtn{display:none!important}
@media(max-width:680px){
#details .sun-order-schedule{grid-template-columns:1fr}
#details .sun-delivery-region,#details .sun-delivery-locality,#details .sun-delivery-district{grid-column:1/-1}
}
</style>
<script id="sun-order-details-precise-address-script-v1">
(()=>{
'use strict';
if(window.__sunOrderDetailsPreciseAddressV1)return;window.__sunOrderDetailsPreciseAddressV1=true;
const q=(s,r=document)=>r.querySelector(s);
const details=q('#details');
const grid=q('#details .sun-delivery-details');
const orderForm=q('#orderForm');
const dateInput=q('#date');
const timeInput=q('#time');
const addressInput=q('#address');
if(!details||!grid||!orderForm||!dateInput||!timeInput||!addressInput)return;
// Move issue date/time out of the Order tab into Details without recreating inputs,
// so all existing save/open/calendar logic continues using the same elements.
let schedule=q('#details .sun-order-schedule');
if(!schedule){
schedule=document.createElement('div');schedule.className='sun-order-schedule';
const dateLabel=dateInput.closest('label'),timeLabel=timeInput.closest('label');
if(dateLabel)schedule.appendChild(dateLabel);
if(timeLabel)schedule.appendChild(timeLabel);
grid.insertAdjacentElement('beforebegin',schedule);
}
// Keep the existing guests input, but move it into Details instead of hiding it
// with the legacy operational event card. This preserves all old save/open hooks.
const moveGuestsToDetails=()=>{
const guests=q('#eventGuests');
if(!guests||!grid)return;
const label=guests.closest('label');
if(!label)return;
label.classList.add('sun-guests-details-field');
if(!label.querySelector(':scope > span')){
[...label.childNodes].filter(n=>n.nodeType===3).forEach(n=>n.remove());
const caption=document.createElement('span');caption.textContent='Количество гостей';label.insertBefore(caption,guests);
}
window.SunSafe.insertBefore(grid,label,grid.firstChild);
};
const removeEventCard=()=>{
moveGuestsToDetails();
q('#openEventCardBtn')?.remove();
document.querySelectorAll('.event-card-section').forEach(el=>{
el.hidden=true;
el.setAttribute('aria-hidden','true');
el.style.setProperty('display','none','important');
});
};
removeEventCard();setTimeout(removeEventCard,80);setTimeout(removeEventCard,400);
// Address disambiguation: region + locality + municipal/district context.
if(!q('#deliveryRegion')){
const title=document.createElement('div');title.className='sun-address-precision-title';title.textContent='Точный адрес доставки';
const region=document.createElement('label');region.className='sun-delivery-region';region.innerHTML=("<span>Регион</span><select id=\"deliveryRegion\"><option value=\"\"></option><option>Санкт-Петербург</option><option>Ленинградская область</option><option>Другой регион</option></select>");
const locality=document.createElement('label');locality.className='sun-delivery-locality';locality.innerHTML='<span>Город / населённый пункт</span><input id="deliveryLocality" autocomplete="address-level2">';
const district=document.createElement('label');district.className='sun-delivery-district';district.innerHTML='<span>Район / муниципалитет</span><input id="deliveryDistrict" autocomplete="address-level3">';
window.SunSafe.insertBefore(grid,title,grid.firstChild);title.insertAdjacentElement('afterend',region);region.insertAdjacentElement('afterend',locality);locality.insertAdjacentElement('afterend',district);
moveGuestsToDetails();
}
// Order-level print preference. Enabled by default for backward compatibility.
if(!q('#orderPrintMap')){
const label=document.createElement('label');label.className='sun-print-map-option';
label.innerHTML='<input id="orderPrintMap" type="checkbox" checked><span><b>Печатать карту в бланке заказа</b><small>Снимите галочку, если нужен бланк без карты.</small></span>';
grid.appendChild(label);
}
const region=q('#deliveryRegion'),locality=q('#deliveryLocality'),district=q('#deliveryDistrict'),printMap=q('#orderPrintMap');
const street=q('#deliveryStreet'),house=q('#deliveryHouse'),entrance=q('#deliveryEntrance'),floor=q('#deliveryFloor'),apartment=q('#deliveryApartment'),office=q('#deliveryOffice');
const val=el=>String(el?.value||'').trim();
const currentDraft=()=>{try{return draft}catch(_){return null}};
const currentOrders=()=>{try{return orders}catch(_){return []}};
function composePreciseAddress(){
const parts=[
val(region),val(locality),val(district),val(street),
val(house)?`д. ${val(house)}`:'',
val(entrance)?`подъезд ${val(entrance)}`:'',
val(floor)?`этаж ${val(floor)}`:'',
val(apartment)?`кв. ${val(apartment)}`:'',
val(office)?`офис ${val(office)}`:''
].filter(Boolean);
const next=parts.join(', ');
if(addressInput.value!==next){addressInput.value=next;addressInput.dispatchEvent(new Event('input',{bubbles:true}));}
return next;
}
window.sunComposeDeliveryAddress=composePreciseAddress;
[region,locality,district,street,house,entrance,floor,apartment,office].forEach(el=>el?.addEventListener('input',()=>setTimeout(composePreciseAddress,0)));
region?.addEventListener('change',composePreciseAddress);
function writeExtraToDraft(){
const d=currentDraft();if(!d)return;
d.deliveryRegion=val(region);d.deliveryLocality=val(locality);d.deliveryDistrict=val(district);
d.printMap=Boolean(printMap?.checked);
d.address=composePreciseAddress();
}
function finalizeSaved(){
writeExtraToDraft();
const d=currentDraft();if(!d?.id)return;
const saved=currentOrders().find(o=>String(o.id)===String(d.id));
if(saved){
saved.deliveryRegion=d.deliveryRegion;saved.deliveryLocality=d.deliveryLocality;saved.deliveryDistrict=d.deliveryDistrict;
saved.printMap=d.printMap;saved.address=d.address;
try{persist()}catch(_){try{localStorage.setItem('sunOrders',JSON.stringify(currentOrders()))}catch(__){}}
}
}
function fillExtra(){
const d=currentDraft()||{};
region.value=String(d.deliveryRegion||'');
locality.value=String(d.deliveryLocality||'');
district.value=String(d.deliveryDistrict||'');
printMap.checked=d.printMap!==false;
// For older orders, keep the legacy address untouched unless precise fields exist.
const hasPrecise=Boolean(val(region)||val(locality)||val(district));
if(hasPrecise)composePreciseAddress();
}
const prevSave=window.saveOrder;
if(typeof prevSave==='function')window.saveOrder=function(...args){
writeExtraToDraft();
const result=prevSave.apply(this,args);
if(result&&typeof result.then==='function')return result.then(v=>{finalizeSaved();return v;},e=>{throw e});
finalizeSaved();return result;
};
const prevOpen=window.openOrder;
if(typeof prevOpen==='function')window.openOrder=function(...args){const r=prevOpen.apply(this,args);setTimeout(fillExtra,0);return r;};
const prevReset=window.resetDraft;
if(typeof prevReset==='function')window.resetDraft=function(...args){const r=prevReset.apply(this,args);region.value='';locality.value='';district.value='';printMap.checked=true;const d=currentDraft();if(d){d.deliveryRegion='';d.deliveryLocality='';d.deliveryDistrict='';d.printMap=true;}return r;};
printMap.addEventListener('change',()=>{const d=currentDraft();if(d)d.printMap=Boolean(printMap.checked);});
// Enforce the saved/selected map preference in the order blank even if older map
// modules try to inject a map asynchronously after the preview opens.
function stripMapWhenDisabled(){
const d=currentDraft();if(!d||d.printMap!==false)return;
const doc=q('#preview .sun-doc');if(!doc)return;
const title=doc.querySelector('.sun-doc-title h2')?.textContent||'';
if(!/Бланк заказа/i.test(title))return;
doc.querySelectorAll('.sun-doc-map,.sun-doc-route').forEach(el=>el.remove());
}
const preview=q('#preview');
if(preview)new MutationObserver(stripMapWhenDisabled).observe(preview,{childList:true,subtree:true});
document.addEventListener('click',e=>{const b=e.target.closest('button');if(b?.textContent.trim()==='Бланк заказа')setTimeout(stripMapWhenDisabled,0);},true);
fillExtra();
})();
</script>
<!-- ===== NO-HANG BUTTON SAFETY v1 ===== -->
<style id="sun-nohang-button-safety-style">
button.sun-busy-safe{position:relative;pointer-events:none;opacity:.72}
button.sun-busy-safe:after{content:"";width:12px;height:12px;margin-left:7px;border:2px solid currentColor;border-right-color:transparent;border-radius:50%;display:inline-block;vertical-align:-2px;animation:sunSafeSpin .65s linear infinite}
@keyframes sunSafeSpin{to{transform:rotate(360deg)}}
</style>
<script id="sun-nohang-button-safety-script">
(()=>{
'use strict';
if(window.__sunNoHangButtonSafetyV1)return;window.__sunNoHangButtonSafetyV1=true;
const toast=(text,type='warn')=>{try{window.SunEnterprise?.toast?.(text,type,3500)}catch(_){}};
// Never allow an accidental double-click to launch the same heavy action twice.
const heavyText=/^(Построить маршрут|Обновить маршрут|Создать карту|Обновить карту|Отправить в облако|Загрузить из облака)$/i;
document.addEventListener('click',e=>{
const b=e.target.closest?.('button');if(!b||!heavyText.test((b.textContent||'').trim()))return;
if(b.dataset.sunBusySafe==='1'){e.preventDefault();e.stopImmediatePropagation();return;}
b.dataset.sunBusySafe='1';b.classList.add('sun-busy-safe');
// Short debounce only. Long artificial disabling used to look like a freeze
// even when the async operation had already failed or completed quickly.
setTimeout(()=>{if(!b.isConnected)return;b.dataset.sunBusySafe='0';b.classList.remove('sun-busy-safe');},1800);
},true);
// Surface runtime errors instead of leaving the UI looking frozen.
window.addEventListener('error',e=>{if(e?.message)toast('Ошибка интерфейса: '+String(e.message).slice(0,120),'error')});
window.addEventListener('unhandledrejection',e=>{const m=e?.reason?.message||String(e?.reason||'');if(m)toast('Операция не завершена: '+m.slice(0,120),'warn')});
})();
</script>
<!-- ===== RECEIPT SETTINGS + CLEAN DETAILS + PDF ACTION v1 ===== -->
<style id="sun-receipt-settings-clean-style-v2">
#orderEconomyPanel{display:none!important}
#details #sunReceiptBuyerCard{display:none!important}
.sun-receipt-settings-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}
.sun-receipt-settings-grid .wide{grid-column:1/-1}
.sun-receipt-settings-checks{display:flex;gap:14px;flex-wrap:wrap;margin:8px 0 12px}
.sun-receipt-settings-checks label{display:flex;flex-direction:row;align-items:center;gap:7px;color:#394653}
.sun-receipt-settings-checks input{width:17px;height:17px}
.sun-receipt-settings-section{border:1px solid #e3e6ea;border-radius:12px;padding:14px;margin-top:12px;background:#fafbfc}
.sun-receipt-settings-section h3{margin:0 0 10px;color:#15364c;font-size:15px}
.sun-receipt-party{margin:12px 0;padding:12px;border:1px solid #dde2e6;border-radius:10px;background:#fafbfc}
.sun-receipt-party h3{margin:0 0 8px;color:#15364c;font-size:14px}
.sun-receipt-party-grid{display:grid;grid-template-columns:1fr 1fr;gap:6px 18px;font-size:12px}
.sun-receipt-party-grid div{padding:3px 0;border-bottom:1px solid #edf0f2}
.sun-receipt-party-grid span{display:block;color:#7b858d;font-size:10px;text-transform:uppercase;letter-spacing:.04em;margin-bottom:2px}
.sun-receipt-party-grid b{font-weight:600;color:#203040;overflow-wrap:anywhere}
.sun-catalog-pdf-button{white-space:nowrap}
#sunCatalogPdfPrintRoot{display:none}
@media print{
body.sun-print-catalog-pdf *{visibility:hidden!important}
body.sun-print-catalog-pdf #sunCatalogPdfPrintRoot,
body.sun-print-catalog-pdf #sunCatalogPdfPrintRoot *{visibility:visible!important}
body.sun-print-catalog-pdf #sunCatalogPdfPrintRoot{display:block!important;position:absolute!important;inset:0!important;background:#fff!important;padding:0!important}
.sun-catalog-pdf-page{page-break-after:always;break-after:page;width:100%;height:277mm;padding:10mm 12mm;display:flex;flex-direction:column;align-items:center;justify-content:flex-start;overflow:hidden;font-family:Arial,sans-serif;color:#17222d;background:#fff}
.sun-catalog-pdf-page:last-child{page-break-after:auto;break-after:auto}
.sun-catalog-pdf-head{width:100%;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #d9dde0;padding-bottom:5mm;margin-bottom:7mm}
.sun-catalog-pdf-brand{display:flex;align-items:center;gap:4mm;color:#15364c;font-weight:700;font-size:14pt}
.sun-catalog-pdf-brand img{width:24mm;height:15mm;object-fit:contain}
.sun-catalog-pdf-category{font-size:10pt;color:#69727c}
.sun-catalog-pdf-photo{width:100%;height:190mm;object-fit:contain;object-position:center;background:#fff}
.sun-catalog-pdf-name{width:100%;font-size:20pt;line-height:1.15;color:#15364c;font-weight:800;text-align:center;margin-top:7mm}
.sun-catalog-pdf-meta{width:100%;display:flex;justify-content:center;gap:8mm;flex-wrap:wrap;margin-top:3mm;font-size:11pt;color:#68717a;text-align:center}
.sun-catalog-pdf-price{font-size:16pt;font-weight:800;color:#15364c;margin-top:3mm}
@page{size:A4 portrait;margin:0}
}
@media(max-width:700px){.sun-receipt-settings-grid,.sun-receipt-party-grid{grid-template-columns:1fr}.sun-receipt-settings-grid .wide{grid-column:auto}}
</style>
<script id="sun-receipt-settings-clean-script-v2">
(()=>{
'use strict';
if(window.__sunReceiptSettingsCleanV2)return;window.__sunReceiptSettingsCleanV2=true;
const $=id=>document.getElementById(id);
const qa=(s,r=document)=>[...r.querySelectorAll(s)];
const esc=window.SunSafe.escapeHTML;
const money=v=>`${Number(v||0).toLocaleString('ru-RU')}`;
const SETTINGS_KEY='sunReceiptSettingsV2';
const LEGACY_KEY='sunReceiptSettingsV1';
const defaults=()=>({
includeSeller:true,includeBuyer:true,
sellerName:(window.CateriumBranding.documentName()),sellerLegalName:'',sellerInn:'',sellerKpp:'',sellerOgrn:'',sellerLegalAddress:'',sellerPhone:'',sellerEmail:'',sellerBank:'',sellerBik:'',sellerAccount:'',sellerCorrAccount:'',
buyerName:'',buyerInn:'',buyerKpp:'',buyerOgrn:'',buyerLegalAddress:'',buyerPhone:'',buyerEmail:'',buyerBank:'',buyerBik:'',buyerAccount:'',buyerCorrAccount:'',buyerContact:''
});
function readSettings(){
try{
const current=JSON.parse(localStorage.getItem(SETTINGS_KEY)||'null');
if(current&&typeof current==='object')return {...defaults(),...current};
const legacy=JSON.parse(localStorage.getItem(LEGACY_KEY)||'null');
return legacy&&typeof legacy==='object'?{...defaults(),...legacy}:defaults();
}catch(_){return defaults()}
}
const saveSettings=v=>{try{localStorage.setItem(SETTINGS_KEY,JSON.stringify({...defaults(),...(v||{})}))}catch(_){}};
/* Details should contain only order/delivery data. Company requisites live in Settings. */
const removeBuyerCard=()=>{$('sunReceiptBuyerCard')?.remove()};
const hideEconomy=()=>{const p=$('orderEconomyPanel');if(p)p.style.setProperty('display','none','important')};
removeBuyerCard();hideEconomy();
const cleanupRoot=$('details');if(cleanupRoot){let cleanupQueued=false;const cleanupObserver=new MutationObserver(()=>{if(cleanupQueued)return;cleanupQueued=true;requestAnimationFrame(()=>{cleanupQueued=false;removeBuyerCard();hideEconomy();});});cleanupObserver.observe(cleanupRoot,{childList:true,subtree:true});}
function injectReceiptSettings(){
const view=$('enterprise-settings');const grid=view?.querySelector('.enterprise-grid');if(!grid)return;
$('sunReceiptSettingsCard')?.remove();
const s=readSettings();
const card=document.createElement('section');card.className='enterprise-card wide';card.id='sunReceiptSettingsCard';
card.innerHTML=`<h2>Настройки реквизитов товарного чека</h2><p class="hint">Все реквизиты настраиваются здесь. В «Деталях» заказа они больше не показываются.</p>
<div class="sun-receipt-settings-checks"><label><input id="receiptIncludeSeller" type="checkbox" ${s.includeSeller?'checked':''}> Печатать продавца</label><label><input id="receiptIncludeBuyer" type="checkbox" ${s.includeBuyer?'checked':''}> Печатать покупателя</label></div>
<section class="sun-receipt-settings-section"><h3>Продавец — ваша компания</h3><div class="sun-receipt-settings-grid">
<label>Название бренда<input id="receiptSellerName" value="${esc(s.sellerName)}"></label><label>Юр. название / ИП<input id="receiptSellerLegalName" value="${esc(s.sellerLegalName)}"></label>
<label>ИНН<input id="receiptSellerInn" value="${esc(s.sellerInn)}"></label><label>КПП<input id="receiptSellerKpp" value="${esc(s.sellerKpp)}"></label><label>ОГРН / ОГРНИП<input id="receiptSellerOgrn" value="${esc(s.sellerOgrn)}"></label><label>Телефон<input id="receiptSellerPhone" value="${esc(s.sellerPhone)}"></label>
<label class="wide">Юридический адрес<input id="receiptSellerLegalAddress" value="${esc(s.sellerLegalAddress)}"></label><label class="wide">E-mail<input id="receiptSellerEmail" value="${esc(s.sellerEmail)}"></label><label class="wide">Банк<input id="receiptSellerBank" value="${esc(s.sellerBank)}"></label>
<label>БИК<input id="receiptSellerBik" value="${esc(s.sellerBik)}"></label><label>Расчётный счёт<input id="receiptSellerAccount" value="${esc(s.sellerAccount)}"></label><label class="wide">Корреспондентский счёт<input id="receiptSellerCorrAccount" value="${esc(s.sellerCorrAccount)}"></label>
</div></section>
<section class="sun-receipt-settings-section"><h3>Покупатель — компания заказчика</h3><div class="sun-receipt-settings-grid">
<label>Компания / ИП<input id="receiptBuyerNameSetting" value="${esc(s.buyerName)}"></label><label>Контактное лицо<input id="receiptBuyerContactSetting" value="${esc(s.buyerContact)}"></label>
<label>ИНН<input id="receiptBuyerInnSetting" value="${esc(s.buyerInn)}"></label><label>КПП<input id="receiptBuyerKppSetting" value="${esc(s.buyerKpp)}"></label><label>ОГРН / ОГРНИП<input id="receiptBuyerOgrnSetting" value="${esc(s.buyerOgrn)}"></label><label>Телефон<input id="receiptBuyerPhoneSetting" value="${esc(s.buyerPhone)}"></label>
<label class="wide">Юридический адрес<input id="receiptBuyerLegalAddressSetting" value="${esc(s.buyerLegalAddress)}"></label><label class="wide">E-mail<input id="receiptBuyerEmailSetting" value="${esc(s.buyerEmail)}"></label><label class="wide">Банк<input id="receiptBuyerBankSetting" value="${esc(s.buyerBank)}"></label>
<label>БИК<input id="receiptBuyerBikSetting" value="${esc(s.buyerBik)}"></label><label>Расчётный счёт<input id="receiptBuyerAccountSetting" value="${esc(s.buyerAccount)}"></label><label class="wide">Корреспондентский счёт<input id="receiptBuyerCorrAccountSetting" value="${esc(s.buyerCorrAccount)}"></label>
</div></section>
<div class="actions"><button class="primary" id="receiptSettingsSave" type="button">Сохранить реквизиты</button></div>`;
window.SunSafe.insertBefore(grid,card,grid.lastElementChild||null);
$('receiptSettingsSave').onclick=()=>{
saveSettings({
includeSeller:$('receiptIncludeSeller').checked,includeBuyer:$('receiptIncludeBuyer').checked,
sellerName:$('receiptSellerName').value.trim(),sellerLegalName:$('receiptSellerLegalName').value.trim(),sellerInn:$('receiptSellerInn').value.trim(),sellerKpp:$('receiptSellerKpp').value.trim(),sellerOgrn:$('receiptSellerOgrn').value.trim(),sellerLegalAddress:$('receiptSellerLegalAddress').value.trim(),sellerPhone:$('receiptSellerPhone').value.trim(),sellerEmail:$('receiptSellerEmail').value.trim(),sellerBank:$('receiptSellerBank').value.trim(),sellerBik:$('receiptSellerBik').value.trim(),sellerAccount:$('receiptSellerAccount').value.trim(),sellerCorrAccount:$('receiptSellerCorrAccount').value.trim(),
buyerName:$('receiptBuyerNameSetting').value.trim(),buyerContact:$('receiptBuyerContactSetting').value.trim(),buyerInn:$('receiptBuyerInnSetting').value.trim(),buyerKpp:$('receiptBuyerKppSetting').value.trim(),buyerOgrn:$('receiptBuyerOgrnSetting').value.trim(),buyerLegalAddress:$('receiptBuyerLegalAddressSetting').value.trim(),buyerPhone:$('receiptBuyerPhoneSetting').value.trim(),buyerEmail:$('receiptBuyerEmailSetting').value.trim(),buyerBank:$('receiptBuyerBankSetting').value.trim(),buyerBik:$('receiptBuyerBikSetting').value.trim(),buyerAccount:$('receiptBuyerAccountSetting').value.trim(),buyerCorrAccount:$('receiptBuyerCorrAccountSetting').value.trim()
});
window.SunEnterprise?.toast?.('Реквизиты товарного чека сохранены.','success');
};
}
const settingsBtn=qa('header nav button').find(b=>String(b.dataset.navLabel||b.textContent||'').trim()==='Настройки');
settingsBtn?.addEventListener('click',()=>setTimeout(injectReceiptSettings,20));
if($('enterprise-settings')?.classList.contains('on'))setTimeout(injectReceiptSettings,0);
function fieldsHtml(title,rows){const filtered=rows.filter(([,v])=>String(v||'').trim());if(!filtered.length)return'';return `<section class="sun-receipt-party"><h3>${esc(title)}</h3><div class="sun-receipt-party-grid">${filtered.map(([k,v])=>`<div><span>${esc(k)}</span><b>${esc(v)}</b></div>`).join('')}</div></section>`;}
function currentDraft(){try{return draft}catch(_){return null}}
function currentRows(){const d=currentDraft()||{};return(d.lines||[]).map((line,index)=>{const item=(typeof boxes!=='undefined'?boxes:[]).find(x=>String(x.id)===String(line.id));const direct=Number(line?.price),price=Number.isFinite(direct)&&direct>=0?direct:Number(item?.price||0),qty=Math.max(0,Number(line.qty||0));return{index:index+1,name:item?.name||line?.name||`Позиция ${line?.id||''}`,qty,price,sum:price*qty};}).filter(x=>x.qty>0)}
function docDate(raw){if(!raw)return'—';const d=new Date(`${raw}T00:00:00`);return Number.isNaN(d.getTime())?raw:d.toLocaleDateString('ru-RU')}
function receiptActions(){return `<div class="sun-doc-actions"><button class="primary" type="button" onclick="window.print()">Печать</button><button class="outline" type="button" onclick="closeModal('preview')">Закрыть</button></div>`;}
function showConfiguredReceipt(){
const d=currentDraft()||{},rows=currentRows(),total=rows.reduce((sum,row)=>sum+row.sum,0),paid=Math.max(0,Number($('prepayment')?.value||d.prepayment||0)),balance=Math.max(0,total-paid),s=readSettings();
const sellerBlock=s.includeSeller?fieldsHtml('Продавец',[["Наименование",s.sellerLegalName||s.sellerName],["ИНН",s.sellerInn],["КПП",s.sellerKpp],["ОГРН / ОГРНИП",s.sellerOgrn],["Юридический адрес",s.sellerLegalAddress],["Телефон",s.sellerPhone],["E-mail",s.sellerEmail],["Банк",s.sellerBank],["БИК",s.sellerBik],["Расчётный счёт",s.sellerAccount],["Корр. счёт",s.sellerCorrAccount]]):'';
const buyerBlock=s.includeBuyer?fieldsHtml('Покупатель',[["Компания / ИП",s.buyerName],["ИНН",s.buyerInn],["КПП",s.buyerKpp],["ОГРН / ОГРНИП",s.buyerOgrn],["Юридический адрес",s.buyerLegalAddress],["Контактное лицо",s.buyerContact],["Телефон",s.buyerPhone],["E-mail",s.buyerEmail],["Банк",s.buyerBank],["БИК",s.buyerBik],["Расчётный счёт",s.buyerAccount],["Корр. счёт",s.buyerCorrAccount]]):'';
const dialog=document.querySelector('#preview .dialog');if(!dialog)return;
dialog.innerHTML=`<div class="sun-doc sun-receipt"><div class="sun-doc-head"><div class="sun-doc-brand"><img src="${window.CateriumBranding.logoHTML()}" alt="${window.CateriumBranding.nameHTML()}"><div><strong>${esc(s.sellerName||(window.CateriumBranding.documentName()))}</strong><small>${esc(s.sellerLegalName||'')}</small></div></div><div class="sun-doc-title"><h2>Товарный чек № ${esc(d.id||'—')}</h2><p>${esc(docDate($('date')?.value||d.date))} · ${esc($('time')?.value||d.time||'')}</p></div></div>${sellerBlock}${buyerBlock}<table><thead><tr><th class="doc-num">№</th><th class="doc-name">Наименование</th><th>Цена</th><th>Кол.</th><th>Сумма</th></tr></thead><tbody>${rows.length?rows.map(r=>`<tr><td class="doc-num">${r.index}</td><td class="doc-name">${esc(r.name)}</td><td>${money(r.price)}</td><td>${r.qty}</td><td>${money(r.sum)}</td></tr>`).join(''):'<tr><td colspan="5">В заказе нет позиций.</td></tr>'}</tbody></table><div class="sun-doc-summary"><p><span>Итого</span><b>${money(total)}</b></p><p><span>Оплачено</span><b>${money(paid)}</b></p><p class="grand"><span>К оплате</span><span>${money(balance)}</span></p></div>${$('note')?.value?`<div class="sun-doc-note"><b>Примечание</b>${esc($('note').value)}</div>`:''}<div class="sun-doc-footer"><div><div class="sun-doc-sign">Продавец / подпись</div></div><div><div class="sun-doc-sign">Покупатель / подпись</div></div></div>${receiptActions()}</div>`;
window.modal?.('preview');
}
window.sunShowConfiguredReceipt=showConfiguredReceipt;
function patchReceiptButton(){qa('#new .actions button').filter(b=>b.textContent.trim()==='Товарный чек').forEach(b=>{b.onclick=showConfiguredReceipt;b.dataset.sunReceiptConfigured='2';});}
patchReceiptButton();setTimeout(patchReceiptButton,100);setTimeout(patchReceiptButton,500);
const actionObserver=new MutationObserver(patchReceiptButton);const actions=[...document.querySelectorAll('#new .actions')].find(a=>[...a.querySelectorAll('button')].some(b=>b.textContent.trim()==='Сохранить'));if(actions)actionObserver.observe(actions,{childList:true,subtree:true});
/* If older code adds “Скачать PDF” to a receipt preview, remove it only from the receipt. */
const preview=$('preview');
const stripReceiptPdf=()=>{if(preview?.querySelector('.sun-receipt'))qa('.sun-pdf-download',preview).forEach(b=>b.remove())};
const previewObserver=new MutationObserver(()=>setTimeout(stripReceiptPdf,0));if(preview)previewObserver.observe(preview,{attributes:true,childList:true,subtree:true,attributeFilter:['class']});
/* Catalog PDF: current category, one full-photo item per A4 page. */
function currentCatalogCategory(){
const active=qa('#new .sun-catalog-top-toolbar .cats button,#new .cats button').find(b=>b.style.outline&&b.style.outline!=='none')||qa('#new .sun-catalog-top-toolbar .cats button,#new .cats button').find(b=>b.getAttribute('aria-current')==='true');
if(active?.dataset.category!=null)return Number(active.dataset.category);
const title=document.querySelector('#new .catalog h1')?.textContent?.trim()||'';
const map={'Боксы':0,'Премиум':5,'Посуда':1,'Дополнения':2,'Напитки':3,'Доставка':4};
return Object.entries(map).find(([name])=>title.startsWith(name))?.[1]??0;
}
function categoryName(cat){return ({0:'Боксы',1:'Посуда',2:'Дополнения',3:'Напитки',4:'Доставка',5:'Премиум'})[cat]||'Каталог'}
function getCatalogItems(cat){try{return (Array.isArray(boxes)?boxes:[]).filter(x=>Number(x.category||0)===Number(cat))}catch(_){return[]}}
function ensureCatalogPdfButton(){
const head=document.querySelector('#new .catalog .catalog-head');const configure=qa('button',head||document).find(b=>b.textContent.trim()==='Настроить');if(!head||!configure)return;
let btn=$('sunCatalogPdfButton');if(!btn){btn=document.createElement('button');btn.id='sunCatalogPdfButton';btn.type='button';btn.className='outline sun-catalog-pdf-button';btn.textContent='Скачать PDF';btn.title='Текущий раздел: одна позиция на одной странице';configure.insertAdjacentElement('beforebegin',btn);}
btn.onclick=printCatalogPdf;
const cat=currentCatalogCategory();btn.hidden=cat===4;btn.style.display=cat===4?'none':'';
}
function printCatalogPdf(){
const cat=currentCatalogCategory(),items=getCatalogItems(cat).filter(item=>item.photo);
if(!items.length){window.SunEnterprise?.toast?.('В этом разделе нет позиций с фотографиями.','warn');return;}
let root=$('sunCatalogPdfPrintRoot');if(!root){root=document.createElement('div');root.id='sunCatalogPdfPrintRoot';document.body.appendChild(root);}
root.innerHTML=items.map(item=>`<article class="sun-catalog-pdf-page"><div class="sun-catalog-pdf-head"><div class="sun-catalog-pdf-brand"><img src="${window.CateriumBranding.logoHTML()}" alt=""><span>${window.CateriumBranding.nameHTML()}</span></div><div class="sun-catalog-pdf-category">${esc(categoryName(cat))}</div></div><img class="sun-catalog-pdf-photo" src="${esc(item.photo)}" alt="${esc(item.name||'')}"><div class="sun-catalog-pdf-name">${esc(item.name||'')}</div><div class="sun-catalog-pdf-meta">${item.catalogSection?`<span>${esc(item.catalogSection)}</span>`:''}${item.weight?`<span>${esc(item.weight)}</span>`:''}</div><div class="sun-catalog-pdf-price">${money(item.price||0)}${Number(item.oldPrice||0)>Number(item.price||0)?` <span style="font-size:.72em;color:#8e949d;text-decoration:line-through;font-weight:500">${money(item.oldPrice)}</span>`:""}</div></article>`).join('');
document.body.classList.add('sun-print-catalog-pdf');
const cleanup=()=>document.body.classList.remove('sun-print-catalog-pdf');
window.addEventListener('afterprint',cleanup,{once:true});
setTimeout(()=>{window.print();setTimeout(cleanup,800);},40);
}
ensureCatalogPdfButton();setTimeout(ensureCatalogPdfButton,100);setTimeout(ensureCatalogPdfButton,500);
document.addEventListener('click',e=>{if(e.target.closest('#new .cats button,#new .sun-catalog-top-toolbar .cats button'))setTimeout(ensureCatalogPdfButton,0)},true);
})();
</script>
<!-- ===== PRINT / DETAILS / STATUS / CALENDAR POLISH v2 ===== -->
<style id="sun-print-details-status-polish-v2">
/* Details: keep legacy compatibility nodes, but remove them from the manager UI. */
#details .sun-address-precision-title,
#details .sun-delivery-region,
#details .sun-delivery-locality,
#details .sun-delivery-district,
#details .sun-print-map-option{display:none!important}
/* Prepayment should be a plain numeric field without browser spinner arrows. */
#prepayment{-moz-appearance:textfield!important;appearance:textfield!important}
#prepayment::-webkit-outer-spin-button,#prepayment::-webkit-inner-spin-button{-webkit-appearance:none!important;margin:0!important}
/* Status is an optional global feature. Default: completely hidden. */
body.sun-status-disabled #details .sun-order-status-wrap,
body.sun-status-disabled #details #orderStatusOptional,
body.sun-status-disabled #details #orderStatusClear,
body.sun-status-disabled #sunOrderCards .sun-order-card-status,
body.sun-status-disabled .sun-order-view-status,
body.sun-status-disabled .client-order-status,
body.sun-status-disabled .geo-order-status,
body.sun-status-disabled .sun-route-status,
body.sun-status-disabled .dash3-status,
body.sun-status-disabled .dash4-next-status,
body.sun-status-disabled #ordersList select,
body.sun-status-disabled #calendar .cal-status,
body.sun-status-disabled #order-map-view #mapStatus,
body.sun-status-disabled #order-map-view #mapStatus+*,
body.sun-status-disabled #order-map-view .static-map-badge,
body.sun-status-disabled #route-map-view #routeStatus,
body.sun-status-disabled #route-map-view #routeStatus+*,
body.sun-status-disabled .sun-doc-badge{display:none!important}
body.sun-status-disabled #order-map-view label:has(#mapStatus),
body.sun-status-disabled #route-map-view label:has(#routeStatus){display:none!important}
body.sun-status-disabled #orders .head span:nth-child(6){display:none!important}
body.sun-status-disabled #orders .head{grid-template-columns:65px 1.15fr 145px 1fr 110px 150px!important}
body.sun-status-disabled #ordersList .row{grid-template-columns:65px 1.15fr 145px 1fr 110px 150px!important}
/* New corpus field follows the existing compact Details grid. */
#details .sun-delivery-corpus{min-width:0}
/* Print center additions. */
.sun-print-extra-card{margin-top:12px}
.sun-print-extra-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px 12px}
.sun-print-extra-grid .print-check{padding:8px 9px;border:1px solid #e7e8ed;border-radius:9px;background:#fafbfc}
@media(max-width:720px){.sun-print-extra-grid{grid-template-columns:1fr}}
/* Group preparations by box. */
.prep-box-groups{display:grid;gap:12px}
.prep-box-group{border:1px solid #e0e3e8;border-radius:10px;padding:11px;break-inside:avoid;background:#fff}
.prep-box-group-head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:8px}
.prep-box-group-head h3{margin:0!important;font-size:15px!important}
.prep-box-group-head b{color:#6e5f36;white-space:nowrap}
.prep-box-group-body{display:grid;grid-template-columns:minmax(0,1fr) 125px;gap:10px;align-items:start}
.prep-box-group .prep-list{grid-template-columns:repeat(2,minmax(0,1fr))}
.prep-box-group-photo img{width:100%;height:105px;object-fit:cover;border-radius:7px;background:#f1f2f4}
.prep-box-group-photo small{display:block;margin-top:4px;color:#737b86;text-align:center}
@media(max-width:720px){.prep-box-group-body{grid-template-columns:1fr}.prep-box-group .prep-list{grid-template-columns:1fr}.prep-box-group-photo{max-width:180px}}
/* Dedicated calendar print sheet: one landscape A4 page. */
#sunCalendarPrintRoot{display:none}
@media print{
@page{size:A4 landscape;margin:5mm}
body.sun-print-calendar-v2>*:not(#sunCalendarPrintRoot){display:none!important}
body.sun-print-calendar-v2{background:#fff!important;margin:0!important;padding:0!important;overflow:visible!important}
body.sun-print-calendar-v2 #sunCalendarPrintRoot{display:block!important;width:277mm!important;height:190mm!important;overflow:hidden!important;background:#fff!important;color:#20242d!important;zoom:var(--sun-cal-print-scale,1)}
#sunCalendarPrintRoot .sun-cal-print-head{display:flex;justify-content:space-between;align-items:end;border-bottom:1px solid #bbb;padding-bottom:2mm;margin-bottom:2mm}
#sunCalendarPrintRoot .sun-cal-print-head h1{font-size:15pt;margin:0}
#sunCalendarPrintRoot .sun-cal-print-head b{font-size:10pt}
#sunCalendarPrintRoot .cal-scroll{overflow:visible!important}
#sunCalendarPrintRoot .cal-month{display:grid!important;grid-template-columns:repeat(7,1fr)!important;grid-template-rows:7mm repeat(6,27mm)!important;gap:0!important;border:1px solid #cfd3d8!important;height:169mm!important}
#sunCalendarPrintRoot .cal-weekday{font-size:8pt!important;padding:1.5mm!important;background:#f2f3f5!important;border:0!important;border-right:1px solid #d9dde1!important}
#sunCalendarPrintRoot .cal-day{min-height:0!important;height:27mm!important;padding:1.2mm!important;border-radius:0!important;border:0!important;border-right:1px solid #d9dde1!important;border-top:1px solid #d9dde1!important;overflow:hidden!important}
#sunCalendarPrintRoot .cal-date{margin-bottom:.7mm!important}.cal-date span{font-size:8pt!important}
#sunCalendarPrintRoot .cal-event{padding:1mm!important;margin:.5mm 0!important;border-radius:1mm!important;min-height:0!important}
#sunCalendarPrintRoot .cal-event b{font-size:6.5pt!important;line-height:1.05!important}
#sunCalendarPrintRoot .cal-event small{font-size:5.8pt!important;line-height:1.05!important;white-space:nowrap!important;overflow:hidden!important;text-overflow:ellipsis!important}
#sunCalendarPrintRoot .cal-count,#sunCalendarPrintRoot .cal-more{font-size:5.5pt!important}
#sunCalendarPrintRoot .cal-agenda{display:grid!important;grid-template-columns:repeat(2,minmax(0,1fr))!important;gap:2mm!important}
#sunCalendarPrintRoot .cal-agenda-day{break-inside:avoid!important;margin:0!important}
#sunCalendarPrintRoot .cal-agenda-date{font-size:8pt!important;padding:1mm 0!important}
#sunCalendarPrintRoot .cal-agenda-order{grid-template-columns:15mm minmax(0,1fr)!important;gap:1mm!important;padding:1mm!important;font-size:6.4pt!important}
#sunCalendarPrintRoot .cal-agenda-order button,#sunCalendarPrintRoot .cal-status{display:none!important}
#sunCalendarPrintRoot .cal-agenda-order small{font-size:5.8pt!important}
}
</style>
<script id="sun-print-details-status-polish-script-v2">
(()=>{
'use strict';
if(window.__sunPrintDetailsStatusPolishV2)return;window.__sunPrintDetailsStatusPolishV2=true;
const $=id=>document.getElementById(id),qa=(s,r=document)=>[...r.querySelectorAll(s)];
const getDraft=()=>{try{return draft}catch(_){return null}};
const getOrders=()=>{try{return orders}catch(_){return[]}};
const getBoxes=()=>{try{return boxes}catch(_){return[]}};
const esc=window.SunSafe.escapeHTML;
const fmtQty=n=>{const x=Number(n||0);return Number.isInteger(x)?String(x):x.toLocaleString('ru-RU',{maximumFractionDigits:2})};
/* 1) Details address: hide region/locality/district and add Corpus beside House. */
const grid=document.querySelector('#details .sun-delivery-details');
let corpus=$('deliveryCorpus');
if(grid&&!corpus){
const house=$('deliveryHouse')?.closest('label');
const label=document.createElement('label');label.className='sun-delivery-corpus';label.innerHTML='<span>Корпус</span><input id="deliveryCorpus" autocomplete="address-line2">';
if(house)house.insertAdjacentElement('afterend',label);else grid.appendChild(label);corpus=$('deliveryCorpus');
}
const street=$('deliveryStreet'),house=$('deliveryHouse'),entrance=$('deliveryEntrance'),floor=$('deliveryFloor'),apartment=$('deliveryApartment'),office=$('deliveryOffice'),address=$('address');
const val=el=>String(el?.value||'').trim();
function composeSimpleAddress(){
if(!address)return'';
const parts=[val(street),val(house)?`д. ${val(house)}`:'',val(corpus)?`корп. ${val(corpus)}`:'',val(entrance)?`подъезд ${val(entrance)}`:'',val(floor)?`этаж ${val(floor)}`:'',val(apartment)?`кв. ${val(apartment)}`:'',val(office)?`офис ${val(office)}`:''].filter(Boolean);
const next=parts.join(', ');
if(address.value!==next){address.value=next;address.dispatchEvent(new Event('input',{bubbles:true}));}
const d=getDraft();if(d){d.deliveryCorpus=val(corpus);d.address=next;}
return next;
}
[street,house,corpus,entrance,floor,apartment,office].forEach(el=>el?.addEventListener('input',()=>setTimeout(composeSimpleAddress,0)));
if(corpus)corpus.addEventListener('change',composeSimpleAddress);
window.sunComposeDeliveryAddress=composeSimpleAddress;
function fillCorpus(){const d=getDraft()||{};if(corpus)corpus.value=String(d.deliveryCorpus||'');setTimeout(composeSimpleAddress,0)}
const prevOpen=window.openOrder;if(typeof prevOpen==='function')window.openOrder=function(...args){const r=prevOpen.apply(this,args);setTimeout(fillCorpus,0);return r};
const prevReset=window.resetDraft;if(typeof prevReset==='function')window.resetDraft=function(...args){const r=prevReset.apply(this,args);if(corpus)corpus.value='';const d=getDraft();if(d)d.deliveryCorpus='';setTimeout(composeSimpleAddress,0);return r};
const prevSave=window.saveOrder;if(typeof prevSave==='function')window.saveOrder=function(...args){composeSimpleAddress();const d=getDraft();if(d)d.deliveryCorpus=val(corpus);const r=prevSave.apply(this,args);const finish=()=>{const cur=getDraft();if(cur?.id){const saved=getOrders().find(o=>String(o.id)===String(cur.id));if(saved){saved.deliveryCorpus=val(corpus);saved.address=composeSimpleAddress();try{persist()}catch(_){}}}};if(r&&typeof r.then==='function')return r.then(v=>{finish();return v});finish();return r};
/* 2) Status becomes an optional application-wide feature, disabled by default. */
const STATUS_KEY='sunOrderStatusFeatureEnabledV1';
const statusEnabled=()=>localStorage.getItem(STATUS_KEY)==='1';
function patchCalendarStatus(enabled){
const cal=$('calendar');if(!cal)return;
const p=cal.querySelector('.cal-head p');if(p){if(!p.dataset.fullText)p.dataset.fullText=p.textContent||'';{const next=enabled?p.dataset.fullText:'Все мероприятия, суммы и загрузка по датам.';if(p.textContent!==next)p.textContent=next;}}
qa('.cal-event',cal).forEach(ev=>{const smalls=qa('small',ev),el=smalls[1];if(!el)return;if(!el.dataset.statusFull)el.dataset.statusFull=el.textContent||'';if(enabled){if(el.textContent!==el.dataset.statusFull)el.textContent=el.dataset.statusFull;return}const next=el.dataset.statusFull.replace(/\s·\s(?:Новый|В работе|Собран|Передан курьеру|Отдан заказчику|Отменён)\s*$/,'');if(el.textContent!==next)el.textContent=next;});
}
function applyStatusFeature(){
const enabled=statusEnabled();document.body.classList.toggle('sun-status-disabled',!enabled);
for(const id of ['mapStatus','routeStatus']){const el=$(id);if(!enabled&&el&&el.value!=='all'){el.value='all';el.dispatchEvent(new Event('change',{bubbles:true}));}}
patchCalendarStatus(enabled);
qa('#stats .stats-panel').forEach(panel=>{const isStatus=/Статусы заказов/i.test(panel.querySelector('h2')?.textContent||'');if(isStatus)panel.style.setProperty('display',enabled?'':'none',enabled?'':'important');});
qa('.route-row').forEach(row=>{const cols=[...row.children],p=cols[3]?.querySelector('p');if(p){if(!p.dataset.statusText)p.dataset.statusText=p.textContent||'';p.style.display=enabled?'':'none';if(enabled&&p.dataset.statusText)p.textContent=p.dataset.statusText;}});
const preview=$('preview');if(!enabled)qa('.sun-doc-badge,.static-map-badge',preview||document).forEach(x=>x.style.setProperty('display','none','important'));
}
window.sunApplyOrderStatusFeature=applyStatusFeature;
applyStatusFeature();
let statusApplyQueued=false;const statusObserver=new MutationObserver(()=>{if(statusApplyQueued)return;statusApplyQueued=true;requestAnimationFrame(()=>{statusApplyQueued=false;applyStatusFeature();});});['calendar','stats','routeList','preview'].map($).filter(Boolean).forEach(root=>statusObserver.observe(root,{childList:true,subtree:true}));
/* 3) Settings card for enabling Status globally. */
function injectStatusSettings(){
const view=$('enterprise-settings'),grid=view?.querySelector('.enterprise-grid');if(!grid||$('sunOrderStatusFeatureCard'))return;
const card=document.createElement('section');card.className='enterprise-card wide';card.id='sunOrderStatusFeatureCard';
card.innerHTML=`<h2>Статусы заказов</h2><p class="hint">Функция выключена по умолчанию. Пока она выключена, статус нигде в приложении не показывается. После включения статусы появятся у всех заказов.</p><label class="print-check"><input id="sunOrderStatusFeatureToggle" type="checkbox" ${statusEnabled()?'checked':''}> Включить статусы заказов</label>`;
grid.appendChild(card);
$('sunOrderStatusFeatureToggle').onchange=e=>{localStorage.setItem(STATUS_KEY,e.target.checked?'1':'0');applyStatusFeature();try{window.renderOrders?.()}catch(_){};window.SunEnterprise?.toast?.(e.target.checked?'Статусы заказов включены.':'Статусы заказов скрыты.','success');};
}
document.addEventListener('click',e=>{const b=e.target.closest('header nav button');if(b&&String(b.dataset.navLabel||b.textContent||'').trim()==='Настройки')setTimeout(injectStatusSettings,20)},true);
setTimeout(injectStatusSettings,80);
/* 4) Print center: map and document options; remove QR-label document. */
const PRINT_KEY='sunPrintSettingsV1';
const readPrint=()=>Object.assign({showMapInBlank:true,showBlankPhone:true,showBlankNote:true,showBlankTotals:true},(()=>{try{return JSON.parse(localStorage.getItem(PRINT_KEY)||'{}')}catch(_){return{}}})());
const savePrint=patch=>{const next=Object.assign({},readPrint(),patch);localStorage.setItem(PRINT_KEY,JSON.stringify(next));return next};
function syncHiddenOrderMapSetting(){const s=readPrint(),cb=$('orderPrintMap'),d=getDraft();if(cb){cb.checked=s.showMapInBlank;cb.dispatchEvent(new Event('change',{bubbles:true}));}if(d)d.printMap=s.showMapInBlank;}
function injectPrintOptions(){
const view=$('sun-print-center');if(!view)return;
const type=$('printType');if(type){qa('option',type).filter(o=>o.value==='labels'||/Этикетки/i.test(o.textContent)).forEach(o=>o.remove());}
const intro=view.querySelector('.print-center-head .hint');if(intro&&intro.textContent!=='Бланки, чеки, заготовки, фотографии и лист курьеру.')intro.textContent='Бланки, чеки, заготовки, фотографии и лист курьеру.';
$('sunPrintExtraOptions')?.remove();
}
function applyBlankOptions(){
const doc=document.querySelector('#preview .sun-doc:not(.sun-receipt)');if(!doc)return;
const s=readPrint();syncHiddenOrderMapSetting();
qa('.sun-doc-field',doc).forEach(field=>{const label=field.querySelector('span')?.textContent?.trim()||'';if(label==='Телефон'&&!s.showBlankPhone)field.remove();});
if(!s.showBlankNote)qa('.sun-doc-note',doc).forEach(x=>x.remove());
if(!s.showBlankTotals)qa('.sun-doc-summary',doc).forEach(x=>x.remove());
if(!s.showMapInBlank)qa('.sun-doc-map,.sun-doc-route',doc).forEach(x=>x.remove());
if(!statusEnabled())qa('.sun-doc-badge',doc).forEach(x=>x.remove());
}
document.addEventListener('click',e=>{const b=e.target.closest('button');if(b?.textContent.trim()==='Бланк заказа'){syncHiddenOrderMapSetting();setTimeout(applyBlankOptions,0);setTimeout(applyBlankOptions,150)}},true);
const preview=$('preview');if(preview)new MutationObserver(()=>setTimeout(applyBlankOptions,0)).observe(preview,{childList:true,subtree:true,attributes:true,attributeFilter:['class']});
document.addEventListener('click',e=>{const b=e.target.closest('header nav button');if(b&&String(b.dataset.navLabel||b.textContent||'').trim()==='Печать')setTimeout(injectPrintOptions,30)},true);
setTimeout(injectPrintOptions,120);
const pc=$('sun-print-center');if(pc)new MutationObserver(()=>setTimeout(injectPrintOptions,0)).observe(pc,{childList:true,subtree:true});
/* 5) Preparations: group ingredients under each box, preserving photo options. */
window.openPreview=()=>{
const d=getDraft()||{lines:[]},items=getBoxes(),settings=readPrint();
const lines=(d.lines||[]).map(line=>({line,item:items.find(x=>String(x.id)===String(line.id)),qty:Math.max(0,Number(line.qty||0))})).filter(x=>x.item&&x.qty>0&&[0,5].includes(Number(x.item.category||0)));
const dialog=document.querySelector('#preview .dialog');if(!dialog)return;
const eventName=$('event')?.value||d.event||'',orderDate=$('date')?.value||d.date||'',orderTime=$('time')?.value||d.time||'';
const photoOnly=Boolean(settings.photosOnly);
const groups=lines.map(({item,qty})=>{
const ingredients=(item.ingredients||[]).filter(part=>Array.isArray(part)&&part[0]).map(part=>`<p><b>${esc(part[0])}</b><span>${esc(fmtQty(Number(part[1]||0)*qty))} ${esc(part[2]||'')}</span></p>`).join('')||'<div class="prep-empty">Состав для заготовок не указан.</div>';
const photo=item.photo?`<div class="prep-box-group-photo"><img src="${esc(item.photo)}" alt="${esc(item.name)}"><small>${esc(item.weight||'')}</small></div>`:'';
return `<section class="prep-box-group"><div class="prep-box-group-head"><h3>${esc(item.name)}</h3><b>× ${esc(qty)}</b></div><div class="prep-box-group-body">${photoOnly?'':`<div class="prep-list">${ingredients}</div>`}${photo||'<div></div>'}</div></section>`;
}).join('')||'<div class="prep-empty">В заказе нет боксов для заготовок.</div>';
dialog.innerHTML=`<div class="prep-doc"><div class="prep-doc-head"><div class="prep-doc-brand"><img src="${window.CateriumBranding.logoHTML()}" alt="${window.CateriumBranding.nameHTML()}"><div><b>${window.CateriumBranding.nameHTML()}</b><small>Производственная заготовка заказа</small></div></div><div class="prep-doc-title"><h2>Заготовки${d.id?` · заказ № ${esc(d.id)}`:''}</h2><p>${esc(eventName)}${orderDate?` · ${esc(orderDate)}`:''}${orderTime?` · ${esc(orderTime)}`:''}</p></div></div><section class="prep-doc-section"><h3>${photoOnly?'Боксы в заказе':'Заготовки по боксам'}</h3><div class="prep-box-groups">${groups}</div></section><div class="prep-doc-actions"><button class="primary" type="button" onclick="window.print()">Печать / PDF</button><button class="outline" type="button" onclick="closeModal('preview')">Закрыть</button></div></div>`;
window.modal?.('preview');
};
/* 6) Calendar print: clone only the calendar sheet and force one landscape A4 page. */
function calendarPrint(){
const cal=$('calendar');if(!cal)return;
const period=cal.querySelector('.cal-period-title')?.textContent?.trim()||'';
const countMetric=cal.querySelector('.cal-metric:first-child')?.outerHTML||'';
const metrics=countMetric?`<div class="cal-metrics cal-metrics-kitchen">${countMetric}</div>`:'';
const source=cal.querySelector('.cal-scroll')||cal.querySelector('.cal-agenda')||cal.querySelector('.cal-empty');
const clone=source?source.cloneNode(true):null;
const overflowDays=[];
if(clone){
clone.querySelectorAll('.amount').forEach(el=>el.remove());
clone.querySelectorAll('.cal-event').forEach(ev=>{
const smalls=[...ev.querySelectorAll('small')],finance=smalls[1];if(!finance)return;
const text=String(finance.textContent||'').trim();
const status=['Новый','В работе','Собран','Передан курьеру','Отдан заказчику','Отменён'].find(x=>text.endsWith(x));
if(status&&!document.body.classList.contains('sun-status-disabled'))finance.textContent=status;else finance.remove();
});
clone.querySelectorAll('.cal-day').forEach(day=>{
const extra=[...day.querySelectorAll('.cal-event-overflow')];if(!extra.length)return;
const date=new Date(day.dataset.date+'T00:00:00').toLocaleDateString('ru-RU',{day:'numeric',month:'long',year:'numeric'});
const rows=extra.map(event=>{event.classList.remove('cal-event-overflow');event.removeAttribute('onclick');event.remove();return event.outerHTML}).join('');
overflowDays.push(`<section class="cal-overflow-day"><h3>${esc(date)}</h3>${rows}</section>`);
const count=day.querySelector('.cal-count');if(count)count.textContent+=' · доп. лист';
day.querySelector('.cal-more')?.remove();
});
clone.querySelectorAll('[onclick]').forEach(el=>el.removeAttribute('onclick'));
}
const content=(clone?.outerHTML||'<div class="cal-empty">Нет данных</div>')+(overflowDays.length?`<section class="cal-print-overflow"><h2>Продолжение календаря · ${esc(period)}</h2><p>Заказы, следующие после девяти событий в ячейке дня.</p>${overflowDays.join('')}</section>`:'');
const w=window.open('','_blank','width=1200,height=850');if(!w){try{window.SunEnterprise?.toast?.('Браузер заблокировал окно печати. Разрешите всплывающие окна для приложения.','warn')}catch(_){}return}
const html=`<!doctype html><html lang="ru"><head><meta charset="utf-8"><title>Календарь заказов · ${esc(period)}</title><style>
*{box-sizing:border-box}html,body{margin:0;padding:0;background:#fff;color:#203040;font-family:Arial,sans-serif;-webkit-print-color-adjust:exact;print-color-adjust:exact}body{padding:6mm}.print-head{display:flex;align-items:flex-end;justify-content:space-between;gap:10mm;border-bottom:1px solid #bfc5ca;padding-bottom:2mm;margin-bottom:2.5mm}.print-head h1{margin:0;font-size:16pt;color:#15364c}.print-head b{font-size:10pt}.cal-metrics{display:grid;grid-template-columns:1fr;max-width:48mm;gap:2mm;margin:0 0 2.5mm}.cal-metric{border:1px solid #d9dde0;border-radius:2mm;padding:2mm;background:#fafafa}.cal-metric small{display:block;color:#68717a;font-size:7pt;margin-bottom:.6mm}.cal-metric b{font-size:10pt;color:#15364c}.cal-scroll{overflow:visible}.cal-month{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));grid-template-rows:7mm repeat(6,25mm);border:1px solid #cfd3d8;width:100%;overflow:hidden}.cal-weekday{padding:1.5mm;text-align:center;font-weight:700;color:#68717a;background:#f2f3f5;border-right:1px solid #d9dde1;border-bottom:1px solid #d9dde1;font-size:8pt}.cal-day{min-width:0;height:25mm;padding:1.2mm;border-right:1px solid #d9dde1;border-bottom:1px solid #d9dde1;overflow:hidden;background:#fff}.cal-day.outside{background:#fafafa;color:#999}.cal-day.today{box-shadow:inset 0 0 0 .6mm #c99a32}.cal-date{display:flex;justify-content:space-between;font-weight:700;margin-bottom:.5mm;font-size:7pt}.cal-count{font-size:5.5pt;background:#eef1f2;border-radius:9px;padding:.2mm 1mm}.cal-event{display:block;width:100%;border:0;border-left:1mm solid #c9d0d8;border-radius:1mm;background:#f7f8f8;padding:.7mm 1mm;margin:.5mm 0;text-align:left;color:#203040;overflow:hidden}.cal-event b{display:block;font-size:6.2pt;line-height:1.05;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.cal-event small{display:block;color:#68717a;font-size:5.4pt;line-height:1.05;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.cal-more{font-size:5.5pt;color:#68717a}.cal-agenda{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:2mm}.cal-agenda-day{border:1px solid #d9dde0;border-radius:2mm;overflow:hidden;break-inside:avoid}.cal-agenda-date{padding:1.5mm 2mm;background:#fafafa;border-bottom:1px solid #d9dde0;font-weight:700;color:#15364c;font-size:8pt}.cal-agenda-order{display:grid;grid-template-columns:16mm minmax(0,1fr);gap:1.5mm;align-items:center;padding:1.5mm 2mm;border-bottom:1px solid #e7e9ea;font-size:7pt;break-inside:avoid}.cal-agenda-order:last-child{border-bottom:0}.cal-agenda-order .time{font-weight:700;color:#15364c}.cal-agenda-order small{display:block;color:#68717a;font-size:6pt;margin-top:.5mm}.cal-agenda-order .amount{text-align:right;font-weight:700}.cal-agenda-order>.outline{display:none}.cal-status{display:none}.cal-empty{padding:10mm;text-align:center;color:#879198;border:1px dashed #d9dde0}.cal-print-btn,#calPrev,#calToday,#calNext,.cal-mode{display:none!important}@page{size:A4 landscape;margin:6mm}@media print{body{padding:0}.print-head{margin-top:0}}
.cal-day--compact .cal-event{display:flex;align-items:center;gap:.6mm;height:2.1mm;padding:0 .6mm;margin:0;border-radius:.5mm;white-space:nowrap}
.cal-day--compact .cal-event b{flex:none;font-size:6pt;line-height:2.1mm}
.cal-day--compact .cal-event-name{flex:1;min-width:0;font-size:5.8pt;line-height:2.1mm;margin:0}
.cal-day--compact .cal-event-finance{display:none}
.cal-day:not(.cal-day--compact) .cal-event{padding:.2mm 1mm;margin:.1mm 0}
.cal-day:not(.cal-day--compact) .cal-event b{font-size:6pt;line-height:2.1mm}
.cal-day:not(.cal-day--compact) .cal-event small{line-height:1.8mm}
.cal-print-overflow{break-before:page;padding-top:2mm}.cal-print-overflow h2{font-size:14pt;margin:0 0 3mm}.cal-print-overflow p{font-size:9pt;color:#68717a}
.cal-overflow-day{break-inside:avoid;margin-bottom:5mm}.cal-overflow-day h3{font-size:11pt;margin:3mm 0}
.cal-overflow-day .cal-event{padding:2mm;margin:1mm 0}.cal-overflow-day .cal-event b{font-size:9pt;line-height:1.3}.cal-overflow-day .cal-event small{font-size:9pt;line-height:1.3;white-space:normal}
</style></head><body><div class="print-head"><h1>Календарь заказов</h1><b>${esc(period)}</b></div>${metrics}${content}<script>window.addEventListener('load',()=>setTimeout(()=>{window.focus();window.print()},120));window.addEventListener('afterprint',()=>setTimeout(()=>window.close(),80));<\/script></body></html>`;
w.document.open();w.document.write(html);w.document.close();
}
document.addEventListener('click',e=>{const b=e.target.closest('#calPrint');if(!b)return;e.preventDefault();e.stopImmediatePropagation();calendarPrint();},true);
/* Initial cleanup/sync. */
syncHiddenOrderMapSetting();fillCorpus();injectPrintOptions();injectStatusSettings();applyStatusFeature();
})();
</script>
</body></html>
<!-- SUN CATALOG PDF OPEN/SAVE/PRINT FIX v1 -->
<style id="sun-catalog-pdf-open-style-v1">
#sunCatalogPdfButton.sun-catalog-pdf-open-btn{display:inline-flex;align-items:center;gap:6px}
</style>
<script id="sun-catalog-pdf-open-script-v1">
(()=>{
'use strict';
if(window.__sunCatalogPdfOpenV1)return;
window.__sunCatalogPdfOpenV1=true;
const PDF_PATH='catalog/solnce-catalog-one-item-per-page.pdf';
function currentCatalogCategory(){
const buttons=[...document.querySelectorAll('#new .sun-catalog-top-toolbar .cats button,#new .cats button')];
const active=buttons.find(b=>b.style.outline&&b.style.outline!=='none')||buttons.find(b=>b.getAttribute('aria-current')==='true');
if(active?.dataset?.category!=null)return Number(active.dataset.category);
const title=document.querySelector('#new .catalog h1')?.textContent?.trim()||'';
const map={'Боксы':0,'Премиум':5,'Посуда':1,'Дополнения':2,'Напитки':3,'Доставка':4};
const found=Object.entries(map).find(([name])=>title.startsWith(name));
return found?Number(found[1]):0;
}
function openCatalogPdf(event){
event?.preventDefault?.();
event?.stopPropagation?.();
const win=window.open(PDF_PATH,'_blank');
if(!win){
const a=document.createElement('a');
a.href=PDF_PATH;
a.target='_blank';
a.rel='noopener';
document.body.appendChild(a);
a.click();
a.remove();
}
}
function install(){
const head=document.querySelector('#new .catalog .catalog-head');
const configure=[...document.querySelectorAll('#new .catalog .catalog-head button')].find(b=>b.textContent.trim()==='Настроить');
if(!head||!configure)return;
let old=document.getElementById('sunCatalogPdfButton');
if(old && old.dataset.sunPdfOpenBound!=='1'){
old.onclick=null;
const replacement=document.createElement('button');
replacement.id='sunCatalogPdfButton';
replacement.type='button';
replacement.className=(old.className||'outline')+' sun-catalog-pdf-open-btn';
replacement.textContent='Скачать PDF';
replacement.title='Открыть готовый PDF-каталог. В просмотрщике PDF можно сохранить файл или распечатать его.';
replacement.dataset.sunPdfOpenBound='1';
replacement.addEventListener('click',openCatalogPdf);
old.replaceWith(replacement);
old=replacement;
}else if(!old){
old=document.createElement('button');
old.id='sunCatalogPdfButton';
old.type='button';
old.className='outline sun-catalog-pdf-open-btn';
old.textContent='Скачать PDF';
old.title='Открыть готовый PDF-каталог. В просмотрщике PDF можно сохранить файл или распечатать его.';
old.dataset.sunPdfOpenBound='1';
old.addEventListener('click',openCatalogPdf);
configure.insertAdjacentElement('beforebegin',old);
}
const cat=currentCatalogCategory();
old.hidden=cat===4;
old.style.display=cat===4?'none':'';
}
install();
setTimeout(install,100);
setTimeout(install,600);
document.addEventListener('click',event=>{
if(event.target.closest('#new .cats button,#new .sun-catalog-top-toolbar .cats button'))setTimeout(install,0);
},true);
const target=document.querySelector('#new .catalog');
if(target&&'MutationObserver' in window)new MutationObserver(()=>install()).observe(target,{childList:true,subtree:true});
})();
</script>
<!-- SUN MAILINGS + EVENT TYPES + PDF VIEWER STABILITY v1 -->
<style id="sun-mailings-eventtypes-pdf-style-v1">
/* Event selector */
#event.sun-event-select{width:100%;min-width:0}
.sun-event-types-list{display:grid;gap:7px;margin-top:10px}
.sun-event-type-row{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:7px;align-items:center}
.sun-event-type-row input{min-width:0}
/* Order communication channel */
.sun-order-contact-channel{min-width:0}
/* Mailings */
.sun-enterprise-sidebar header nav .nav-mailings{--sun-icon:url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%221.9%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Crect%20x%3D%223%22%20y%3D%225%22%20width%3D%2218%22%20height%3D%2214%22%20rx%3D%223%22%2F%3E%3Cpath%20d%3D%22m4.5%207%207.5%206%207.5-6%22%2F%3E%3C%2Fsvg%3E")}
#mailings{max-width:1500px;margin:0 auto}
.sun-mailing-head{display:flex;justify-content:space-between;gap:16px;align-items:flex-start;margin-bottom:16px}
.sun-mailing-head h1{margin-bottom:4px}
.sun-mailing-note{padding:11px 13px;border:1px solid #e7ddbd;background:#fffaf0;border-radius:12px;color:#75633e;font-size:12.5px;max-width:560px}
.sun-mailing-grid{display:grid;grid-template-columns:minmax(0,1.25fr) minmax(330px,.75fr);gap:14px;align-items:start}
.sun-mailing-card{background:#fff;border:1px solid #e0e2e8;border-radius:14px;padding:16px;box-shadow:0 2px 10px rgba(35,29,73,.05)}
.sun-mailing-card.wide{grid-column:1/-1}
.sun-mailing-card h2{margin:0 0 12px;font-size:17px}
.sun-mailing-form{display:grid;grid-template-columns:1fr 1fr;gap:10px}
.sun-mailing-form .wide{grid-column:1/-1}
.sun-mailing-form textarea{min-height:140px;resize:vertical}
.sun-mailing-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:12px}
.sun-mailing-vars{display:flex;gap:6px;flex-wrap:wrap;margin-top:7px}
.sun-mailing-vars button{min-height:28px;padding:4px 8px;font-size:11px}
.sun-mailing-audience{display:grid;grid-template-columns:repeat(3,minmax(90px,1fr));gap:7px;margin-top:12px}
.sun-mailing-metric{padding:10px;background:#f7f7fb;border:1px solid #e7e7ee;border-radius:10px}
.sun-mailing-metric small{display:block;color:#7b7f8c;font-size:10px;margin-bottom:4px}
.sun-mailing-metric b{font-size:17px;color:#30284f}
.sun-mailing-template-list,.sun-mailing-history{display:grid;gap:7px}
.sun-mailing-template-row,.sun-mailing-history-row{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:8px;align-items:center;padding:9px 10px;border:1px solid #e4e5eb;border-radius:10px;background:#fafafe}
.sun-mailing-template-row b,.sun-mailing-history-row b{display:block;font-size:12.5px}
.sun-mailing-template-row small,.sun-mailing-history-row small{display:block;color:#7e8390;margin-top:2px}
.sun-mailing-row-actions{display:flex;gap:5px;flex-wrap:wrap;justify-content:flex-end}
.sun-mailing-row-actions button{min-height:30px;padding:4px 8px;font-size:11px}
.sun-client-comms-panel{margin:14px 0;padding:14px;border:1px solid #dedfea;border-radius:12px;background:#fbfbfe}
.sun-client-comms-panel h3{margin:0 0 4px;font-size:15px}
.sun-client-comms-panel>.hint{margin:0 0 11px}
.sun-client-comms-grid{display:grid;grid-template-columns:1fr 1fr;gap:9px}
.sun-client-comms-grid .wide{grid-column:1/-1}
.sun-client-comms-consent{display:flex!important;align-items:center;gap:8px!important;background:#f4fbf6;border:1px solid #dceee1;border-radius:9px;padding:9px 10px!important}
.sun-client-comms-consent input{width:auto!important;margin:0!important}
.sun-mail-queue{max-height:62vh;overflow:auto;display:grid;gap:7px}
.sun-mail-queue-row{display:grid;grid-template-columns:1.1fr 100px minmax(180px,1fr) auto;gap:8px;align-items:center;padding:9px;border:1px solid #e2e4ea;border-radius:10px}
.sun-mail-queue-row small{color:#7b808b}
.sun-mail-channel{display:inline-flex;align-items:center;justify-content:center;min-height:26px;padding:3px 7px;border-radius:999px;background:#f1effa;color:#55458a;font-size:10.5px;font-weight:800}
.sun-mail-channel.whatsapp{background:#eef9f1;color:#29713d}.sun-mail-channel.telegram{background:#eef6fc;color:#27709f}.sun-mail-channel.sms{background:#fff6e8;color:#8c621d}.sun-mail-channel.email{background:#f2f0ff;color:#5d4aa8}.sun-mail-channel.max{background:#eef5ff;color:#245ea6}.sun-mail-channel.phone{background:#f5f5f5;color:#666}.sun-mail-channel.none{background:#fff0f0;color:#9a4040}
.sun-mailing-channel-settings{grid-column:1/-1;border:1px solid #e2e4ea;border-radius:10px;padding:10px;background:#fafafe}.sun-mailing-channel-settings>span{display:block;font-size:11px;font-weight:800;margin-bottom:7px;color:#555b68}.sun-mailing-channel-options{display:flex;gap:7px;flex-wrap:wrap}.sun-mailing-channel-options label{display:inline-flex;flex-direction:row;align-items:center;gap:6px;padding:7px 9px;border:1px solid #dddfea;border-radius:9px;background:#fff;font-size:11.5px}.sun-mailing-channel-options input{width:auto!important;margin:0!important}
/* Stable PDF catalog link. Keep the legacy button only as an invisible sink for old handlers. */
#sunCatalogPdfButton.sun-pdf-legacy-dummy{display:none!important;visibility:hidden!important;pointer-events:none!important;position:absolute!important;left:-99999px!important;width:1px!important;height:1px!important;overflow:hidden!important}
#sunCatalogPdfViewerLink{display:inline-flex;align-items:center;justify-content:center;gap:6px;text-decoration:none}
@media(max-width:900px){
.sun-mailing-head{display:block}.sun-mailing-note{margin-top:10px;max-width:none}
.sun-mailing-grid{grid-template-columns:1fr}.sun-mailing-card.wide{grid-column:auto}
.sun-mailing-form,.sun-client-comms-grid{grid-template-columns:1fr}.sun-mailing-form .wide,.sun-client-comms-grid .wide{grid-column:auto}
.sun-mailing-audience{grid-template-columns:1fr 1fr}
.sun-mail-queue-row{grid-template-columns:1fr auto}.sun-mail-queue-row>div:nth-child(3){grid-column:1/-1}
}
</style>
<script id="sun-mailings-eventtypes-pdf-script-v1">
(()=>{
'use strict';
if(window.__sunMailingsEventTypesPdfV1)return;
window.__sunMailingsEventTypesPdfV1=true;
const $=id=>document.getElementById(id), qa=(sel,root=document)=>[...root.querySelectorAll(sel)];
const esc=window.SunSafe.escapeHTML;
const readJson=(key,fallback)=>{try{const v=JSON.parse(localStorage.getItem(key)||'null');return v??fallback}catch(_){return fallback}};
const writeJson=(key,value)=>{try{localStorage.setItem(key,JSON.stringify(value));return true}catch(_){return false}};
const getOrders=()=>{try{return Array.isArray(orders)?orders:[]}catch(_){return[]}};
const getDraft=()=>{try{return draft}catch(_){return null}};
const toast=(msg,type='success')=>{try{window.SunEnterprise?.toast?.(msg,type)}catch(_){}};
const today=()=>new Date().toISOString().slice(0,10);
/* ---------- Event types ---------- */
const EVENT_TYPES_KEY='sunEventTypesV1';
const DEFAULT_EVENT_TYPES=['Фуршет','Доставка боксов','Кейтеринг','Банкет'];
const eventTypes=()=>{
const list=readJson(EVENT_TYPES_KEY,DEFAULT_EVENT_TYPES);
const clean=(Array.isArray(list)?list:DEFAULT_EVENT_TYPES).map(x=>String(x||'').trim()).filter(Boolean);
return [...new Set(clean.length?clean:DEFAULT_EVENT_TYPES)];
};
function installEventSelect(preferred){
let control=$('event');if(!control)return null;
const value=preferred!==undefined?String(preferred||''):String(control.value||'');
if(control.tagName!=='SELECT'){
const select=document.createElement('select');select.id='event';select.className=(control.className||'')+' sun-event-select';select.setAttribute('aria-label','Мероприятие');
control.replaceWith(select);control=select;
}
const options=['',...eventTypes()];
if(value && !options.includes(value))options.push(value);
control.innerHTML=options.map(v=>`<option value="${esc(v)}">${v?esc(v):'— Не указывать —'}</option>`).join('');
control.value=value&&options.includes(value)?value:'';
return control;
}
installEventSelect($('event')?.value||'');
function injectEventTypeSettings(){
const view=$('enterprise-settings'),grid=view?.querySelector('.enterprise-grid');if(!grid)return;
let card=$('sunEventTypesSettingsCard');
if(!card){
card=document.createElement('section');card.id='sunEventTypesSettingsCard';card.className='enterprise-card wide';
card.innerHTML=`<h2>Типы мероприятий</h2><p class="hint">Этот список появляется стрелкой в поле «Мероприятие» при оформлении заказа.</p><div class="sun-event-types-list" id="sunEventTypesRows"></div><div class="actions"><button class="outline" id="sunEventTypeAdd" type="button">Добавить тип</button><button class="primary" id="sunEventTypesSave" type="button">Сохранить список</button></div>`;
window.SunSafe.insertBefore(grid,card,grid.lastElementChild||null);
$('sunEventTypeAdd').onclick=()=>{const rows=$('sunEventTypesRows');const row=document.createElement('div');row.className='sun-event-type-row';row.innerHTML=`<input class="sun-event-type-input" value=""><button class="outline" type="button">Убрать</button>`;row.querySelector('button').onclick=()=>row.remove();rows.appendChild(row);row.querySelector('input').focus();};
$('sunEventTypesSave').onclick=()=>{
const list=qa('.sun-event-type-input',card).map(i=>i.value.trim()).filter(Boolean);const clean=[...new Set(list)];
if(!clean.length)return toast('Добавьте хотя бы один тип мероприятия.','warn');
writeJson(EVENT_TYPES_KEY,clean);const cur=$('event')?.value||'';installEventSelect(cur);renderEventRows();toast('Список мероприятий сохранён.');
};
}
renderEventRows();
function renderEventRows(){
const rows=$('sunEventTypesRows');if(!rows)return;rows.innerHTML=eventTypes().map(v=>`<div class="sun-event-type-row"><input class="sun-event-type-input" value="${esc(v)}"><button class="outline" type="button">Убрать</button></div>`).join('');qa('button',rows).forEach(b=>b.onclick=()=>b.closest('.sun-event-type-row')?.remove());
}
}
document.addEventListener('click',e=>{const b=e.target.closest('header nav button');if(b&&String(b.dataset.navLabel||b.textContent||'').trim()==='Настройки')setTimeout(injectEventTypeSettings,20)},true);
if($('enterprise-settings')?.classList.contains('on'))injectEventTypeSettings();
const priorOpen=window.openOrder;if(typeof priorOpen==='function')window.openOrder=function(...args){const r=priorOpen.apply(this,args);setTimeout(()=>installEventSelect(getDraft()?.event||$('event')?.value||''),0);return r};
const priorReset=window.resetDraft;if(typeof priorReset==='function')window.resetDraft=function(...args){const r=priorReset.apply(this,args);setTimeout(()=>installEventSelect(''),0);return r};
const priorSave=window.saveOrder;if(typeof priorSave==='function')window.saveOrder=function(...args){const d=getDraft(),ev=$('event');if(d&&ev)d.event=ev.value;const r=priorSave.apply(this,args);return r};
/* ---------- Communication channel in order + client card ---------- */
const CLIENT_COMM_KEY='sunClientCommunicationV1';
function phoneDigits(value){return String(value||'').replace(/\D/g,'')}
function clientKeyFromOrder(order){const p=phoneDigits(order?.phone);if(p)return`p:${p}`;const n=String(order?.contact||'').trim().toLowerCase();return n?`n:${n}`:''}
const readCommStore=()=>readJson(CLIENT_COMM_KEY,{});
const saveCommStore=store=>writeJson(CLIENT_COMM_KEY,store||{});
function clientSnapshot(key){
const list=getOrders().filter(o=>clientKeyFromOrder(o)===key).sort((a,b)=>`${b.date||''}T${b.time||''}`.localeCompare(`${a.date||''}T${a.time||''}`));
const latest=list[0]||{};return{orders:list,latest,name:String(latest.contact||'Без имени'),phone:String(latest.phone||''),lastChannel:String(latest.contactChannel||'')};
}
function installOrderContactChannel(){
const details=$('details'),grid=details?.querySelector('.sun-delivery-details');if(!grid||$('orderContactChannel'))return;
const phone=$('phone')?.closest('label');const label=document.createElement('label');label.className='sun-order-contact-channel';label.innerHTML=`Канал связи<select id="orderContactChannel"><option value="">Не указан</option><option value="whatsapp">WhatsApp</option><option value="max">MAX</option><option value="telegram">Telegram</option><option value="email">E-mail</option><option value="sms">SMS</option><option value="phone">Телефон</option><option value="other">Другое</option></select>`;
if(phone)phone.insertAdjacentElement('afterend',label);else window.SunSafe.insertBefore(grid,label,grid.firstChild);
label.querySelector('select').onchange=()=>{const d=getDraft();if(d)d.contactChannel=label.querySelector('select').value};
}
installOrderContactChannel();
function fillOrderContactChannel(){installOrderContactChannel();const d=getDraft();if($('orderContactChannel'))$('orderContactChannel').value=String(d?.contactChannel||'')}
const openAfterChannel=window.openOrder;if(typeof openAfterChannel==='function')window.openOrder=function(...args){const r=openAfterChannel.apply(this,args);setTimeout(fillOrderContactChannel,0);return r};
const resetAfterChannel=window.resetDraft;if(typeof resetAfterChannel==='function')window.resetDraft=function(...args){const r=resetAfterChannel.apply(this,args);setTimeout(()=>{if($('orderContactChannel'))$('orderContactChannel').value='';const d=getDraft();if(d)d.contactChannel=''},0);return r};
const saveAfterChannel=window.saveOrder;if(typeof saveAfterChannel==='function')window.saveOrder=function(...args){const d=getDraft();if(d)d.contactChannel=$('orderContactChannel')?.value||'';const r=saveAfterChannel.apply(this,args);const sync=()=>{const cur=getDraft();if(!cur)return;const key=clientKeyFromOrder(cur);if(!key)return;const store=readCommStore(),old=store[key]||{};store[key]={...old,lastChannel:String(cur.contactChannel||old.lastChannel||''),updatedAt:new Date().toISOString()};saveCommStore(store)};if(r&&typeof r.then==='function')return r.then(v=>{sync();return v});sync();return r};
const priorClientCard=window.openClientCard;
function injectClientCommunications(encodedKey){
const key=decodeURIComponent(encodedKey),host=$('client-card-content');if(!host)return;host.querySelector('#sunClientCommsPanel')?.remove();
const snap=clientSnapshot(key),store=readCommStore(),saved=store[key]||{};
const preferred=String(saved.preferred||'auto'),wa=String(saved.whatsapp||snap.phone||''),sms=String(saved.sms||snap.phone||''),telegram=String(saved.telegram||''),email=String(saved.email||''),max=String(saved.max||''),consent=Boolean(saved.marketingConsent);
const panel=document.createElement('section');panel.id='sunClientCommsPanel';panel.className='sun-client-comms-panel';
panel.innerHTML=`<h3>Связь и рассылки</h3><p class="hint">Укажите контакты и предпочтительный канал. Для одной кампании клиент получает одно сообщение: сначала по предпочтительному доступному каналу, затем по следующему разрешённому.</p><div class="sun-client-comms-grid"><label>Предпочтительный канал<select id="clientPreferredChannel"><option value="auto" ${preferred==='auto'?'selected':''}>Авто · по последнему заказу</option><option value="whatsapp" ${preferred==='whatsapp'?'selected':''}>WhatsApp</option><option value="max" ${preferred==='max'?'selected':''}>MAX</option><option value="telegram" ${preferred==='telegram'?'selected':''}>Telegram</option><option value="email" ${preferred==='email'?'selected':''}>E-mail</option><option value="sms" ${preferred==='sms'?'selected':''}>SMS</option><option value="none" ${preferred==='none'?'selected':''}>Не отправлять</option></select></label><label>WhatsApp<input id="clientWhatsApp" value="${esc(wa)}" placeholder="+7 ..."></label><label>SMS<input id="clientSms" value="${esc(sms)}" placeholder="+7 ..."></label><label>E-mail<input id="clientEmail" type="email" value="${esc(email)}" placeholder="name@example.com"></label><label>MAX<input id="clientMax" value="${esc(max)}" placeholder="username, ID или телефон"></label><label class="wide">Telegram username / chat ID<input id="clientTelegram" value="${esc(telegram)}" placeholder="@username или chat ID"></label><label class="wide sun-client-comms-consent"><input id="clientMarketingConsent" type="checkbox" ${consent?'checked':''}> Клиент согласен получать акции и предложения</label></div><div class="actions"><button class="primary" id="clientCommsSave" type="button">Сохранить связь</button></div>`;
const detail=host.querySelector('.client-detail-grid');if(detail)detail.insertAdjacentElement('beforebegin',panel);else host.appendChild(panel);
$('clientCommsSave').onclick=()=>{const all=readCommStore();all[key]={...(all[key]||{}),preferred:$('clientPreferredChannel').value,whatsapp:$('clientWhatsApp').value.trim(),sms:$('clientSms').value.trim(),telegram:$('clientTelegram').value.trim(),email:$('clientEmail').value.trim(),max:$('clientMax').value.trim(),marketingConsent:$('clientMarketingConsent').checked,lastChannel:String(all[key]?.lastChannel||snap.lastChannel||''),updatedAt:new Date().toISOString()};saveCommStore(all);toast('Каналы связи клиента сохранены.');};
}
if(typeof priorClientCard==='function')window.openClientCard=function(encodedKey,...args){const r=priorClientCard.call(this,encodedKey,...args);setTimeout(()=>injectClientCommunications(encodedKey),0);return r};
/* ---------- Mailings center ---------- */
const TEMPLATE_KEY='sunMarketingTemplatesV1', CAMPAIGN_KEY='sunMarketingCampaignsV1', PROMO_KEY='sunPromoCodesV1';
const defaultTemplates=[{id:'sep1',name:'1 сентября · скидка 10%',text:("{{Имя}}, к 1 сентября для вас скидка {{Скидка}} на заказ в «"+window.CateriumBranding.documentName()+"». Промокод: {{Промокод}}. Будем рады приготовить ваш праздник!")},{id:'return',name:'Вернуть клиента',text:("{{Имя}}, давно вас не видели! Для вас действует персональное предложение от «"+window.CateriumBranding.documentName()+"». Напишите нам, и подберём формат под ваш праздник.")}];
function templates(){const list=readJson(TEMPLATE_KEY,defaultTemplates),src=Array.isArray(list)&&list.length?list:defaultTemplates;let changed=false;const migrated=src.map(t=>{const text=String(t.text||'').replaceAll('{{Скидка}}%','{{Скидка}}');if(text!==t.text)changed=true;return {...t,text}});if(changed)saveTemplates(migrated);return migrated}
function saveTemplates(list){writeJson(TEMPLATE_KEY,list)}
function campaigns(){const list=readJson(CAMPAIGN_KEY,[]);return Array.isArray(list)?list:[]}
function saveCampaigns(list){writeJson(CAMPAIGN_KEY,list)}
function customerList(){
const map={};
getOrders().forEach(o=>{const key=clientKeyFromOrder(o);if(!key)return;if(!map[key])map[key]={key,name:String(o.contact||'Без имени')||'Без имени',phone:String(o.phone||''),orders:[],total:0,lastDate:'',lastChannel:''};const p=map[key];p.orders.push(o);p.name=String(o.contact||p.name);p.phone=String(o.phone||p.phone);if(String(o.status||'')!=='Отменён')p.total+=Number(typeof orderTotalValue==='function'?orderTotalValue(o):o.total||0);const stamp=`${o.date||''}T${o.time||''}`;if(stamp>=p.lastDate){p.lastDate=stamp;p.lastChannel=String(o.contactChannel||p.lastChannel||'')}});
return Object.values(map);
}
function daysSince(iso){if(!iso)return 99999;const d=new Date(String(iso).slice(0,10)+'T12:00:00');return Number.isNaN(d.getTime())?99999:Math.floor((Date.now()-d.getTime())/86400000)}
function segmentClients(segment){return customerList().filter(p=>{if(segment==='repeat')return p.orders.length>=2;if(segment==='inactive60')return daysSince(p.lastDate)>=60;if(segment==='inactive90')return daysSince(p.lastDate)>=90;if(segment==='vip100')return p.total>=100000;return true})}
const MAIL_CHANNELS=['whatsapp','max','telegram','email','sms'];
const MAIL_CHANNEL_DEFAULTS_KEY='sunMarketingChannelDefaultsV1';
function contactFor(channel,person,pref){if(channel==='whatsapp')return pref.whatsapp||person.phone||'';if(channel==='sms')return pref.sms||person.phone||'';if(channel==='telegram')return pref.telegram||'';if(channel==='email')return pref.email||'';if(channel==='max')return pref.max||'';return''}
function channelFor(person,pref,allowedChannels=MAIL_CHANNELS){const allowed=new Set((allowedChannels||[]).filter(ch=>MAIL_CHANNELS.includes(ch))),raw=String(pref.preferred||'auto');if(raw==='none'||!allowed.size)return'none';const candidates=[];if(raw!=='auto')candidates.push(raw);else candidates.push(String(pref.lastChannel||person.lastChannel||''));candidates.push('whatsapp','max','telegram','email','sms');for(const ch of candidates){if(allowed.has(ch)&&contactFor(ch,person,pref))return ch}return'none'}
function messageFor(person,text,discount,promo){return String(text||'').replaceAll('{{Имя}}',person.name||'').replaceAll('{{Скидка}}',String(discount||'')).replaceAll('{{Промокод}}',String(promo||''))}
function makeQueue(segment,text,discount,promo,allowedChannels=MAIL_CHANNELS){
const store=readCommStore();return segmentClients(segment).filter(p=>Boolean(store[p.key]?.marketingConsent)).map(p=>{const pref=store[p.key]||{},channel=channelFor(p,pref,allowedChannels),contact=contactFor(channel,p,pref);return{key:p.key,name:p.name,channel,contact,message:messageFor(p,text,discount,promo)}});
}
function queueStats(queue){const c={whatsapp:0,max:0,telegram:0,email:0,sms:0,none:0,total:queue.length};queue.forEach(r=>{c[r.channel]!==undefined?c[r.channel]++:c.none++});return c}
function channelLabel(ch){return({whatsapp:'WhatsApp',max:'MAX',telegram:'Telegram',email:'E-mail',sms:'SMS',none:'Без канала'})[ch]||'Без канала'}
function quickLink(row){const digits=phoneDigits(row.contact);if(row.channel==='whatsapp'&&digits)return`https://wa.me/${digits}?text=${encodeURIComponent(row.message)}`;if(row.channel==='sms'&&row.contact)return`sms:${row.contact}?body=${encodeURIComponent(row.message)}`;if(row.channel==='email'&&row.contact)return`mailto:${encodeURIComponent(row.contact)}?subject=${encodeURIComponent((window.CateriumBranding.documentName()))}&body=${encodeURIComponent(row.message)}`;if(row.channel==='telegram'){const u=String(row.contact||'').trim().replace(/^@/,'');if(u&&!/^\d+$/.test(u))return`https://t.me/${encodeURIComponent(u)}`;}return''}
const nav=document.querySelector('header nav');
let mailingButton=$('sunMailingsNav');
if(!mailingButton){mailingButton=document.createElement('button');mailingButton.id='sunMailingsNav';mailingButton.type='button';mailingButton.className='sun-nav-button nav-mailings';mailingButton.dataset.navLabel='Рассылки';mailingButton.textContent='Рассылки';const group=nav?.querySelector('.sun-nav-management')||nav;const clients=qa('button',group||document).find(b=>String(b.dataset.navLabel||b.textContent||'').trim()==='Клиенты');if(clients)clients.insertAdjacentElement('afterend',mailingButton);else group?.appendChild(mailingButton)}
let mailingView=$('mailings');
if(!mailingView){mailingView=document.createElement('section');mailingView.id='mailings';mailingView.className='view page';mailingView.innerHTML=`<div class="sun-mailing-head"><div><h1>Рассылки</h1><p class="hint" style="margin:0">Акции по клиентской базе с выбором разрешённых каналов: WhatsApp, MAX, Telegram, E-mail и SMS. Каждый клиент получает сообщение только по одному доступному каналу.</p></div><div class="sun-mailing-note">Выберите каналы кампании. Приложение подготовит по одному сообщению на клиента; полностью автоматическая отправка потребует подключения API соответствующих каналов.</div></div><div class="sun-mailing-grid"><section class="sun-mailing-card"><h2>Новая кампания</h2><div class="sun-mailing-form"><label class="wide">Название кампании<input id="mailCampaignName" value="1 сентября · скидка 10%"></label><label>Скидка, %<input id="mailDiscount" type="number" min="0" max="100" step="1" value="10"></label><label>Промокод<input id="mailPromo" value="SEPT10"></label><label>Сегмент<select id="mailSegment"><option value="all">Все клиенты с согласием</option><option value="repeat">Постоянные · 2+ заказа</option><option value="inactive60">Не заказывали 60+ дней</option><option value="inactive90">Не заказывали 90+ дней</option><option value="vip100">Оборот от 100 000 ₽</option></select></label><label>Шаблон<select id="mailTemplateSelect"></select></label><label class="wide">Текст сообщения<textarea id="mailMessage"></textarea></label></div><div class="sun-mailing-vars"><button class="outline" type="button" data-mail-var="{{Имя}}">+ Имя</button><button class="outline" type="button" data-mail-var="{{Скидка}}">+ Скидка</button><button class="outline" type="button" data-mail-var="{{Промокод}}">+ Промокод</button></div><div id="mailAudience" class="sun-mailing-audience"></div><div class="sun-mailing-actions"><button class="outline" id="mailPreviewAudience" type="button">Рассчитать аудиторию</button><button class="primary" id="mailLaunch" type="button">Запустить рассылку</button></div></section><section class="sun-mailing-card"><h2>Шаблоны сообщений</h2><div class="sun-mailing-form"><label class="wide">Название шаблона<input id="mailTemplateName" placeholder="Например, 1 сентября"></label><label class="wide">Текст<textarea id="mailTemplateText" rows="5" placeholder="Текст с {{Имя}}, {{Скидка}}, {{Промокод}}"></textarea></label></div><div class="sun-mailing-actions"><button class="primary" id="mailTemplateSave" type="button">Сохранить шаблон</button></div><div id="mailTemplateList" class="sun-mailing-template-list" style="margin-top:12px"></div></section><section class="sun-mailing-card wide"><h2>История кампаний</h2><div id="mailCampaignHistory" class="sun-mailing-history"></div></section></div>`;document.body.appendChild(mailingView)}
let queueModal=$('sunMailQueueModal');
if(!queueModal){queueModal=document.createElement('div');queueModal.id='sunMailQueueModal';queueModal.className='modal';queueModal.innerHTML=`<div class="dialog" style="max-width:1050px"><div class="dialog-head"><h2 id="sunMailQueueTitle">Очередь рассылки</h2><button type="button" onclick="closeModal('sunMailQueueModal')">×</button></div><div id="sunMailQueueContent"></div></div>`;document.body.appendChild(queueModal)}
function promoCodes(){const list=readJson(PROMO_KEY,[]);return Array.isArray(list)?list:[]}
function savePromoCodes(list){writeJson(PROMO_KEY,Array.isArray(list)?list:[]);window.sunRefreshOrderPromos?.()}
const localToday=()=>{const d=new Date(),p=n=>String(n).padStart(2,'0');return `${d.getFullYear()}-${p(d.getMonth()+1)}-${p(d.getDate())}`};
function promoActive(p,date=localToday()){if(!p||p.enabled===false)return false;const start=String(p.startDate||''),end=String(p.endDate||'');return (!start||start<=date)&&(!end||end>=date)}
function promoDiscountText(p){const n=Math.max(0,Number(p?.discountValue||0));return p?.discountType==='amount'?`${n.toLocaleString('ru-RU',{maximumFractionDigits:2})} ₽`:`${n.toLocaleString('ru-RU',{maximumFractionDigits:2})}%`}
function promoState(p){if(p.enabled===false)return'Отключён';const now=localToday();if(p.startDate&&p.startDate>now)return`С ${p.startDate}`;if(p.endDate&&p.endDate<now)return'Истёк';return'Активен'}
function selectedMailPromo(){const id=$('mailPromoSelect')?.value||'';return promoCodes().find(p=>String(p.id)===String(id))||null}
function readMailChannelDefaults(){const saved=readJson(MAIL_CHANNEL_DEFAULTS_KEY,MAIL_CHANNELS);return Array.isArray(saved)?saved.filter(ch=>MAIL_CHANNELS.includes(ch)):MAIL_CHANNELS.slice()}
function selectedMailChannels(){return qa('[data-mail-channel]',mailingView).filter(cb=>cb.checked).map(cb=>cb.dataset.mailChannel).filter(ch=>MAIL_CHANNELS.includes(ch))}
function ensureMailChannelUi(){const form=$('mailCampaignName')?.closest('.sun-mailing-form');if(!form||$('mailChannelSettings'))return;const selected=new Set(readMailChannelDefaults()),box=document.createElement('div');box.id='mailChannelSettings';box.className='sun-mailing-channel-settings wide';box.innerHTML=`<span>Каналы этой рассылки</span><div class="sun-mailing-channel-options">${MAIL_CHANNELS.map(ch=>`<label><input type="checkbox" data-mail-channel="${ch}" ${selected.has(ch)?'checked':''}> ${channelLabel(ch)}</label>`).join('')}</div>`;const segment=$('mailSegment')?.closest('label');if(segment)segment.insertAdjacentElement('beforebegin',box);else form.appendChild(box);qa('[data-mail-channel]',box).forEach(cb=>cb.addEventListener('change',()=>{const list=selectedMailChannels();writeJson(MAIL_CHANNEL_DEFAULTS_KEY,list);renderAudience()}))}
function ensurePromoUi(){
const form=$('mailCampaignName')?.closest('.sun-mailing-form');if(!form)return;
ensureMailChannelUi();
const oldDiscount=$('mailDiscount')?.closest('label'),oldPromo=$('mailPromo')?.closest('label');
if(!$('mailPromoSelect')){
const label=document.createElement('label');label.innerHTML='Промокод<select id="mailPromoSelect"><option value="">Без промокода</option></select>';
(oldDiscount||form.firstElementChild)?.insertAdjacentElement('afterend',label);
}
oldDiscount?.remove();oldPromo?.remove();
let card=$('sunPromoManagerCard');
if(!card){
card=document.createElement('section');card.id='sunPromoManagerCard';card.className='sun-mailing-card wide';
card.innerHTML=`<h2>Промокоды</h2><p class="hint">Создайте код один раз. В заказе можно выбрать только действующий промокод, а его скидка подставится автоматически.</p><div class="sun-mailing-form"><input id="promoEditId" type="hidden"><label>Промокод<input id="promoManagerCode" placeholder="Например, SEPT10"></label><label>Скидка<div style="display:grid;grid-template-columns:minmax(0,1fr) 80px;gap:6px"><input id="promoManagerValue" type="number" min="0" step="1"><select id="promoManagerType"><option value="percent">%</option><option value="amount">₽</option></select></div></label><label>Начало действия<input id="promoManagerStart" type="date"></label><label>Окончание действия<input id="promoManagerEnd" type="date"></label><label class="wide" style="flex-direction:row;align-items:center;gap:8px"><input id="promoManagerEnabled" type="checkbox" checked style="width:auto"> Промокод включён</label></div><div class="sun-mailing-actions"><button class="primary" id="promoManagerSave" type="button">Создать промокод</button><button class="outline" id="promoManagerCancel" type="button" style="display:none">Отмена</button></div><div id="promoManagerList" class="sun-mailing-template-list" style="margin-top:12px"></div>`;
const history=$('mailCampaignHistory')?.closest('.sun-mailing-card');if(history)history.insertAdjacentElement('beforebegin',card);else mailingView.querySelector('.sun-mailing-grid')?.appendChild(card);
$('promoManagerStart').value=localToday();
$('promoManagerSave').onclick=savePromoFromForm;$('promoManagerCancel').onclick=clearPromoForm;
card.addEventListener('click',e=>{const edit=e.target.closest('[data-promo-edit]'),toggle=e.target.closest('[data-promo-toggle]'),del=e.target.closest('[data-promo-delete]');if(edit){editPromo(edit.dataset.promoEdit);return}if(toggle){togglePromo(toggle.dataset.promoToggle);return}if(del){deletePromo(del.dataset.promoDelete);return}});
}
renderPromoControls();
}
function clearPromoForm(){if(!$('promoEditId'))return;$('promoEditId').value='';$('promoManagerCode').value='';$('promoManagerValue').value='';$('promoManagerType').value='percent';$('promoManagerStart').value=localToday();$('promoManagerEnd').value='';$('promoManagerEnabled').checked=true;$('promoManagerSave').textContent='Создать промокод';$('promoManagerCancel').style.display='none'}
function savePromoFromForm(){
const id=$('promoEditId').value.trim(),code=$('promoManagerCode').value.trim().toUpperCase(),value=Math.max(0,Number($('promoManagerValue').value||0)),type=$('promoManagerType').value==='amount'?'amount':'percent',start=$('promoManagerStart').value,end=$('promoManagerEnd').value,enabled=$('promoManagerEnabled').checked;
if(!code)return toast('Введите промокод.','warn');if(!value)return toast('Укажите размер скидки.','warn');if(type==='percent'&&value>100)return toast('Процент скидки не может быть больше 100%.','warn');if(!start||!end)return toast('Укажите начало и окончание действия промокода.','warn');if(end<start)return toast('Дата окончания не может быть раньше даты начала.','warn');
const list=promoCodes(),duplicate=list.find(p=>String(p.code||'').trim().toLowerCase()===code.toLowerCase()&&String(p.id)!==String(id));if(duplicate)return toast('Такой промокод уже существует.','warn');
if(id){const p=list.find(x=>String(x.id)===String(id));if(p)Object.assign(p,{code,discountValue:value,discountType:type,startDate:start,endDate:end,enabled,updatedAt:new Date().toISOString()})}
else list.push({id:`promo-${Date.now()}`,code,discountValue:value,discountType:type,startDate:start,endDate:end,enabled,createdAt:new Date().toISOString(),updatedAt:new Date().toISOString()});
savePromoCodes(list);clearPromoForm();renderPromoControls();toast(id?'Промокод обновлён.':'Промокод создан.');
}
function editPromo(id){const p=promoCodes().find(x=>String(x.id)===String(id));if(!p)return;$('promoEditId').value=p.id;$('promoManagerCode').value=p.code||'';$('promoManagerValue').value=Number(p.discountValue||0);$('promoManagerType').value=p.discountType==='amount'?'amount':'percent';$('promoManagerStart').value=p.startDate||localToday();$('promoManagerEnd').value=p.endDate||'';$('promoManagerEnabled').checked=p.enabled!==false;$('promoManagerSave').textContent='Сохранить изменения';$('promoManagerCancel').style.display='inline-flex';$('promoManagerCode').focus()}
function togglePromo(id){const list=promoCodes(),p=list.find(x=>String(x.id)===String(id));if(!p)return;p.enabled=p.enabled===false; p.updatedAt=new Date().toISOString();savePromoCodes(list);renderPromoControls()}
function deletePromo(id){const p=promoCodes().find(x=>String(x.id)===String(id));if(!p)return;if(!confirm(`Удалить промокод ${p.code}? Старые заказы сохранят уже применённую скидку.`))return;savePromoCodes(promoCodes().filter(x=>String(x.id)!==String(id)));renderPromoControls();clearPromoForm()}
function renderPromoControls(){
const list=promoCodes().slice().sort((a,b)=>String(b.createdAt||'').localeCompare(String(a.createdAt||''))),select=$('mailPromoSelect');
if(select){const current=select.value,active=list.filter(p=>promoActive(p));select.innerHTML='<option value="">Без промокода</option>'+active.map(p=>`<option value="${esc(p.id)}">${esc(p.code)} · ${esc(promoDiscountText(p))} · до ${esc(p.endDate||'—')}</option>`).join('');if(current&&active.some(p=>String(p.id)===String(current)))select.value=current;}
const host=$('promoManagerList');if(host)host.innerHTML=list.length?list.map(p=>`<div class="sun-mailing-template-row"><div><b>${esc(p.code)}</b><small>${esc(promoDiscountText(p))} · ${esc(p.startDate||'—')}${esc(p.endDate||'—')} · ${esc(promoState(p))}</small></div><div class="sun-mailing-row-actions"><button class="outline" type="button" data-promo-edit="${esc(p.id)}">Изменить</button><button class="outline" type="button" data-promo-toggle="${esc(p.id)}">${p.enabled===false?'Включить':'Отключить'}</button><button class="danger" type="button" data-promo-delete="${esc(p.id)}">Удалить</button></div></div>`).join(''):'<p class="hint">Промокодов пока нет.</p>';
}
function renderTemplateControls(){
const list=templates(),select=$('mailTemplateSelect');if(select){const current=select.value;select.innerHTML='<option value="">Без шаблона</option>'+list.map(t=>`<option value="${esc(t.id)}">${esc(t.name)}</option>`).join('');if(current&&list.some(t=>t.id===current))select.value=current;}
const host=$('mailTemplateList');if(host)host.innerHTML=list.map(t=>`<div class="sun-mailing-template-row"><div><b>${esc(t.name)}</b><small>${esc(String(t.text||'').slice(0,95))}${String(t.text||'').length>95?'…':''}</small></div><div class="sun-mailing-row-actions"><button class="outline" type="button" data-mail-template-use="${esc(t.id)}">Использовать</button><button class="outline" type="button" data-mail-template-edit="${esc(t.id)}">Изменить</button><button class="danger" type="button" data-mail-template-delete="${esc(t.id)}">Удалить</button></div></div>`).join('')||'<p class="hint">Шаблонов пока нет.</p>';
}
function currentQueue(){const p=selectedMailPromo(),channels=selectedMailChannels();return makeQueue($('mailSegment')?.value||'all',$('mailMessage')?.value||'',p?promoDiscountText(p):'',p?.code||'',channels)}
function renderAudience(){const q=currentQueue(),s=queueStats(q),host=$('mailAudience');if(host)host.innerHTML=`<div class="sun-mailing-metric"><small>WhatsApp</small><b>${s.whatsapp}</b></div><div class="sun-mailing-metric"><small>MAX</small><b>${s.max}</b></div><div class="sun-mailing-metric"><small>Telegram</small><b>${s.telegram}</b></div><div class="sun-mailing-metric"><small>E-mail</small><b>${s.email}</b></div><div class="sun-mailing-metric"><small>SMS</small><b>${s.sms}</b></div><div class="sun-mailing-metric"><small>Без доступного канала</small><b>${s.none}</b></div>`;return q}
function renderHistory(){const host=$('mailCampaignHistory');if(!host)return;const list=campaigns().slice().reverse();host.innerHTML=list.length?list.map(c=>`<div class="sun-mailing-history-row"><div><b>${esc(c.name||'Кампания')}</b><small>${esc(c.createdAt?.slice(0,16).replace('T',' ')||'')} · ${Number(c.counts?.total||0)} получателей · WhatsApp ${Number(c.counts?.whatsapp||0)} · MAX ${Number(c.counts?.max||0)} · Telegram ${Number(c.counts?.telegram||0)} · E-mail ${Number(c.counts?.email||0)} · SMS ${Number(c.counts?.sms||0)}</small></div><div class="sun-mailing-row-actions"><button class="outline" type="button" data-mail-history-open="${esc(c.id)}">Очередь</button></div></div>`).join(''):'<p class="hint">Запущенных кампаний пока нет.</p>'}
function openQueue(campaign){
if(!campaign)return;const rows=campaign.queue||[];$('sunMailQueueTitle').textContent=`Очередь · ${campaign.name||'Кампания'}`;
$('sunMailQueueContent').innerHTML=`<p class="hint">Для WhatsApp, SMS и E-mail можно открыть подготовленное сообщение. Для Telegram и MAX текст можно скопировать; автоматическая отправка появится после подключения API каналов.</p><div class="sun-mail-queue">${rows.map((r,i)=>{const link=quickLink(r);return`<div class="sun-mail-queue-row"><div><b>${esc(r.name)}</b><small>${esc(r.contact||'Контакт не указан')}</small></div><span class="sun-mail-channel ${esc(r.channel)}">${esc(channelLabel(r.channel))}</span><div><small>${esc(String(r.message||'').slice(0,150))}${String(r.message||'').length>150?'…':''}</small></div><div class="sun-mailing-row-actions"><button class="outline" type="button" data-mail-copy="${i}">Копировать</button>${link?`<a class="outline" style="text-decoration:none;display:inline-flex;align-items:center" href="${esc(link)}" target="_blank" rel="noopener">Открыть</a>`:''}</div></div>`}).join('')||'<p class="hint">Получателей нет.</p>'}</div>`;
qa('[data-mail-copy]',$('sunMailQueueContent')).forEach(btn=>btn.onclick=async()=>{const row=rows[Number(btn.dataset.mailCopy)];try{await navigator.clipboard.writeText(row.message);toast('Текст скопирован.')}catch(_){const ta=document.createElement('textarea');ta.value=row.message;document.body.appendChild(ta);ta.select();document.execCommand('copy');ta.remove();toast('Текст скопирован.')}});window.modal?.('sunMailQueueModal');
}
function bindMailings(){
ensurePromoUi();renderTemplateControls();renderPromoControls();renderHistory();const first=templates()[0];if(!$('mailMessage').value&&first)$('mailMessage').value=first.text;renderAudience();
$('mailTemplateSelect').onchange=()=>{const t=templates().find(x=>x.id===$('mailTemplateSelect').value);if(t)$('mailMessage').value=t.text;renderAudience()};
qa('[data-mail-var]',mailingView).forEach(b=>b.onclick=()=>{const ta=$('mailMessage'),v=b.dataset.mailVar,start=ta.selectionStart??ta.value.length,end=ta.selectionEnd??start;ta.value=ta.value.slice(0,start)+v+ta.value.slice(end);ta.focus();ta.setSelectionRange(start+v.length,start+v.length);renderAudience()});
$('mailPreviewAudience').onclick=()=>{const q=renderAudience();toast(`Аудитория рассчитана: ${q.length} получателей.`)};
$('mailLaunch').onclick=()=>{const all=renderAudience(),channels=selectedMailChannels();if(!channels.length)return toast('Выберите хотя бы один канал рассылки.','warn');const q=all.filter(r=>r.channel!=='none');if(!q.length)return toast('Нет клиентов с согласием и доступным контактом в выбранных каналах.','warn');const p=selectedMailPromo();const c={id:`mail-${Date.now()}`,name:$('mailCampaignName').value.trim()||'Кампания',createdAt:new Date().toISOString(),segment:$('mailSegment').value,channels,promoId:p?.id||'',promo:p?.code||'',discountValue:Number(p?.discountValue||0),discountType:p?.discountType||'percent',discount:p?promoDiscountText(p):'',promoStartDate:p?.startDate||'',promoEndDate:p?.endDate||'',text:$('mailMessage').value,counts:queueStats(q),queue:q,status:'queue'};const list=campaigns();list.push(c);saveCampaigns(list);renderHistory();openQueue(c);toast('Рассылка подготовлена. Каждый клиент добавлен только в один канал.');};
$('mailTemplateSave').onclick=()=>{const name=$('mailTemplateName').value.trim(),text=$('mailTemplateText').value.trim();if(!name||!text)return toast('Введите название и текст шаблона.','warn');const list=templates(),existing=list.find(t=>t.name.toLowerCase()===name.toLowerCase());if(existing)existing.text=text;else list.push({id:`tpl-${Date.now()}`,name,text});saveTemplates(list);$('mailTemplateName').value='';$('mailTemplateText').value='';renderTemplateControls();toast('Шаблон сохранён.');};
mailingView.onclick=e=>{const use=e.target.closest('[data-mail-template-use]'),edit=e.target.closest('[data-mail-template-edit]'),del=e.target.closest('[data-mail-template-delete]'),hist=e.target.closest('[data-mail-history-open]');if(use){const t=templates().find(x=>x.id===use.dataset.mailTemplateUse);if(t){$('mailTemplateSelect').value=t.id;$('mailMessage').value=t.text;renderAudience()}return}if(edit){const t=templates().find(x=>x.id===edit.dataset.mailTemplateEdit);if(t){$('mailTemplateName').value=t.name;$('mailTemplateText').value=t.text}return}if(del){const list=templates().filter(x=>x.id!==del.dataset.mailTemplateDelete);saveTemplates(list.length?list:defaultTemplates);renderTemplateControls();return}if(hist){openQueue(campaigns().find(x=>x.id===hist.dataset.mailHistoryOpen));}};
['mailSegment','mailPromoSelect','mailMessage'].forEach(id=>$(id)?.addEventListener(id==='mailMessage'?'input':'change',renderAudience));
}
ensurePromoUi();
let mailBound=false;
mailingButton.onclick=()=>{qa('.view').forEach(v=>v.classList.remove('on'));mailingView.classList.add('on');qa('header nav button').forEach(b=>b.classList.remove('on'));mailingButton.classList.add('on');ensurePromoUi();ensureMailChannelUi();if(!mailBound){bindMailings();mailBound=true}else{renderTemplateControls();renderPromoControls();renderHistory();renderAudience()}};
/* ---------- Final PDF catalog fix ----------
Old historical code repeatedly rebinds #sunCatalogPdfButton to window.print().
Leave that ID as a hidden sink and use a plain link for the real PDF viewer.
*/
const PDF_PATH='catalog/solnce-catalog-one-item-per-page.pdf';
function currentCatalogCategory(){const buttons=qa('#new .sun-catalog-top-toolbar .cats button,#new .cats button'),active=buttons.find(b=>b.style.outline&&b.style.outline!=='none')||buttons.find(b=>b.getAttribute('aria-current')==='true');if(active?.dataset.category!=null)return Number(active.dataset.category);const title=document.querySelector('#new .catalog h1')?.textContent?.trim()||'';const map={'Боксы':0,'Премиум':5,'Посуда':1,'Дополнения':2,'Напитки':3,'Доставка':4};const f=Object.entries(map).find(([n])=>title.startsWith(n));return f?Number(f[1]):0}
function installStablePdfLink(){
const head=document.querySelector('#new .catalog .catalog-head'),configure=qa('button',head||document).find(b=>b.textContent.trim()==='Настроить');if(!head||!configure)return;
let legacy=$('sunCatalogPdfButton');if(legacy){legacy.classList.add('sun-pdf-legacy-dummy');legacy.setAttribute('aria-hidden','true');legacy.tabIndex=-1;legacy.title='';}
else{legacy=document.createElement('button');legacy.id='sunCatalogPdfButton';legacy.type='button';legacy.className='sun-pdf-legacy-dummy';legacy.setAttribute('aria-hidden','true');legacy.tabIndex=-1;head.appendChild(legacy)}
let link=$('sunCatalogPdfViewerLink');if(!link){link=document.createElement('a');link.id='sunCatalogPdfViewerLink';link.className='outline sun-catalog-pdf-button';link.textContent='Скачать PDF';link.href=PDF_PATH;link.target='_blank';link.rel='noopener';link.title='Открыть готовый PDF-каталог. В просмотрщике можно сохранить файл или распечатать.';configure.insertAdjacentElement('beforebegin',link)}
const hide=currentCatalogCategory()===4;link.hidden=hide;link.style.display=hide?'none':'';
}
installStablePdfLink();setTimeout(installStablePdfLink,50);setTimeout(installStablePdfLink,650);
document.addEventListener('click',e=>{if(e.target.closest('#new .cats button,#new .sun-catalog-top-toolbar .cats button'))setTimeout(installStablePdfLink,10)},true);
const catalog=document.querySelector('#new .catalog');if(catalog&&'MutationObserver'in window)new MutationObserver(()=>installStablePdfLink()).observe(catalog,{childList:true,subtree:true});
/* Initial sync */
fillOrderContactChannel();
setTimeout(()=>{injectEventTypeSettings();installStablePdfLink()},150);
})();
</script>
<!-- ===== USER REQUEST FINAL PATCH v3 ===== -->
<style id="sun-user-final-patch-style-v3">
/* Blank-form settings live only in Settings. Keep the legacy print-page card inert and invisible. */
#sun-print-center #sunPrintExtraOptions{display:none!important}
#enterprise-settings:not(.on){display:none!important}
#enterprise-settings.on{display:block!important}
body #sunBlankSettingsCardV3{display:none!important}
#enterprise-settings.on #sunBlankSettingsCardV3{display:block!important}
/* Older PDF controls remain as compatibility sinks; only the live catalog button is visible. */
#sunCatalogPdfViewerLink,#sunCatalogPdfButton{display:none!important}
#sunLiveCatalogPdfButton{display:inline-flex;align-items:center;justify-content:center;gap:6px;white-space:nowrap}
#sunLiveCatalogPrintRoot{display:none}
.sun-settings-blank-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin-top:10px}
.sun-settings-blank-grid label{display:flex;flex-direction:row;align-items:center;gap:8px;color:#394653}
.sun-settings-blank-grid input{width:17px;height:17px}
@media(max-width:700px){.sun-settings-blank-grid{grid-template-columns:1fr}}
@media print{
body.sun-live-catalog-print{-webkit-print-color-adjust:exact!important;print-color-adjust:exact!important;background:#f8f4eb!important}
body.sun-live-catalog-print *{visibility:hidden!important}
body.sun-live-catalog-print #sunLiveCatalogPrintRoot,
body.sun-live-catalog-print #sunLiveCatalogPrintRoot *{visibility:visible!important}
body.sun-live-catalog-print #sunLiveCatalogPrintRoot{display:block!important;position:absolute!important;left:0!important;top:0!important;width:210mm!important;background:#f8f4eb!important;padding:0!important;margin:0!important}
.sun-live-catalog-page{page-break-after:always;break-after:page;width:210mm;height:297mm;padding:11.5mm 17mm 10mm;display:flex;flex-direction:column;overflow:hidden;position:relative;font-family:Arial,sans-serif;color:#1f1f1d;background:#f8f4eb}
.sun-live-catalog-page:last-child{page-break-after:auto;break-after:auto}
.sun-live-catalog-head{display:grid;grid-template-columns:auto 1fr auto;align-items:center;gap:5mm;min-height:17mm;padding-bottom:5.5mm;border-bottom:.45mm solid #c99a32}
.sun-live-catalog-logo{width:14mm;height:14mm;object-fit:contain}
.sun-live-catalog-brand strong{display:block;font-size:15.3pt;line-height:1.05;font-weight:850;letter-spacing:.01em;color:#1e1e1c}
.sun-live-catalog-brand small{display:block;margin-top:1.5mm;font-size:7.8pt;color:#87827a;letter-spacing:.035em;text-transform:uppercase}
.sun-live-catalog-counter{font-size:7.5pt;color:#7e7971;align-self:start;padding-top:2.4mm}
.sun-live-catalog-visual{position:relative;margin-top:6.8mm;height:96mm;border-radius:4mm;background:#f0ebdf;display:flex;align-items:center;justify-content:center;overflow:hidden;padding:3mm}
.sun-live-catalog-photo{width:100%;height:100%;object-fit:contain;object-position:center}
.sun-live-catalog-photo.is-placeholder{max-width:55mm;max-height:55mm;opacity:.55}
.sun-live-catalog-section{position:absolute;left:6mm;top:4.8mm;z-index:2;max-width:74%;border-radius:999px;background:#191919;color:#fff;padding:3mm 5.5mm 2.7mm;font-size:7.7pt;line-height:1;font-weight:800;text-transform:uppercase;letter-spacing:.015em}
.sun-live-catalog-title-row{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:8mm;align-items:start;margin-top:5mm}
.sun-live-catalog-name{font-size:18pt;line-height:1.03;font-weight:850;letter-spacing:.005em;color:#1f1f1d}
.sun-live-catalog-price{font-size:18pt;line-height:1;font-weight:850;color:#c99a32;white-space:nowrap;padding-top:1.5mm}
.sun-live-catalog-facts{display:grid;grid-template-columns:repeat(2,1fr);gap:7mm;margin-top:6.2mm;width:58%}
.sun-live-catalog-fact small{display:block;color:#77736d;font-size:6.6pt;font-weight:800;text-transform:uppercase;margin-bottom:1mm}
.sun-live-catalog-fact b{display:block;font-size:9pt;color:#232321;font-weight:800;text-transform:uppercase}
.sun-live-catalog-divider{height:.25mm;background:#d9d0c2;margin-top:5.4mm}
.sun-live-catalog-composition{margin-top:5.2mm;min-height:45mm}
.sun-live-catalog-composition h3{margin:0 0 3.4mm;font-size:10.5pt;line-height:1;font-weight:850;text-transform:uppercase;color:#1f1f1d}
.sun-live-catalog-composition ul{list-style:none;margin:0;padding:0;display:grid;gap:1.6mm}
.sun-live-catalog-composition li{position:relative;padding-left:6mm;font-size:8.3pt;line-height:1.28;color:#33312e}
.sun-live-catalog-composition li:before{content:'';position:absolute;left:.3mm;top:.43em;width:2mm;height:2mm;border-radius:50%;background:#c99a32}
.sun-live-catalog-empty{font-size:8pt;color:#77736d;margin:0}
.sun-live-catalog-footer{position:absolute;left:17mm;right:17mm;bottom:10mm;padding-top:3.3mm;border-top:.25mm solid #d9d0c2;display:flex;justify-content:space-between;gap:8mm;color:#88827a;font-size:6.6pt}
@page{size:A4 portrait;margin:0}
}
</style>
<script id="sun-user-final-patch-script-v3">
(()=>{
'use strict';
if(window.__sunUserFinalPatchV3)return;window.__sunUserFinalPatchV3=true;
const $=id=>document.getElementById(id),qa=(sel,root=document)=>[...root.querySelectorAll(sel)];
const esc=window.SunSafe.escapeHTML;
const PRINT_KEY='sunPrintSettingsV1';
const printDefaults={showMapInBlank:true,showBlankPhone:true,showBlankNote:true,showBlankTotals:true};
function readPrint(){try{return Object.assign({},printDefaults,JSON.parse(localStorage.getItem(PRINT_KEY)||'{}'))}catch(_){return {...printDefaults}}}
function writePrint(patch){const next=Object.assign({},readPrint(),patch);try{localStorage.setItem(PRINT_KEY,JSON.stringify(next))}catch(_){}return next}
function auditCard(grid){return qa(':scope > .enterprise-card',grid).find(card=>(card.querySelector('h2')?.textContent||'').trim()==='\u0418\u0441\u0442\u043e\u0440\u0438\u044f \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0439')||null}
function pinAuditLast(){const grid=document.querySelector('#enterprise-settings .enterprise-grid');if(!grid)return;const audit=auditCard(grid);if(audit&&grid.lastElementChild!==audit)grid.appendChild(audit)}
function syncMapSetting(value){const cb=$('orderPrintMap');if(cb){cb.checked=Boolean(value);cb.dispatchEvent(new Event('change',{bubbles:true}))}try{if(typeof draft!=='undefined'&&draft)draft.printMap=Boolean(value)}catch(_){}}
function ensureBlankSettings(){
const grid=document.querySelector('#enterprise-settings .enterprise-grid');if(!grid)return;
let card=$('sunBlankSettingsCardV3');const s=readPrint();
if(!card){
card=document.createElement('section');card.id='sunBlankSettingsCardV3';card.className='enterprise-card wide';
card.innerHTML=`<h2>\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0431\u043b\u0430\u043d\u043a\u0430</h2><p class="hint">\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0431\u043b\u0430\u043d\u043a\u0430 \u0437\u0430\u043a\u0430\u0437\u0430 \u0434\u043b\u044f \u0432\u0441\u0435\u0445 \u0437\u0430\u043a\u0430\u0437\u043e\u0432.</p><div class="sun-settings-blank-grid"><label><input id="sunSettingsBlankMap" type="checkbox"> \u041f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u043a\u0430\u0440\u0442\u0443</label><label><input id="sunSettingsBlankPhone" type="checkbox"> \u041f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0442\u0435\u043b\u0435\u0444\u043e\u043d \u043a\u043b\u0438\u0435\u043d\u0442\u0430</label><label><input id="sunSettingsBlankNote" type="checkbox"> \u041f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u043f\u0440\u0438\u043c\u0435\u0447\u0430\u043d\u0438\u0435</label><label><input id="sunSettingsBlankTotals" type="checkbox"> \u041f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0440\u0430\u0441\u0447\u0451\u0442 \u043e\u043f\u043b\u0430\u0442\u044b</label></div>`;
const audit=auditCard(grid);window.SunSafe.insertBefore(grid,card,audit);
const bind=(id,key)=>{$(id).addEventListener('change',e=>{const next=writePrint({[key]:e.target.checked});if(key==='showMapInBlank')syncMapSetting(next.showMapInBlank)})};
bind('sunSettingsBlankMap','showMapInBlank');bind('sunSettingsBlankPhone','showBlankPhone');bind('sunSettingsBlankNote','showBlankNote');bind('sunSettingsBlankTotals','showBlankTotals');
}
const now=readPrint();
if($('sunSettingsBlankMap'))$('sunSettingsBlankMap').checked=now.showMapInBlank;
if($('sunSettingsBlankPhone'))$('sunSettingsBlankPhone').checked=now.showBlankPhone;
if($('sunSettingsBlankNote'))$('sunSettingsBlankNote').checked=now.showBlankNote;
if($('sunSettingsBlankTotals'))$('sunSettingsBlankTotals').checked=now.showBlankTotals;
pinAuditLast();
}
function enforceBlankSettingsPlacement(){
const settings=$('enterprise-settings');
qa('section,div').filter(el=>(el.querySelector(':scope > h2')?.textContent||'').trim()==='Настройки бланка').forEach(el=>{
if(el.id!=='sunBlankSettingsCardV3'||!settings?.contains(el))el.remove();
});
}
function settingsRefresh(){setTimeout(()=>{ensureBlankSettings();enforceBlankSettingsPlacement();pinAuditLast()},90)}
document.addEventListener('click',e=>{const b=e.target.closest('header nav button');if(!b)return;const name=String(b.dataset.navLabel||b.textContent||'').trim();if(name==='\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438')settingsRefresh()},false);
const settingsView=$('enterprise-settings');if(settingsView)new MutationObserver(()=>queueMicrotask(()=>{enforceBlankSettingsPlacement();pinAuditLast()})).observe(settingsView,{childList:true,subtree:true});
document.addEventListener('click',e=>{const b=e.target.closest('header nav button');if(!b)return;const name=String(b.dataset.navLabel||b.textContent||'').trim();if(name!=='Настройки')settingsView?.classList.remove('on');setTimeout(enforceBlankSettingsPlacement,0)},true);
// enterprise-settings is already observed above; no body-wide observer is needed here.
function stripClientManagerChannel(){
const panel=$('sunClientCommsPanel');if(!panel)return;
qa('label',panel).filter(label=>(label.textContent||'').trim().startsWith('\u041f\u043e\u0441\u043b\u0435\u0434\u043d\u0438\u0439 \u043a\u0430\u043d\u0430\u043b \u043c\u0435\u043d\u0435\u0434\u0436\u0435\u0440\u0430')).forEach(label=>label.remove());
}
const previousOpenClient=window.openClientCard;
if(typeof previousOpenClient==='function')window.openClientCard=function(...args){const result=previousOpenClient.apply(this,args);setTimeout(stripClientManagerChannel,0);setTimeout(stripClientManagerChannel,80);return result};
function currentCategory(){
const buttons=qa('#new .sun-catalog-top-toolbar .cats button,#new .cats button');
const active=buttons.find(b=>b.style.outline&&b.style.outline!=='none')||buttons.find(b=>b.getAttribute('aria-current')==='true');
if(active?.dataset?.category!=null)return Number(active.dataset.category);
const title=document.querySelector('#new .catalog h1')?.textContent?.trim()||'';
const names=[['\u0411\u043e\u043a\u0441\u044b',0],['\u041f\u0440\u0435\u043c\u0438\u0443\u043c',5],['\u041f\u043e\u0441\u0443\u0434\u0430',1],['\u0414\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f',2],['\u041d\u0430\u043f\u0438\u0442\u043a\u0438',3],['\u0414\u043e\u0441\u0442\u0430\u0432\u043a\u0430',4]];
return names.find(([name])=>title.startsWith(name))?.[1]??0;
}
const categoryName=cat=>({0:'\u0411\u043e\u043a\u0441\u044b',1:'\u041f\u043e\u0441\u0443\u0434\u0430',2:'\u0414\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f',3:'\u041d\u0430\u043f\u0438\u0442\u043a\u0438',4:'\u0414\u043e\u0441\u0442\u0430\u0432\u043a\u0430',5:'\u041f\u0440\u0435\u043c\u0438\u0443\u043c'})[cat]||'\u041a\u0430\u0442\u0430\u043b\u043e\u0433';
function allBoxes(){try{if(Array.isArray(boxes))return boxes}catch(_){}try{const saved=JSON.parse(localStorage.getItem('sunBoxes')||'[]');return Array.isArray(saved)?saved:[]}catch(_){return[]}}
function ingredientWeight(row){if(!Array.isArray(row))return 0;const direct=Math.max(0,Number(row[3]||0));if(direct)return direct;const m=String(row[0]||'').match(/[—–-]\s*(\d+(?:[.,]\d+)?)\s*г(?:\s*\/\s*шт\.?)?\s*$/i);return m?Math.max(0,Number(String(m[1]).replace(',','.'))):0}
function cleanIngredientName(name){return String(name||'').replace(/\s*[—–-]\s*\d+(?:[.,]\d+)?\s*г(?:\s*\/\s*шт\.?)?\s*$/i,'').trim()}
function composition(item){
if(Array.isArray(item?.ingredients)&&item.ingredients.some(x=>Array.isArray(x)&&x[0]))return item.ingredients.filter(x=>Array.isArray(x)&&x[0]).map(x=>{const q=Math.max(0,Number(x[1]||0)),u=String(x[2]||'').trim(),name=cleanIngredientName(x[0]),w=ingredientWeight(x),shown=Number.isInteger(q)?String(q):q.toLocaleString('ru-RU',{maximumFractionDigits:2});if(/^поз\.?$/i.test(u)&&q<=1)return `${name}${w?`${w.toLocaleString('ru-RU')} г/шт.`:''}`;return `${shown} ${u} ${name}${w?`${w.toLocaleString('ru-RU')} г/шт.`:''}`.replace(/\s+/g,' ').trim()});
if(Array.isArray(item?.composition)&&item.composition.length)return item.composition.map(x=>String(x||'').trim()).filter(Boolean);
return [];
}
const money=value=>`${Number(value||0).toLocaleString('ru-RU')} \u20bd`;
function ensureLivePdfButton(){
const head=document.querySelector('#new .catalog .catalog-head');if(!head)return;
const configure=qa('button',head).find(b=>(b.textContent||'').trim()==='\u041d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c');if(!configure)return;
let btn=$('sunLiveCatalogPdfButton');
if(!btn){btn=document.createElement('button');btn.id='sunLiveCatalogPdfButton';btn.type='button';btn.className='outline';btn.textContent='\u0421\u043a\u0430\u0447\u0430\u0442\u044c PDF';btn.title='PDF \u0444\u043e\u0440\u043c\u0438\u0440\u0443\u0435\u0442\u0441\u044f \u0438\u0437 \u0442\u0435\u043a\u0443\u0449\u0438\u0445 \u043f\u043e\u0437\u0438\u0446\u0438\u0439 \u0438 \u0446\u0435\u043d \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f.';btn.addEventListener('click',printLiveCatalog);configure.insertAdjacentElement('beforebegin',btn)}
const hide=currentCategory()===4;btn.hidden=hide;btn.style.display=hide?'none':'';
}
async function printLiveCatalog(){
const cat=currentCategory(),items=allBoxes().filter(item=>Number(item.category||0)===Number(cat));
if(!items.length){window.SunEnterprise?.toast?.('\u0412 \u044d\u0442\u043e\u043c \u0440\u0430\u0437\u0434\u0435\u043b\u0435 \u043d\u0435\u0442 \u043f\u043e\u0437\u0438\u0446\u0438\u0439.','warn');return}
let root=$('sunLiveCatalogPrintRoot');if(!root){root=document.createElement('div');root.id='sunLiveCatalogPrintRoot';document.body.appendChild(root)}
const total=items.length,dateText=new Date().toLocaleDateString('ru-RU');
root.innerHTML=items.map((item,index)=>{
const lines=composition(item),section=String(item.catalogSection||categoryName(cat)).toUpperCase(),photo=item.photo||(window.CateriumBranding.documentImage()),placeholder=item.photo?'':' is-placeholder';
const subtitle=cat===0?'\u041f\u041e\u041b\u041d\u042b\u0419 \u041a\u0410\u0422\u0410\u041b\u041e\u0413 \u0424\u0423\u0420\u0428\u0415\u0422\u041d\u042b\u0425 \u0411\u041e\u041a\u0421\u041e\u0412':`\u041a\u0410\u0422\u0410\u041b\u041e\u0413 ${String(categoryName(cat)).toUpperCase()}`;
const page=`${String(index+1).padStart(2,'0')} / ${String(total).padStart(2,'0')}`;
return `<article class="sun-live-catalog-page"><div class="sun-live-catalog-head"><img class="sun-live-catalog-logo" src="${window.CateriumBranding.logoHTML()}" alt=""><div class="sun-live-catalog-brand"><strong>${window.CateriumBranding.nameHTML().toUpperCase()}</strong><small>${esc(subtitle)}</small></div><div class="sun-live-catalog-counter">${esc(page)}</div></div><div class="sun-live-catalog-visual"><div class="sun-live-catalog-section">${esc(section)}</div><img class="sun-live-catalog-photo${placeholder}" src="${esc(photo)}" alt="${esc(item.name||'')}"></div><div class="sun-live-catalog-title-row"><div class="sun-live-catalog-name">${esc(item.name||'')}</div><div class="sun-live-catalog-price">${money(item.price||0)}</div></div><div class="sun-live-catalog-facts"><div class="sun-live-catalog-fact"><small>\u0412\u0435\u0441</small><b>${esc(String(item.weight||'\u2014').toUpperCase())}</b></div><div class="sun-live-catalog-fact"><small>\u041a\u043e\u043b-\u0432\u043e</small><b>${Number(item.pieces||0)>0?`${Math.round(Number(item.pieces))} \u0448\u0442.`:'\u2014'}</b></div></div><div class="sun-live-catalog-divider"></div><div class="sun-live-catalog-composition"><h3>\u0421\u043e\u0441\u0442\u0430\u0432</h3>${lines.length?`<ul>${lines.map(line=>`<li>${esc(line)}</li>`).join('')}</ul>`:'<p class="sun-live-catalog-empty">\u0421\u043e\u0441\u0442\u0430\u0432 \u043d\u0435 \u0443\u043a\u0430\u0437\u0430\u043d.</p>'}</div><div class="sun-live-catalog-footer"><span>${window.CateriumBranding.contactsHTML()}</span><span>Цены актуальны на ${esc(dateText)}</span></div></article>`
}).join('');
document.body.classList.add('sun-live-catalog-print');
const waitImages=qa('img',root).map(img=>img.complete?Promise.resolve():new Promise(resolve=>{img.addEventListener('load',resolve,{once:true});img.addEventListener('error',resolve,{once:true})}));
await Promise.race([Promise.all(waitImages),new Promise(resolve=>setTimeout(resolve,900))]);
const cleanup=()=>document.body.classList.remove('sun-live-catalog-print');
window.addEventListener('afterprint',cleanup,{once:true});window.print();setTimeout(cleanup,1200);
}
window.sunPrintLiveCatalog=printLiveCatalog;
ensureLivePdfButton();setTimeout(ensureLivePdfButton,120);setTimeout(ensureLivePdfButton,700);
document.addEventListener('click',e=>{if(e.target.closest('#new .cats button,#new .sun-catalog-top-toolbar .cats button'))setTimeout(ensureLivePdfButton,20)},true);
const catalog=$('new')?.querySelector('.catalog');if(catalog)new MutationObserver(()=>ensureLivePdfButton()).observe(catalog,{childList:true,subtree:true});
setTimeout(()=>{if($('enterprise-settings')?.classList.contains('on'))ensureBlankSettings();enforceBlankSettingsPlacement();pinAuditLast();stripClientManagerChannel()},180);
})();
</script>
<script src="app-runtime.js?v=20260918-safari-login"></script>