diff --git a/docs/ORDER-HISTORY-IMPORT-20260917.md b/docs/ORDER-HISTORY-IMPORT-20260917.md new file mode 100644 index 0000000..b093149 --- /dev/null +++ b/docs/ORDER-HISTORY-IMPORT-20260917.md @@ -0,0 +1,7 @@ +# Preserving imported order history + +Imported orders can carry `autoCompletionDisabled: true`. Both server and local automation leave their status, prepayment and balance unchanged, so old dates cannot manufacture full payment. The import archive is a typed workspace storage value (`sunTelegramImportArchiveV1`); reads require both order and customer viewing permissions. The Orders screen exposes searchable original messages and images using text-only DOM rendering and validated image data URLs. Customer records and source files remain outside this repository. + +Opening an order now hydrates its delivery and discount controls before reading them back. The late order-pane patch retains shared pricing, and order enhancements use its item subtotal plus delivery once. Saved historical prices and promotion discounts survive open/save. + +Validation: deployment checks and database permission tests; 66 browser checks passed, 2 existing skips. A private rehearsal round-tripped the import, confirmed idempotency and payment preservation, and opened/saved every imported order while comparing names, phone numbers, addresses, notes, prices, payment and line counts. diff --git a/docs/release-manifest.json b/docs/release-manifest.json index 1039a36..1ec8f32 100644 --- a/docs/release-manifest.json +++ b/docs/release-manifest.json @@ -11,7 +11,7 @@ "serverReady": true, "workspaceAutoDiscovery": true, "invitesTemporarilyDisabled": false, - "pwaCache": "v84-20260917-company-branding", + "pwaCache": "v85-20260917-order-import", "fullOfferDescriptions": true, "dynamicOfferRows": true, "pdfOfferDescriptionFix": true, diff --git a/package.json b/package.json index 92a7cbd..50b8e13 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "version": "17.7.3", "type": "module", "scripts": { - "check:syntax": "node --check public/app-runtime.js && node --check public/service-worker.js && node --check public/legacy/bootstrap.js && node --check public/core/sun-safe.js && node --check public/core/account-center-v1780.js && node --check public/core/performance.js && node --check public/core/auth-security-v1774.js && node --check public/core/trial-promo-developer-v181.js && node --check public/core/order-enhancements-v1775.js && node --check public/core/data-layer-v1773.js && node --check public/core/server-automation-v1770.js && node --check public/core/hotfix-v1763.js && node --check public/core/ops-ux-v1762.js && node --check public/core/ux-fixes-v1764.js && node --check public/core/pdf-engine.js && node --check public/core/classic-offer-pdf-v1767.js && node --check public/core/signature-offer-pdf-v18.js && node --check public/core/developer-console-v1768.js && node --check public/core/offer-workspace-v1769.js && node --check public/core/brand-theme.js && node --check public/core/company-branding.js", + "check:syntax": "node --check public/app-runtime.js && node --check public/service-worker.js && node --check public/legacy/bootstrap.js && node --check public/core/sun-safe.js && node --check public/core/account-center-v1780.js && node --check public/core/performance.js && node --check public/core/auth-security-v1774.js && node --check public/core/trial-promo-developer-v181.js && node --check public/core/order-enhancements-v1775.js && node --check public/core/data-layer-v1773.js && node --check public/core/server-automation-v1770.js && node --check public/core/hotfix-v1763.js && node --check public/core/ops-ux-v1762.js && node --check public/core/ux-fixes-v1764.js && node --check public/core/pdf-engine.js && node --check public/core/classic-offer-pdf-v1767.js && node --check public/core/signature-offer-pdf-v18.js && node --check public/core/developer-console-v1768.js && node --check public/core/offer-workspace-v1769.js && node --check public/core/brand-theme.js && node --check public/core/company-branding.js && node --check public/core/import-archive.js", "test:static": "node tests/static-security.mjs && node tests/auth-security-v1774.mjs && node tests/employee-create-v1774.mjs && node tests/html-integrity-v1774.mjs && node tests/edge-security-v1774.mjs && node tests/branding-v1774.mjs && node tests/order-enhancements-v1775.mjs", "check:release": "node tests/release-check.mjs", "check:deploy": "npm run check:syntax && npm run test:static && npm run check:release && node tests/backend-cutover.mjs && npm run test:db", diff --git a/public/app-runtime.js b/public/app-runtime.js index 484d1de..b1dd43e 100644 --- a/public/app-runtime.js +++ b/public/app-runtime.js @@ -628,7 +628,9 @@ window.SUN_LEGACY_CATALOG_V175=[{"id":"1","name":"Фуршетный бокс updatePromoMeta(); } function updatePromoMeta(){const meta=$('promoCodeMeta'),select=$('promoCodeSelect');if(!meta||!select)return;const id=select.value;if(!id){meta.textContent='Выберите активный промокод из раздела «Рассылки».';return}if(id==='__snapshot__'){meta.textContent=`Условия сохранены в заказе: ${draft?.promoCode||''} · ${draft?.promoDiscountType==='amount'?money(draft?.promoDiscountValue||0):`${Number(draft?.promoDiscountValue||0).toLocaleString('ru-RU',{maximumFractionDigits:2})}%`}.`;return}const p=promoById(id);meta.textContent=p?`${p.code} · скидка ${promoDiscountText(p)} · действует ${p.startDate||'без даты'} — ${p.endDate||'без даты'}`:'Промокод недоступен.';} + let hydratingOrder=false; function captureDiscounts(){ + if(hydratingOrder)return; if(!draft)return;draft.manualDiscountValue=Math.max(0,Number($('manualDiscountValue')?.value||0));draft.manualDiscountType=$('manualDiscountType')?.value==='amount'?'amount':'percent'; const select=$('promoCodeSelect');if(!select)return;const id=String(select.value||''); if(!id){draft.promoId='';draft.promoCode='';draft.promoDiscountValue=0;draft.promoDiscountType='percent';draft.promoStartDate='';draft.promoEndDate='';return} @@ -636,7 +638,7 @@ window.SUN_LEGACY_CATALOG_V175=[{"id":"1","name":"Фуршетный бокс const p=promoById(id);if(!p||!promoActive(p)){renderPromoSelect();return} draft.promoId=String(p.id||'');draft.promoCode=String(p.code||'').trim();draft.promoDiscountValue=Math.max(0,Number(p.discountValue||0));draft.promoDiscountType=p.discountType==='amount'?'amount':'percent';draft.promoStartDate=String(p.startDate||'');draft.promoEndDate=String(p.endDate||'');updatePromoMeta(); } - function captureDelivery(){if(!draft)return;draft.deliveryCost=Math.max(0,Number($('deliveryCost')?.value||0));} + function captureDelivery(){if(!draft||hydratingOrder)return;draft.deliveryCost=Math.max(0,Number($('deliveryCost')?.value||0));} function syncDeliveryField(){ensureDiscountUI();if($('deliveryCost'))$('deliveryCost').value=Math.max(0,Number(draft?.deliveryCost||0));} function discountAmount(value,type,base){const n=Math.max(0,Number(value||0));return Math.min(base,type==='amount'?n:base*Math.min(100,n)/100)} function pricing(order=draft){const base=baseTotal(order),manual=discountAmount(order?.manualDiscountValue,order?.manualDiscountType,base),afterManual=Math.max(0,base-manual),promo=discountAmount(order?.promoDiscountValue,order?.promoDiscountType,afterManual),itemsTotal=Math.max(0,afterManual-promo),delivery=Math.max(0,Number(order?.deliveryCost||0)),total=itemsTotal+delivery,paid=Math.max(0,Number(order===draft?($('prepayment')?.value||order?.prepayment||0):(order?.prepayment||0))),balance=Math.max(0,total-paid);return {base,manual,promo,itemsTotal,delivery,total,paid,balance,discount:manual+promo}} @@ -685,7 +687,7 @@ window.SUN_LEGACY_CATALOG_V175=[{"id":"1","name":"Фуршетный бокс const previousSave=window.saveOrder; if(typeof previousSave==='function')window.saveOrder=function(...args){captureBuyerFromFields();captureDiscounts();captureDelivery();const result=previousSave.apply(this,args);renderPromoStats();return result}; const previousOpen=window.openOrder; - if(typeof previousOpen==='function')window.openOrder=function(...args){const r=previousOpen.apply(this,args);setTimeout(()=>{syncBuyerFields();syncDiscountFields();syncDeliveryField();setOrderPane('order');patchDocButtons();renderCatalogV5();},0);return r}; + if(typeof previousOpen==='function')window.openOrder=function(...args){hydratingOrder=true;let r;try{r=previousOpen.apply(this,args)}catch(error){hydratingOrder=false;throw error}setTimeout(()=>{try{syncBuyerFields();syncDiscountFields();syncDeliveryField();setOrderPane('order');patchDocButtons();renderCatalogV5()}finally{hydratingOrder=false;updateOrderSummary()}},0);return r}; const previousReset=window.resetDraft; if(typeof previousReset==='function')window.resetDraft=function(...args){const r=previousReset.apply(this,args);draft.buyerRequisites=buyerDefaults();draft.manualDiscountValue=0;draft.manualDiscountType='percent';draft.promoId='';draft.promoCode='';draft.promoDiscountValue=0;draft.promoDiscountType='percent';draft.promoStartDate='';draft.promoEndDate='';draft.deliveryCost=0;delete draft.qrImage;setTimeout(()=>{syncBuyerFields();syncDiscountFields();syncDeliveryField();setOrderPane('order');renderCatalogV5();},0);return r}; const previousStats=window.renderStats; @@ -5738,4 +5740,3 @@ window.SUN_LEGACY_CATALOG_V175=[{"id":"1","name":"Фуршетный бокс window.SunChatV31=window.SunChatV29; if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',boot,{once:true});else boot(); })(); - diff --git a/public/core/import-archive.js b/public/core/import-archive.js new file mode 100644 index 0000000..50a4036 --- /dev/null +++ b/public/core/import-archive.js @@ -0,0 +1,29 @@ +(()=>{ + 'use strict'; + if(window.CateriumImportArchive)return; + const KEY='sunTelegramImportArchiveV1'; + let dialog=null; + const read=()=>{try{return JSON.parse(localStorage.getItem(KEY)||'null')}catch(_){return null}}; + const node=(tag,text)=>{const e=document.createElement(tag);if(text!==undefined)e.textContent=String(text);return e}; + function close(){dialog?.remove();dialog=null} + function show(){ + const archive=read();if(!archive)return;close(); + dialog=node('dialog');dialog.setAttribute('aria-label','Архив заказов из Telegram');dialog.style.cssText='width:min(900px,94vw);max-height:88vh;overflow:auto;border:1px solid #ccd2d6;border-radius:16px;padding:24px'; + const top=node('div');top.style.cssText='display:flex;justify-content:space-between;gap:20px';top.append(node('h2','Исходный архив Telegram'));const exit=node('button','Закрыть');exit.onclick=close;top.append(exit);dialog.append(top); + dialog.append(node('p',`${archive.orderCount} заказов · ${archive.clientCount} клиентов. Цены и платежи сохранены по исходным сообщениям. Неуказанная оплата не подтверждает долг. Автоматическое закрытие импортированных заказов отключено.`)); + const search=node('input');search.type='search';search.placeholder='Номер заказа, имя, телефон или адрес';search.setAttribute('aria-label','Поиск в архиве');search.style.cssText='width:100%;padding:12px;margin:8px 0';dialog.append(search); + const list=node('div');dialog.append(list); + const render=()=>{list.replaceChildren();const q=search.value.toLocaleLowerCase('ru');for(const message of archive.messages||[]){if(q&&!`${message.id} ${message.text} ${message.transcription||''}`.toLocaleLowerCase('ru').includes(q))continue;const d=node('details');d.style.cssText='padding:10px 0;border-bottom:1px solid #ddd';d.append(node('summary',`${message.sentAt} · ${message.id} · ${(message.text||'Фотография').split('\n')[0]}`));const pre=node('pre',message.text);pre.style.cssText='white-space:pre-wrap;overflow-wrap:anywhere;font:inherit';d.append(pre);if(message.transcription)d.append(node('p',message.transcription));for(const a of archive.attachments||[]){if(a.messageId!==message.id||!/^data:image\/(?:jpeg|png|webp);base64,[A-Za-z0-9+/=]+$/.test(a.data||''))continue;const img=node('img');img.src=a.data;img.alt=a.href;img.loading='lazy';img.style.cssText='display:block;max-width:100%;max-height:80vh;object-fit:contain';d.append(img);}list.append(d);}}; + search.oninput=render;render();document.body.append(dialog);dialog.showModal();dialog.addEventListener('close',close); + } + function install(){ + const archive=read(),ordersView=document.getElementById('orders');let b=document.getElementById('cateriumTelegramArchive'); + if(!archive){b?.remove();return}if(!ordersView||b)return; + b=node('button','Исходный архив Telegram');b.id='cateriumTelegramArchive';b.type='button';b.className='outline';b.onclick=show; + const anchor=ordersView.querySelector('h1');if(anchor)anchor.insertAdjacentElement('afterend',b);else ordersView.prepend(b); + } + window.CateriumImportArchive={show,refresh:install}; + window.addEventListener('sun:cloud-state-applied',()=>{close();install()}); + window.addEventListener('sun:cloud-permissions-changed',()=>{close();install()}); + if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',install,{once:true});else install(); +})(); diff --git a/public/core/order-enhancements-v1775.js b/public/core/order-enhancements-v1775.js index 472e479..8b514a0 100644 --- a/public/core/order-enhancements-v1775.js +++ b/public/core/order-enhancements-v1775.js @@ -9,11 +9,11 @@ function activeLines(){return Array.isArray(draft?.lines)?draft.lines.filter(l=> function comments(){let rows=lineRows(),ls=activeLines();rows.forEach((row,i)=>{let l=ls[i];if(!l)return;let w=$('.sun-line-comment-v1775',row);if(!w){w=document.createElement('div');w.className='sun-line-comment-v1775';w.innerHTML='
';row.appendChild(w)}let btn=$('.sun-line-comment-btn-v1775',w),editor=$('.sun-line-comment-editor-v1775',w),input=$('input',w),save=$('.sun-line-comment-save-v1775',w);let current=safe(l.comment);btn.classList.toggle('has-comment',!!current);btn.textContent=current?'💬 Комментарий добавлен':'💬 Оставить комментарий';if(document.activeElement!==input&&input.value!==current)input.value=current;if(!btn.dataset.bound){btn.dataset.bound='1';btn.addEventListener('click',()=>{editor.classList.toggle('on');if(editor.classList.contains('on')){input.value=safe(l.comment);setTimeout(()=>input.focus(),0)}});input.addEventListener('input',()=>{l.comment=input.value.slice(0,300)});input.addEventListener('keydown',e=>{if(e.key==='Enter'){e.preventDefault();save.click()}});save.addEventListener('click',()=>{l.comment=input.value.trim().slice(0,300);btn.classList.toggle('has-comment',!!l.comment);btn.textContent=l.comment?'💬 Комментарий добавлен':'💬 Оставить комментарий';editor.classList.remove('on')})}})} function searchBox(){let catalog=$('#new .catalog');if(!catalog||$('#sunCatalogSearchV1775'))return;let i=document.createElement('input');i.id='sunCatalogSearchV1775';i.type='search';i.placeholder='Поиск по № бокса или названию';($('.catalog-head',catalog)||catalog.firstElementChild)?.insertAdjacentElement('afterend',i);i.addEventListener('input',()=>{let q=safe(i.value).toLowerCase().replace(/ё/g,'е');$$('#tiles .tile').forEach(t=>{let text=safe(t.textContent).toLowerCase().replace(/ё/g,'е');t.hidden=t.classList.contains('add')?!!q:!!(q&&!text.includes(q))})})} function delivery(){return Math.max(0,Number($('#orderDeliveryCost')?.value||draft?.costs?.delivery||draft?.deliveryCost||0))} -function financial(){let positions=Math.max(0,Number(typeof calcOrderTotal==='function'?calcOrderTotal():0)),d=delivery(),paid=Math.max(0,Number($('#prepayment')?.value||draft?.prepayment||0)),total=positions+d;return{positions,d,total,paid,balance:Math.max(0,total-paid)}} +function financial(){const p=window.sunOrderPricing?.(draft);let d=p?Number(p.delivery||0):delivery(),positions=p?Number(p.itemsTotal||0):Math.max(0,Number(typeof calcOrderTotal==='function'?calcOrderTotal():0)),paid=Math.max(0,Number($('#prepayment')?.value||draft?.prepayment||0)),total=positions+d;return{positions,d,total,paid,balance:Math.max(0,total-paid)}} function summary(){let h=$('.order-summary');if(!h)return;let f=financial();if($('#summaryTotal'))$('#summaryTotal').textContent=fmt(f.positions);let d=$('#sunSummaryDeliveryV1775');if(!d){d=document.createElement('p');d.id='sunSummaryDeliveryV1775';h.appendChild(d)}d.innerHTML=`Доставка${fmt(f.d)}`;let t=$('#sunSummaryGrandV1775');if(!t){t=document.createElement('p');t.id='sunSummaryGrandV1775';t.className='sun-summary-grand-v1775';h.appendChild(t)}t.innerHTML=`Итого${fmt(f.total)}`;if($('#orderTotal'))$('#orderTotal').value=Math.round(f.total);if($('#balance'))$('#balance').value=Math.round(f.balance);if($('#summaryBalance'))$('#summaryBalance').textContent=fmt(f.balance);draft.total=f.total;draft.balance=f.balance} function post(){styles();searchBox();comments();summary()} function wrap(n){let old=window[n];if(typeof old!=='function'||old.__commentsV1775)return;let fn=function(...a){let r=old.apply(this,a);queueMicrotask(post);setTimeout(post,50);return r};fn.__commentsV1775=true;window[n]=fn} styles();['render','openOrder','resetDraft','updateOrderSummary','saveOrder'].forEach(wrap);post(); let timer=0;new MutationObserver(ms=>{if(!ms.some(m=>m.addedNodes.length||m.removedNodes.length))return;clearTimeout(timer);timer=setTimeout(()=>{comments();searchBox()},30)}).observe(document.body,{childList:true,subtree:true}); document.addEventListener('input',e=>{if(e.target?.id==='orderDeliveryCost'||e.target?.id==='prepayment')summary()},true); -})(); \ No newline at end of file +})(); diff --git a/public/core/performance.js b/public/core/performance.js index 70df1c6..4666ce9 100644 --- a/public/core/performance.js +++ b/public/core/performance.js @@ -1,7 +1,7 @@ (()=>{ 'use strict'; const VERSION='17.7.3'; - const RELEASE='20260916-v18-1-1-template-whitelist-fix'; + const RELEASE='20260917-order-import'; const hasStoredSession=()=>{try{return Object.keys(localStorage).some(k=>/^sb-.*-auth-token$/i.test(k)&&String(localStorage.getItem(k)||'').length>20)}catch(_){return false}}; function installAuthBoot(){ diff --git a/public/core/ux-fixes-v1764.js b/public/core/ux-fixes-v1764.js index e0378ff..76c525b 100644 --- a/public/core/ux-fixes-v1764.js +++ b/public/core/ux-fixes-v1764.js @@ -76,7 +76,7 @@ if(!canAutoWrite())return {changed:0,ids:[]}; const list=readOrders(),ids=[]; for(const order of list){ - if(!order||String(order.status||'').trim()==='Отменён'||isAutoCompleted(order))continue; + if(!order||order.autoCompletionDisabled===true||String(order.status||'').trim()==='Отменён'||isAutoCompleted(order))continue; const due=dueAt(order);if(!Number.isFinite(due)||nowMs