fix: bound proposal loading and preserve per-order preview
This commit is contained in:
parent
44d6ec9e81
commit
6472dba7ee
@ -11,7 +11,7 @@
|
|||||||
"serverReady": true,
|
"serverReady": true,
|
||||||
"workspaceAutoDiscovery": true,
|
"workspaceAutoDiscovery": true,
|
||||||
"invitesTemporarilyDisabled": false,
|
"invitesTemporarilyDisabled": false,
|
||||||
"pwaCache": "v94-20260918-sync-demo-images-2",
|
"pwaCache": "v95-20260918-proposal-loading",
|
||||||
"fullOfferDescriptions": true,
|
"fullOfferDescriptions": true,
|
||||||
"dynamicOfferRows": true,
|
"dynamicOfferRows": true,
|
||||||
"pdfOfferDescriptionFix": true,
|
"pdfOfferDescriptionFix": true,
|
||||||
@ -409,6 +409,11 @@
|
|||||||
"proposalLayout": "core/proposal-layout.js",
|
"proposalLayout": "core/proposal-layout.js",
|
||||||
"proposalLocalCyrillicFonts": true,
|
"proposalLocalCyrillicFonts": true,
|
||||||
"proposalRasterDpi": 290,
|
"proposalRasterDpi": 290,
|
||||||
|
"proposalPhotoRequestTimeoutMs": 6000,
|
||||||
|
"proposalMediaBudgetMs": 10000,
|
||||||
|
"proposalMediaConcurrent": true,
|
||||||
|
"proposalMissingPhotoNotice": true,
|
||||||
|
"proposalLoadCancellation": true,
|
||||||
"cloudReadTimeoutMs": 12000,
|
"cloudReadTimeoutMs": 12000,
|
||||||
"cloudFallbackTimeoutMs": 15000,
|
"cloudFallbackTimeoutMs": 15000,
|
||||||
"cloudWriteTimeoutMs": 35000,
|
"cloudWriteTimeoutMs": 35000,
|
||||||
|
|||||||
11
docs/releases/2026-09-18-PROPOSAL-LOADING.md
Normal file
11
docs/releases/2026-09-18-PROPOSAL-LOADING.md
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# Client proposal loading recovery
|
||||||
|
|
||||||
|
Release cache: `v95-20260918-proposal-loading`.
|
||||||
|
|
||||||
|
The proposal preview could wait indefinitely for a photo response/body or unrelated UI font, leaving the download button disabled. Photos, logo and gallery now load together, with a 6-second limit per request and a 10-second budget for the media batch, including fallback URLs. The PDF renderer waits only for its three local fonts. Unavailable photos no longer block order text, quantities or prices; the preview identifies missing images and offers a refresh.
|
||||||
|
|
||||||
|
Closing or refreshing the proposal cancels the previous media load. Rendering failures show a retry message instead of an endless loading indicator. Both preview and download still share the same rendered A4 pages. The order's selected template is copied into newly generated and legacy snapshots before rendering, removing a timing dependency on the template picker.
|
||||||
|
|
||||||
|
Repeated cloud notifications no longer rebuild an unchanged proposal template, and a closed proposal does not rerender in the background. Explicit per-order design changes still update the open preview, including older snapshots.
|
||||||
|
|
||||||
|
Regression coverage includes a hanging photo, refresh recovery, a stalled response body cancelled on close, unrelated fonts that never finish, render failure/retry, all 21 covers, long Cyrillic content and actual PDF download/preview parity on desktop and mobile.
|
||||||
@ -1045,7 +1045,8 @@ window.SUN_LEGACY_CATALOG_V175=[];
|
|||||||
return {id:String(item.id??line.id??''),name:String(item.name||line.name||'Позиция'),categoryId,categoryName:catName(categoryId),catalogSection:String(item.catalogSection||''),qty,unitPrice:price,sum:price*qty,weight:String(item.weight||''),boxGuests:String(item.guests||''),photo:String(item.photo||''),composition:comp,compositionTotal,pieces};
|
return {id:String(item.id??line.id??''),name:String(item.name||line.name||'Позиция'),categoryId,categoryName:catName(categoryId),catalogSection:String(item.catalogSection||''),qty,unitPrice:price,sum:price*qty,weight:String(item.weight||''),boxGuests:String(item.guests||''),photo:String(item.photo||''),composition:comp,compositionTotal,pieces};
|
||||||
}).filter(x=>x.qty>0);
|
}).filter(x=>x.qty>0);
|
||||||
const extras=all.filter(item=>[1,2,3].includes(Number(item.category))&&!selected.has(String(item.id))&&!item.hidden).slice(0,6).map(item=>({name:String(item.name||''),categoryName:catName(item.category)})).filter(x=>x.name);
|
const extras=all.filter(item=>[1,2,3].includes(Number(item.category))&&!selected.has(String(item.id))&&!item.hidden).slice(0,6).map(item=>({name:String(item.name||''),categoryName:catName(item.category)})).filter(x=>x.name);
|
||||||
return {version:8,createdAt:new Date().toISOString(),orderId:order?.id||'',sourceSignature:orderSignature(order),event:String(order?.event||''),date:String(order?.date||''),time:String(order?.time||''),client:String(order?.contact||''),guests,foodGrams,foodPieces,drinkMl,pricing:p,promoCode:String(order?.promoCode||''),items,extras,offerOverrides:structuredClone(order?.clientOfferSnapshot?.offerOverrides||{})};
|
const template=order?.clientOfferTemplateId||order?.clientOfferSnapshot?.offerTemplateId;
|
||||||
|
return {version:8,createdAt:new Date().toISOString(),orderId:order?.id||'',offerTemplateId:OFFER_TEMPLATE_IDS.has(template)?template:offerTemplateId(),sourceSignature:orderSignature(order),event:String(order?.event||''),date:String(order?.date||''),time:String(order?.time||''),client:String(order?.contact||''),guests,foodGrams,foodPieces,drinkMl,pricing:p,promoCode:String(order?.promoCode||''),items,extras,offerOverrides:structuredClone(order?.clientOfferSnapshot?.offerOverrides||{})};
|
||||||
}
|
}
|
||||||
function persistSnapshot(orderId,snapshot){
|
function persistSnapshot(orderId,snapshot){
|
||||||
let list=getOrders(),i=list.findIndex(o=>String(o.id)===String(orderId));if(i<0)return false;
|
let list=getOrders(),i=list.findIndex(o=>String(o.id)===String(orderId));if(i<0)return false;
|
||||||
@ -1058,7 +1059,7 @@ window.SUN_LEGACY_CATALOG_V175=[];
|
|||||||
function ensureModal(){
|
function ensureModal(){
|
||||||
if($('sunClientOfferModal'))return;
|
if($('sunClientOfferModal'))return;
|
||||||
const modal=document.createElement('div');modal.id='sunClientOfferModal';modal.className='modal';modal.innerHTML=`<div class="dialog"><div class="sun-offer-modal-head"><div><h2>Предложение клиенту</h2><p class="hint" id="sunOfferMeta"></p></div><div class="spacer"></div><div id="sunOfferStale" class="sun-offer-stale">Заказ изменён — предложение будет автоматически обновлено из текущих данных.</div><div class="sun-offer-modal-actions"><button class="primary" id="sunOfferPdf" type="button">Скачать PDF</button><button class="outline" id="sunOfferEdit" type="button">Редактировать</button><button class="outline" id="sunOfferRefresh" type="button">Обновить</button><button class="outline" id="sunOfferClose" type="button">Закрыть</button></div></div><section class="sun-offer-editor" id="sunOfferEditor"><div class="sun-offer-editor-grid"><label>Название мероприятия<input data-offer-data="event" type="text"></label><label>Имя клиента<input data-offer-data="client" type="text"></label><label>Дата<input data-offer-data="date" type="date"></label><label>Гостей<input data-offer-data="guests" type="number" min="0" step="1"></label><label>Метка предложения<input data-offer-text="proposalLabel" type="text"></label><label>Заголовок меню<input data-offer-text="menuTitle" type="text"></label><label class="wide">Короткий текст первого блока<textarea data-offer-text="heroNote"></textarea></label><label>Заголовок продающего блока<input data-offer-text="salesTitle" type="text"></label><label class="wide">Продающий текст<textarea data-offer-text="salesNote"></textarea></label><label>Заголовок итогов<input data-offer-text="summaryTitle" type="text"></label><label>Заголовок стоимости<input data-offer-text="totalPriceTitle" type="text"></label><label>Заголовок блока «Всё под контролем»<input data-offer-text="controlTitle" type="text"></label><label>Блок рекомендаций<input data-offer-text="extrasTitle" type="text"></label><label class="wide">Пункты блока «Всё под контролем» — каждый с новой строки<textarea data-offer-list="controlLines" rows="7"></textarea></label><label>Заголовок фото на столе<input data-offer-text="finalGalleryTitle" type="text"></label><label class="wide">Примечание внизу<textarea data-offer-text="footerNote"></textarea></label><div class="sun-offer-editor-gallery wide"><div><b>Фото «Как это выглядит на вашем столе»</b><small>В PDF автоматически попадёт 1 или 2 фото только если на последней странице есть свободное место.</small></div><div class="sun-offer-editor-gallery-images"><img src="offer-gallery/001.jpg" alt=""><img src="offer-gallery/002.jpg" alt=""></div></div></div><p class="sun-offer-editor-note">Изменения относятся только к этому предложению. Просмотр ниже — те же страницы, которые попадут в скачанный PDF.</p></section><div class="sun-offer-frame-wrap"><div id="sunClientOfferPreview" aria-label="Предпросмотр предложения"></div></div></div>`;document.body.appendChild(modal);
|
const modal=document.createElement('div');modal.id='sunClientOfferModal';modal.className='modal';modal.innerHTML=`<div class="dialog"><div class="sun-offer-modal-head"><div><h2>Предложение клиенту</h2><p class="hint" id="sunOfferMeta"></p></div><div class="spacer"></div><div id="sunOfferStale" class="sun-offer-stale">Заказ изменён — предложение будет автоматически обновлено из текущих данных.</div><div class="sun-offer-modal-actions"><button class="primary" id="sunOfferPdf" type="button">Скачать PDF</button><button class="outline" id="sunOfferEdit" type="button">Редактировать</button><button class="outline" id="sunOfferRefresh" type="button">Обновить</button><button class="outline" id="sunOfferClose" type="button">Закрыть</button></div></div><section class="sun-offer-editor" id="sunOfferEditor"><div class="sun-offer-editor-grid"><label>Название мероприятия<input data-offer-data="event" type="text"></label><label>Имя клиента<input data-offer-data="client" type="text"></label><label>Дата<input data-offer-data="date" type="date"></label><label>Гостей<input data-offer-data="guests" type="number" min="0" step="1"></label><label>Метка предложения<input data-offer-text="proposalLabel" type="text"></label><label>Заголовок меню<input data-offer-text="menuTitle" type="text"></label><label class="wide">Короткий текст первого блока<textarea data-offer-text="heroNote"></textarea></label><label>Заголовок продающего блока<input data-offer-text="salesTitle" type="text"></label><label class="wide">Продающий текст<textarea data-offer-text="salesNote"></textarea></label><label>Заголовок итогов<input data-offer-text="summaryTitle" type="text"></label><label>Заголовок стоимости<input data-offer-text="totalPriceTitle" type="text"></label><label>Заголовок блока «Всё под контролем»<input data-offer-text="controlTitle" type="text"></label><label>Блок рекомендаций<input data-offer-text="extrasTitle" type="text"></label><label class="wide">Пункты блока «Всё под контролем» — каждый с новой строки<textarea data-offer-list="controlLines" rows="7"></textarea></label><label>Заголовок фото на столе<input data-offer-text="finalGalleryTitle" type="text"></label><label class="wide">Примечание внизу<textarea data-offer-text="footerNote"></textarea></label><div class="sun-offer-editor-gallery wide"><div><b>Фото «Как это выглядит на вашем столе»</b><small>В PDF автоматически попадёт 1 или 2 фото только если на последней странице есть свободное место.</small></div><div class="sun-offer-editor-gallery-images"><img src="offer-gallery/001.jpg" alt=""><img src="offer-gallery/002.jpg" alt=""></div></div></div><p class="sun-offer-editor-note">Изменения относятся только к этому предложению. Просмотр ниже — те же страницы, которые попадут в скачанный PDF.</p></section><div class="sun-offer-frame-wrap"><div id="sunClientOfferPreview" aria-label="Предпросмотр предложения"></div></div></div>`;document.body.appendChild(modal);
|
||||||
$('sunOfferClose').onclick=()=>window.closeModal?.('sunClientOfferModal');
|
$('sunOfferClose').onclick=()=>{offerRenderSeq++;offerAssetController?.abort();clearTimeout(currentOffer.editTimer);window.closeModal?.('sunClientOfferModal')};
|
||||||
$('sunOfferRefresh').onclick=refreshCurrentOffer;$('sunOfferPdf').onclick=downloadCurrentOfferPdf;$('sunOfferEdit').onclick=toggleOfferEditor;bindOfferEditor();
|
$('sunOfferRefresh').onclick=refreshCurrentOffer;$('sunOfferPdf').onclick=downloadCurrentOfferPdf;$('sunOfferEdit').onclick=toggleOfferEditor;bindOfferEditor();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1086,32 +1087,40 @@ window.SUN_LEGACY_CATALOG_V175=[];
|
|||||||
|
|
||||||
const SAFE_PIXEL='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Y9Zwv8AAAAASUVORK5CYII=';
|
const SAFE_PIXEL='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Y9Zwv8AAAAASUVORK5CYII=';
|
||||||
const isSafeRasterDataUrl=src=>/^data:image\/(?!svg\+xml)[a-z0-9.+-]+;/i.test(String(src||''));
|
const isSafeRasterDataUrl=src=>/^data:image\/(?!svg\+xml)[a-z0-9.+-]+;/i.test(String(src||''));
|
||||||
async function asDataUrlOne(src){
|
async function asDataUrlOne(src,{signal}={}){
|
||||||
if(!src)return'';
|
if(!src||signal?.aborted)return '';
|
||||||
const raw=window.SunSafe.imageAssetSrc(src);
|
const raw=window.SunSafe.imageAssetSrc(src);
|
||||||
if(isSafeRasterDataUrl(raw))return raw;
|
if(isSafeRasterDataUrl(raw))return raw;
|
||||||
if(/^data:/i.test(raw))return'';
|
if(/^data:/i.test(raw))return '';
|
||||||
|
const controller=new AbortController(),cancel=()=>controller.abort();
|
||||||
|
signal?.addEventListener('abort',cancel,{once:true});
|
||||||
|
const timer=setTimeout(cancel,6000);
|
||||||
try{
|
try{
|
||||||
const url=new URL(raw,document.baseURI||location.href);
|
const url=new URL(raw,document.baseURI||location.href);
|
||||||
if(!['http:','https:','blob:'].includes(url.protocol))return'';
|
if(!['http:','https:','blob:'].includes(url.protocol))return '';
|
||||||
const sameOrigin=url.protocol==='blob:'||url.origin===location.origin;
|
const sameOrigin=url.protocol==='blob:'||url.origin===location.origin;
|
||||||
const res=await fetch(url,{mode:'cors',credentials:sameOrigin?'same-origin':'omit',cache:'force-cache'});
|
const read=async()=>{
|
||||||
|
const res=await fetch(url,{mode:'cors',credentials:sameOrigin?'same-origin':'omit',cache:'force-cache',signal:controller.signal});
|
||||||
if(!res.ok)throw new Error(`HTTP ${res.status}`);
|
if(!res.ok)throw new Error(`HTTP ${res.status}`);
|
||||||
const blob=await res.blob();
|
const blob=await res.blob();
|
||||||
if(!/^image\/(?!svg\+xml)[a-z0-9.+-]+$/i.test(String(blob.type||'')))throw new Error('Неподдерживаемый формат изображения');
|
if(!/^image\/(?!svg\+xml)[a-z0-9.+-]+$/i.test(String(blob.type||'')))return '';
|
||||||
const data=await new Promise((resolve,reject)=>{const r=new FileReader();r.onload=()=>resolve(String(r.result||''));r.onerror=()=>reject(r.error||new Error('FileReader'));r.readAsDataURL(blob)});
|
return await new Promise((resolve,reject)=>{const r=new FileReader();r.onload=()=>resolve(String(r.result||''));r.onerror=()=>reject(r.error||new Error('FileReader'));r.readAsDataURL(blob)});
|
||||||
|
};
|
||||||
|
// The explicit race also bounds a stalled response body/FileReader, not
|
||||||
|
// just receipt of HTTP headers. Aborting stops the underlying fetch.
|
||||||
|
const data=await Promise.race([read(),new Promise(resolve=>controller.signal.addEventListener('abort',()=>resolve(''),{once:true}))]);
|
||||||
return isSafeRasterDataUrl(data)?data:'';
|
return isSafeRasterDataUrl(data)?data:'';
|
||||||
}catch(err){return''}
|
}catch(_){return ''}
|
||||||
|
finally{clearTimeout(timer);signal?.removeEventListener('abort',cancel);controller.abort()}
|
||||||
}
|
}
|
||||||
async function asDataUrl(src){
|
async function asDataUrl(src,options={}){
|
||||||
const list=window.SunCatalogPhoto?.candidates?.(src)||[src];
|
const list=window.SunCatalogPhoto?.candidates?.(src)||[src];
|
||||||
for(const candidate of list){const data=await asDataUrlOne(candidate);if(data)return data;}
|
for(const candidate of list){if(options.signal?.aborted)break;const data=await asDataUrlOne(candidate,options);if(data)return data;}
|
||||||
console.warn('PDF image unavailable after all fallbacks:',src);
|
return '';
|
||||||
return'';
|
|
||||||
}
|
}
|
||||||
async function preparedSnapshot(snapshot){
|
async function preparedSnapshot(snapshot){
|
||||||
const copy=structuredClone(snapshot),liveCatalog=getBoxes(),legacyCatalog=getLegacyBoxes();
|
const copy=structuredClone(snapshot),liveCatalog=getBoxes(),legacyCatalog=getLegacyBoxes();
|
||||||
// Preview uses normal image URLs. Expensive Base64 conversion is deferred until PDF download.
|
// Resolve current company/catalog presentation before the shared preview/PDF media pass.
|
||||||
const companyBrand=window.CateriumBranding.identity();
|
const companyBrand=window.CateriumBranding.identity();
|
||||||
copy.logo=companyBrand.logo;copy.brandName=companyBrand.name;
|
copy.logo=companyBrand.logo;copy.brandName=companyBrand.name;
|
||||||
copy.brandContacts=companyBrand.contacts;copy.brandCity=companyBrand.city;
|
copy.brandContacts=companyBrand.contacts;copy.brandCity=companyBrand.city;
|
||||||
@ -1139,14 +1148,26 @@ window.SUN_LEGACY_CATALOG_V175=[];
|
|||||||
try{const cfg=offerSettingsForSnapshot(copy);copy.controlLines=Array.isArray(cfg.controlLines)&&cfg.controlLines.length?cfg.controlLines:DELIVERY_CONTROL;copy.extraServices=Array.isArray(cfg.extraServices)&&cfg.extraServices.length?cfg.extraServices:EXTRA_SERVICES.map(x=>x[0]);copy.controlTitle=cfg.texts?.controlTitle||'';copy.extrasTitle=cfg.texts?.extrasTitle||'';}catch(_){}
|
try{const cfg=offerSettingsForSnapshot(copy);copy.controlLines=Array.isArray(cfg.controlLines)&&cfg.controlLines.length?cfg.controlLines:DELIVERY_CONTROL;copy.extraServices=Array.isArray(cfg.extraServices)&&cfg.extraServices.length?cfg.extraServices:EXTRA_SERVICES.map(x=>x[0]);copy.controlTitle=cfg.texts?.controlTitle||'';copy.extrasTitle=cfg.texts?.extrasTitle||'';}catch(_){}
|
||||||
return copy;
|
return copy;
|
||||||
}
|
}
|
||||||
async function preparedPdfSnapshot(prepared){
|
async function preparedPdfSnapshot(prepared,{signal,onProgress}={}){
|
||||||
const copy=structuredClone(prepared),cache=new Map();
|
const copy=structuredClone(prepared),cache=new Map(),missing=new Set(),controller=new AbortController();
|
||||||
|
const cancel=()=>controller.abort();signal?.addEventListener('abort',cancel,{once:true});if(signal?.aborted)cancel();
|
||||||
|
// One budget for the whole batch, including all fallback candidates.
|
||||||
|
const timer=setTimeout(cancel,10000);let done=0;
|
||||||
if(copy.orderId&&window.SunOfferWorkspaceV1769?.galleryFor)copy.finalGallery=window.SunOfferWorkspaceV1769.galleryFor(copy.orderId);
|
if(copy.orderId&&window.SunOfferWorkspaceV1769?.galleryFor)copy.finalGallery=window.SunOfferWorkspaceV1769.galleryFor(copy.orderId);
|
||||||
const resolve=src=>{const key=String(src||'');if(!key)return Promise.resolve('');if(!cache.has(key))cache.set(key,asDataUrl(key));return cache.get(key);};
|
const cfg=copy.pdfSettings||offerSettingsForSnapshot(copy);if(cfg.showFinalPhoto===false)copy.finalGallery=[];
|
||||||
copy.logo=await resolve(copy.logo);
|
const resolve=src=>{
|
||||||
copy.finalGallery=(await Promise.all((copy.finalGallery||[]).map(resolve))).filter(Boolean);
|
const key=String(src||'');if(!key)return Promise.resolve('');
|
||||||
await Promise.all((copy.items||[]).map(async item=>{item.photoData=(await resolve(item.photoData||item.photo||''))||copy.logo;}));
|
if(!cache.has(key))cache.set(key,asDataUrl(key,{signal:controller.signal}).then(data=>{if(!data)missing.add(key);onProgress?.(++done,cache.size);return data}));
|
||||||
|
return cache.get(key);
|
||||||
|
};
|
||||||
|
try{
|
||||||
|
// A slow decorative gallery must not delay every product photo.
|
||||||
|
const [logo,photos,gallery]=await Promise.all([resolve(copy.logo),Promise.all((copy.items||[]).map(item=>resolve(item.photoData||item.photo||''))),Promise.all((copy.finalGallery||[]).map(resolve))]);
|
||||||
|
copy.logo=logo;copy.finalGallery=gallery.filter(Boolean);
|
||||||
|
(copy.items||[]).forEach((item,i)=>{item.photoData=photos[i]||logo;item.photo=photos[i]||''});
|
||||||
|
copy.missingPhotoCount=missing.size;
|
||||||
return copy;
|
return copy;
|
||||||
|
}finally{clearTimeout(timer);signal?.removeEventListener('abort',cancel);controller.abort()}
|
||||||
}
|
}
|
||||||
|
|
||||||
const OFFER_TEMPLATE_KEY='sunOfferTemplateV1';
|
const OFFER_TEMPLATE_KEY='sunOfferTemplateV1';
|
||||||
@ -1292,15 +1313,42 @@ window.SUN_LEGACY_CATALOG_V175=[];
|
|||||||
let currentOffer={orderId:null,snapshot:null,prepared:null,pdfPrepared:null,pages:null,pdfBlob:null,pdfPromise:null,renderToken:0,preferDraft:false,transient:false,editTimer:null};
|
let currentOffer={orderId:null,snapshot:null,prepared:null,pdfPrepared:null,pages:null,pdfBlob:null,pdfPromise:null,renderToken:0,preferDraft:false,transient:false,editTimer:null};
|
||||||
function renderOfferPages(pages){const host=$('sunClientOfferPreview');if(!host)return;host.innerHTML='';for(const page of (pages||[])){page.classList.add('sun-offer-page-canvas');page.setAttribute('aria-label','Страница предложения');host.appendChild(page)}}
|
function renderOfferPages(pages){const host=$('sunClientOfferPreview');if(!host)return;host.innerHTML='';for(const page of (pages||[])){page.classList.add('sun-offer-page-canvas');page.setAttribute('aria-label','Страница предложения');host.appendChild(page)}}
|
||||||
function offerRenderMessage(text='Обновляю предложение…'){const host=$('sunClientOfferPreview');if(host)host.innerHTML=`<div class="sun-offer-rendering">${esc(text)}</div>`}
|
function offerRenderMessage(text='Обновляю предложение…'){const host=$('sunClientOfferPreview');if(host)host.innerHTML=`<div class="sun-offer-rendering">${esc(text)}</div>`}
|
||||||
|
let offerAssetController=null;
|
||||||
|
let offerRenderTemplateId=null;
|
||||||
async function renderOffer(snapshot){
|
async function renderOffer(snapshot){
|
||||||
const renderToken=++offerRenderSeq;currentOffer.snapshot=snapshot;currentOffer.pdfBlob=null;currentOffer.pdfPromise=null;currentOffer.pages=null;offerRenderMessage();const pdfBtn=$('sunOfferPdf');if(pdfBtn){pdfBtn.disabled=true;pdfBtn.textContent='Готовлю просмотр…'}
|
offerAssetController?.abort();const controller=new AbortController();offerAssetController=controller;
|
||||||
const prepared=await preparedSnapshot(snapshot);const explicitTemplate=String(snapshot?.offerTemplateId||prepared?.offerTemplateId||'').trim();prepared.offerTemplateId=OFFER_TEMPLATE_IDS.has(explicitTemplate)?explicitTemplate:offerTemplateId();
|
const renderToken=++offerRenderSeq,current=()=>renderToken===offerRenderSeq&&!controller.signal.aborted;
|
||||||
const pdfPrepared=await preparedPdfSnapshot(prepared),pages=await renderOfferPdfPages(pdfPrepared);
|
const explicitTemplate=String(snapshot?.offerTemplateId||'').trim(),selectedTemplate=OFFER_TEMPLATE_IDS.has(explicitTemplate)?explicitTemplate:offerTemplateId();offerRenderTemplateId=selectedTemplate;
|
||||||
if(renderToken!==offerRenderSeq)return null;
|
currentOffer.snapshot=snapshot;currentOffer.pdfBlob=null;currentOffer.pdfPromise=null;currentOffer.pages=null;
|
||||||
|
offerRenderMessage('Загружаю фотографии…');
|
||||||
|
const pdfBtn=$('sunOfferPdf');if(pdfBtn){pdfBtn.disabled=true;pdfBtn.textContent='Готовлю просмотр…'}
|
||||||
|
$('sunOfferAssetWarning')?.remove();
|
||||||
|
try{
|
||||||
|
const prepared=await preparedSnapshot(snapshot);if(!current())return null;
|
||||||
|
prepared.offerTemplateId=selectedTemplate;
|
||||||
|
// Warm only the proposal's fonts while its photos load.
|
||||||
|
const fonts=window.CateriumProposalPDF?.readyFonts?.();
|
||||||
|
const pdfPrepared=await preparedPdfSnapshot(prepared,{signal:controller.signal,onProgress:(done,total)=>{if(current())offerRenderMessage(`Загружаю фотографии: ${done} из ${total}…`)}});
|
||||||
|
if(!current())return null;offerRenderMessage('Готовлю страницы предложения…');
|
||||||
|
const pages=await withTimeout(Promise.resolve(fonts).then(()=>renderOfferPdfPages(pdfPrepared)),15000,'Предпросмотр');
|
||||||
|
if(!current())return null;
|
||||||
currentOffer.snapshot=snapshot;currentOffer.prepared=prepared;currentOffer.pdfPrepared=pdfPrepared;currentOffer.pages=pages;currentOffer.renderToken=renderToken;
|
currentOffer.snapshot=snapshot;currentOffer.prepared=prepared;currentOffer.pdfPrepared=pdfPrepared;currentOffer.pages=pages;currentOffer.renderToken=renderToken;
|
||||||
renderOfferPages(pages);syncOfferEditor();
|
renderOfferPages(pages);syncOfferEditor();
|
||||||
|
if(pdfPrepared.missingPhotoCount){
|
||||||
|
const notice=document.createElement('div');notice.id='sunOfferAssetWarning';notice.className='sun-offer-stale on';notice.setAttribute('role','status');
|
||||||
|
notice.textContent=`Не загрузились фотографии: ${pdfPrepared.missingPhotoCount}. Предложение открыто с доступными изображениями. Нажмите «Обновить», чтобы повторить загрузку фото.`;
|
||||||
|
$('sunClientOfferPreview')?.before(notice);
|
||||||
|
}
|
||||||
if(pdfBtn){pdfBtn.disabled=false;pdfBtn.textContent='Скачать PDF'}
|
if(pdfBtn){pdfBtn.disabled=false;pdfBtn.textContent='Скачать PDF'}
|
||||||
return pages;
|
return pages;
|
||||||
|
}catch(error){
|
||||||
|
if(!current())return null;
|
||||||
|
console.error('Offer preview failed:',error);
|
||||||
|
currentOffer.prepared=null;currentOffer.pdfPrepared=null;currentOffer.pages=null;
|
||||||
|
offerRenderMessage('Не удалось подготовить предложение. Нажмите «Обновить», чтобы повторить.');
|
||||||
|
if(pdfBtn){pdfBtn.disabled=true;pdfBtn.textContent='PDF недоступен'}
|
||||||
|
return null;
|
||||||
|
}finally{controller.abort();if(offerAssetController===controller)offerAssetController=null}
|
||||||
}
|
}
|
||||||
function toggleOfferEditor(){const panel=$('sunOfferEditor'),btn=$('sunOfferEdit');if(!panel)return;const on=!panel.classList.contains('on');panel.classList.toggle('on',on);if(btn)btn.textContent=on?'Скрыть редактор':'Редактировать';if(on)syncOfferEditor()}
|
function toggleOfferEditor(){const panel=$('sunOfferEditor'),btn=$('sunOfferEdit');if(!panel)return;const on=!panel.classList.contains('on');panel.classList.toggle('on',on);if(btn)btn.textContent=on?'Скрыть редактор':'Редактировать';if(on)syncOfferEditor()}
|
||||||
function offerOverrideTextValue(key){const over=currentOffer.snapshot?.offerOverrides?.texts||{},cfg=offerSettings();return Object.prototype.hasOwnProperty.call(over,key)?String(over[key]??''):String(cfg.texts?.[key]??'')}
|
function offerOverrideTextValue(key){const over=currentOffer.snapshot?.offerOverrides?.texts||{},cfg=offerSettings();return Object.prototype.hasOwnProperty.call(over,key)?String(over[key]??''):String(cfg.texts?.[key]??'')}
|
||||||
@ -1334,10 +1382,14 @@ window.SUN_LEGACY_CATALOG_V175=[];
|
|||||||
// always receive the fresh snapshot so every template/PDF opens consistently.
|
// always receive the fresh snapshot so every template/PDF opens consistently.
|
||||||
if(!draftDiff&&!persistSnapshot(saved.id,snapshot))return;
|
if(!draftDiff&&!persistSnapshot(saved.id,snapshot))return;
|
||||||
}
|
}
|
||||||
|
// Older snapshots can predate the per-order template field. Resolve it
|
||||||
|
// before rendering instead of waiting for the asynchronously mounted picker.
|
||||||
|
if(!OFFER_TEMPLATE_IDS.has(snapshot.offerTemplateId))snapshot={...snapshot,offerTemplateId:OFFER_TEMPLATE_IDS.has(order.clientOfferTemplateId)?order.clientOfferTemplateId:offerTemplateId()};
|
||||||
currentOffer={orderId:saved.id,snapshot,prepared:null,pdfPrepared:null,pages:null,pdfBlob:null,pdfPromise:null,renderToken:0,preferDraft:Boolean(preferDraft),transient:draftDiff,editTimer:null};
|
currentOffer={orderId:saved.id,snapshot,prepared:null,pdfPrepared:null,pages:null,pdfBlob:null,pdfPromise:null,renderToken:0,preferDraft:Boolean(preferDraft),transient:draftDiff,editTimer:null};
|
||||||
setMeta(order,snapshot,{transient:draftDiff,autoRefreshed});
|
setMeta(order,snapshot,{transient:draftDiff,autoRefreshed});
|
||||||
window.modal?.('sunClientOfferModal');await renderOffer(snapshot);
|
window.modal?.('sunClientOfferModal');const pages=await renderOffer(snapshot);
|
||||||
if(autoRefreshed)toast('Предложение автоматически обновлено по текущему заказу.');
|
if(pages&&autoRefreshed)toast('Предложение автоматически обновлено по текущему заказу.');
|
||||||
|
return pages;
|
||||||
}
|
}
|
||||||
async function openCurrentDraftOffer(){
|
async function openCurrentDraftOffer(){
|
||||||
const d=getDraft();if(!d?.id)return alert('Сначала сохраните заказ. После сохранения появится предложение клиенту.');
|
const d=getDraft();if(!d?.id)return alert('Сначала сохраните заказ. После сохранения появится предложение клиенту.');
|
||||||
@ -1346,8 +1398,8 @@ window.SUN_LEGACY_CATALOG_V175=[];
|
|||||||
}
|
}
|
||||||
async function refreshCurrentOffer(){
|
async function refreshCurrentOffer(){
|
||||||
if(!currentOffer.orderId)return;
|
if(!currentOffer.orderId)return;
|
||||||
await openOfferForOrder(currentOffer.orderId,{forceRefresh:true,preferDraft:currentOffer.preferDraft});
|
const pages=await openOfferForOrder(currentOffer.orderId,{forceRefresh:true,preferDraft:currentOffer.preferDraft});
|
||||||
toast('Предложение обновлено.');
|
if(pages)toast('Предложение обновлено.');
|
||||||
}
|
}
|
||||||
async function ensureCurrentOfferFresh(){
|
async function ensureCurrentOfferFresh(){
|
||||||
if(!currentOffer.orderId)return false;
|
if(!currentOffer.orderId)return false;
|
||||||
@ -1679,7 +1731,7 @@ window.SUN_LEGACY_CATALOG_V175=[];
|
|||||||
}
|
}
|
||||||
const url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url;a.download=name;a.style.display='none';a.rel='noopener';document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(url),120000);return 'download';
|
const url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url;a.download=name;a.style.display='none';a.rel='noopener';document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(url),120000);return 'download';
|
||||||
}
|
}
|
||||||
function withTimeout(promise,ms,label='Операция'){return Promise.race([promise,new Promise((_,reject)=>setTimeout(()=>reject(new Error(label+' превысила время ожидания')),ms))])}
|
function withTimeout(promise,ms,label='Операция'){let timer;return Promise.race([promise,new Promise((_,reject)=>{timer=setTimeout(()=>reject(new Error(label+' превысила время ожидания')),ms)})]).finally(()=>clearTimeout(timer))}
|
||||||
function prepareCurrentOfferPdf(prepared,renderToken=currentOffer.renderToken){
|
function prepareCurrentOfferPdf(prepared,renderToken=currentOffer.renderToken){
|
||||||
const btn=$('sunOfferPdf');if(btn){btn.disabled=true;btn.textContent='Готовлю PDF…'}
|
const btn=$('sunOfferPdf');if(btn){btn.disabled=true;btn.textContent='Готовлю PDF…'}
|
||||||
let promise;
|
let promise;
|
||||||
@ -1720,8 +1772,14 @@ window.SUN_LEGACY_CATALOG_V175=[];
|
|||||||
await renderOffer(currentOffer.snapshot);
|
await renderOffer(currentOffer.snapshot);
|
||||||
}
|
}
|
||||||
|
|
||||||
const refreshOpenOffer=()=>{if(currentOffer.snapshot&¤tOffer.orderId)renderOffer(currentOffer.snapshot).catch(err=>console.warn('Offer refresh:',err))};
|
const refreshOpenOffer=()=>{if(currentOffer.snapshot&¤tOffer.orderId&&$('sunClientOfferModal')?.classList.contains('on'))renderOffer(currentOffer.snapshot).catch(err=>console.warn('Offer refresh:',err))};
|
||||||
window.addEventListener('sunoffertemplatechange',refreshOpenOffer);
|
window.addEventListener('sunoffertemplatechange',event=>{
|
||||||
|
if(!currentOffer.snapshot||!currentOffer.orderId)return;
|
||||||
|
const change=event.detail||{};if(change.perOffer&&String(change.orderId)!==String(currentOffer.orderId))return;
|
||||||
|
const order=currentSavedOrder(currentOffer.orderId),id=change.perOffer?change.id:(order?.clientOfferTemplateId||order?.clientOfferSnapshot?.offerTemplateId||offerTemplateId());
|
||||||
|
if(!OFFER_TEMPLATE_IDS.has(id)||id===offerRenderTemplateId)return;
|
||||||
|
currentOffer.snapshot={...currentOffer.snapshot,offerTemplateId:id};refreshOpenOffer();
|
||||||
|
});
|
||||||
window.addEventListener('sunclientoffersettingschange',refreshOpenOffer);
|
window.addEventListener('sunclientoffersettingschange',refreshOpenOffer);
|
||||||
window.sunOpenClientOffer=openOfferForOrder;window.sunOpenCurrentClientOffer=openCurrentDraftOffer;window.sunGetClientOfferPdfBlob=()=>currentOffer.pdfBlob;window.sunClientOfferDownload=downloadCurrentOfferPdf;window.sunClientOfferCatalogChanged=catalogPhotoChanged;window.sunClientOfferDebugHtml=offerHtml;window.sunClientOfferDebugPdf=async prepared=>buildOfferPdfBlob(await preparedPdfSnapshot(prepared));window.sunClientOfferDebugPdfPages=async prepared=>renderOfferPdfPages(await preparedPdfSnapshot(prepared));window.sunClientOfferDebugCreateSnapshot=createSnapshot;window.sunClientOfferDebugBoxCount=offerBoxCount;
|
window.sunOpenClientOffer=openOfferForOrder;window.sunOpenCurrentClientOffer=openCurrentDraftOffer;window.sunGetClientOfferPdfBlob=()=>currentOffer.pdfBlob;window.sunClientOfferDownload=downloadCurrentOfferPdf;window.sunClientOfferCatalogChanged=catalogPhotoChanged;window.sunClientOfferDebugHtml=offerHtml;window.sunClientOfferDebugPdf=async prepared=>buildOfferPdfBlob(await preparedPdfSnapshot(prepared));window.sunClientOfferDebugPdfPages=async prepared=>renderOfferPdfPages(await preparedPdfSnapshot(prepared));window.sunClientOfferDebugCreateSnapshot=createSnapshot;window.sunClientOfferDebugBoxCount=offerBoxCount;
|
||||||
(()=>{let queued=false;const run=()=>{if(queued)return;queued=true;setTimeout(()=>{queued=false;ensureButton();patchOrderViewer();},50);};[$('new'),$('orders')].filter(Boolean).forEach(root=>new MutationObserver(run).observe(root,{childList:true,subtree:true}));})();
|
(()=>{let queued=false;const run=()=>{if(queued)return;queued=true;setTimeout(()=>{queued=false;ensureButton();patchOrderViewer();},50);};[$('new'),$('orders')].filter(Boolean).forEach(root=>new MutationObserver(run).observe(root,{childList:true,subtree:true}));})();
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
(()=>{
|
(()=>{
|
||||||
'use strict';
|
'use strict';
|
||||||
const VERSION='17.7.3';
|
const VERSION='17.7.3';
|
||||||
const RELEASE='20260918-sync-demo-images-2';
|
const RELEASE='20260918-proposal-loading';
|
||||||
|
|
||||||
const hasStoredSession=()=>{try{return Object.keys(localStorage).some(k=>/^sb-.*-auth-token$/i.test(k)&&String(localStorage.getItem(k)||'').length>20)}catch(_){return false}};
|
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(){
|
function installAuthBoot(){
|
||||||
|
|||||||
@ -45,7 +45,8 @@
|
|||||||
const face=new FontFace(family,`url("${new URL('fonts/'+file,document.baseURI)}")`,{weight});
|
const face=new FontFace(family,`url("${new URL('fonts/'+file,document.baseURI)}")`,{weight});
|
||||||
try{await Promise.race([face.load(),new Promise((_,reject)=>setTimeout(()=>reject(new Error('Font timeout')),8000))]);document.fonts.add(face)}catch(e){console.warn('Proposal font fallback:',family,e.message)}
|
try{await Promise.race([face.load(),new Promise((_,reject)=>setTimeout(()=>reject(new Error('Font timeout')),8000))]);document.fonts.add(face)}catch(e){console.warn('Proposal font fallback:',family,e.message)}
|
||||||
}));
|
}));
|
||||||
await document.fonts.ready;
|
// Other app/UI webfonts may still be loading indefinitely. Only the
|
||||||
|
// three awaited proposal faces determine this canvas layout.
|
||||||
})();
|
})();
|
||||||
return fontsPromise;
|
return fontsPromise;
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -1,5 +1,5 @@
|
|||||||
const CACHE='sun-catering-pwa-v94-20260918-sync-demo-images-2';
|
const CACHE='sun-catering-pwa-v95-20260918-proposal-loading';
|
||||||
const VERSION='20260918-sync-demo-images-2';
|
const VERSION='20260918-proposal-loading';
|
||||||
const CORE=[
|
const CORE=[
|
||||||
'./','./index.html',`./core/proposal-layout.js?v=${VERSION}`,'./fonts/Manrope.ttf','./fonts/PlayfairDisplay.ttf','./fonts/PlayfairDisplay-Italic.ttf',`./core/trial-demo.js?v=${VERSION}`,`./core/cloud-transport.js?v=${VERSION}`,`./core/banquet-menu.js?v=${VERSION}`,`./core/access-policy.js?v=${VERSION}`,`./core/import-archive.js?v=${VERSION}`,`./core/company-branding.js?v=${VERSION}`,`./core/signature-offer-pdf-v18.js?v=${VERSION}`,`./core/brand-theme.js?v=${VERSION}`,
|
'./','./index.html',`./core/proposal-layout.js?v=${VERSION}`,'./fonts/Manrope.ttf','./fonts/PlayfairDisplay.ttf','./fonts/PlayfairDisplay-Italic.ttf',`./core/trial-demo.js?v=${VERSION}`,`./core/cloud-transport.js?v=${VERSION}`,`./core/banquet-menu.js?v=${VERSION}`,`./core/access-policy.js?v=${VERSION}`,`./core/import-archive.js?v=${VERSION}`,`./core/company-branding.js?v=${VERSION}`,`./core/signature-offer-pdf-v18.js?v=${VERSION}`,`./core/brand-theme.js?v=${VERSION}`,
|
||||||
`./core/sun-safe.js?v=${VERSION}`,`./core/performance.js?v=${VERSION}`,`./core/account-center-v1780.js?v=${VERSION}`,`./core/login-signature-v1776.js?v=${VERSION}`,`./core/data-layer-v1773.js?v=${VERSION}`,`./core/server-automation-v1770.js?v=${VERSION}`,`./core/hotfix-v1763.js?v=${VERSION}`,`./core/ops-ux-v1762.js?v=${VERSION}`,`./core/ux-fixes-v1764.js?v=${VERSION}`,`./core/pdf-engine.js?v=${VERSION}`,`./core/classic-offer-pdf-v1767.js?v=${VERSION}`,`./core/developer-console-v1768.js?v=${VERSION}`,`./core/offer-workspace-v1769.js?v=${VERSION}`,`./core/auth-security-v1774.js?v=${VERSION}`,`./core/order-enhancements-v1775.js?v=${VERSION}`,`./legacy/bootstrap.js?v=${VERSION}`,`./app-runtime.js?v=${VERSION}`,
|
`./core/sun-safe.js?v=${VERSION}`,`./core/performance.js?v=${VERSION}`,`./core/account-center-v1780.js?v=${VERSION}`,`./core/login-signature-v1776.js?v=${VERSION}`,`./core/data-layer-v1773.js?v=${VERSION}`,`./core/server-automation-v1770.js?v=${VERSION}`,`./core/hotfix-v1763.js?v=${VERSION}`,`./core/ops-ux-v1762.js?v=${VERSION}`,`./core/ux-fixes-v1764.js?v=${VERSION}`,`./core/pdf-engine.js?v=${VERSION}`,`./core/classic-offer-pdf-v1767.js?v=${VERSION}`,`./core/developer-console-v1768.js?v=${VERSION}`,`./core/offer-workspace-v1769.js?v=${VERSION}`,`./core/auth-security-v1774.js?v=${VERSION}`,`./core/order-enhancements-v1775.js?v=${VERSION}`,`./legacy/bootstrap.js?v=${VERSION}`,`./app-runtime.js?v=${VERSION}`,
|
||||||
|
|||||||
@ -1,4 +1,72 @@
|
|||||||
import {test,expect} from '@playwright/test';
|
import {test,expect} from '@playwright/test';
|
||||||
|
import {demo} from '../ops/demo/trial-demo-data.mjs';
|
||||||
|
|
||||||
|
test('a stalled photo cannot block the actual offer, and refresh recovers the missing image',async({page})=>{
|
||||||
|
test.setTimeout(35000);await boot(page);
|
||||||
|
await page.evaluate(d=>{
|
||||||
|
boxes=structuredClone(d.boxes.slice(0,6));orders=[{id:'slow-photo',event:'Проверка загрузки',lines:boxes.map(b=>({id:b.id,qty:1})),total:14900,status:'Новый',clientOfferTemplateId:'event-ticket'}];
|
||||||
|
const fetchOriginal=window.fetch;window.hangPhoto=true;window.photoAborted=false;
|
||||||
|
window.fetch=(input,init)=>{
|
||||||
|
if(window.hangPhoto&&String(input.url||input).includes('bruschetta-tomato.webp'))return new Promise((_,reject)=>init?.signal?.addEventListener('abort',()=>{window.photoAborted=true;reject(init.signal.reason)},{once:true}));
|
||||||
|
return fetchOriginal(input,init);
|
||||||
|
};
|
||||||
|
void window.sunOpenClientOffer('slow-photo');
|
||||||
|
},demo);
|
||||||
|
await expect(page.locator('#sunClientOfferPreview canvas').first()).toBeVisible({timeout:14000});
|
||||||
|
await expect(page.locator('#sunOfferPdf')).toBeEnabled();await expect(page.locator('#sunOfferAssetWarning')).toContainText('Не загрузились');
|
||||||
|
expect(await page.evaluate(()=>window.photoAborted)).toBe(true);
|
||||||
|
const words=await page.locator('#sunClientOfferPreview canvas').evaluateAll(cs=>cs.flatMap(c=>c.__proposalLayout).map(t=>t.text||'').join(' '));
|
||||||
|
for(const box of demo.boxes.slice(0,6))expect(words.replaceAll('\n',' ')).toContain(box.name);
|
||||||
|
await page.evaluate(()=>{window.hangPhoto=false;document.getElementById('sunOfferRefresh').click()});
|
||||||
|
await expect(page.locator('#sunOfferAssetWarning')).toBeHidden();await expect(page.locator('#sunOfferPdf')).toHaveText('Скачать PDF');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('unrelated page fonts cannot keep the proposal font loader waiting',async({page})=>{
|
||||||
|
await boot(page);
|
||||||
|
const result=await page.evaluate(async s=>{
|
||||||
|
Object.defineProperty(document.fonts,'ready',{configurable:true,get:()=>new Promise(()=>{})});
|
||||||
|
const pages=await sunClientOfferDebugPdfPages({...s,offerTemplateId:'light'});
|
||||||
|
return {pages:pages.length,fonts:[...document.fonts].filter(f=>f.family.includes('Caterium')).map(f=>f.status)};
|
||||||
|
},base);
|
||||||
|
expect(result.pages).toBeGreaterThan(0);expect(result.fonts).toEqual(['loaded','loaded','loaded']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('closing a loading offer cancels its response body and does not overwrite a later offer',async({page})=>{
|
||||||
|
await boot(page);
|
||||||
|
await page.evaluate(d=>{
|
||||||
|
boxes=structuredClone(d.boxes.slice(0,2));orders=boxes.map((b,i)=>({id:'cancel-'+i,event:'Мероприятие '+i,lines:[{id:b.id,qty:1}],total:b.price,status:'Новый',clientOfferTemplateId:i?'event-ticket':'light'}));
|
||||||
|
const fetchOriginal=window.fetch;window.bodyStarted=false;window.bodyAborted=false;
|
||||||
|
window.fetch=async(input,init)=>{
|
||||||
|
if(String(input.url||input).includes('bruschetta-tomato.webp')){
|
||||||
|
init.signal.addEventListener('abort',()=>{window.bodyAborted=true},{once:true});
|
||||||
|
return {ok:true,blob:()=>{window.bodyStarted=true;return new Promise(()=>{})}};
|
||||||
|
}
|
||||||
|
return fetchOriginal(input,init);
|
||||||
|
};
|
||||||
|
window.firstOfferDone=false;void sunOpenClientOffer('cancel-0').then(()=>{window.firstOfferDone=true});
|
||||||
|
},demo);
|
||||||
|
await expect.poll(()=>page.evaluate(()=>window.bodyStarted)).toBe(true);
|
||||||
|
await page.evaluate(()=>document.getElementById('sunOfferClose').click());
|
||||||
|
await expect.poll(()=>page.evaluate(()=>window.bodyAborted&&window.firstOfferDone)).toBe(true);
|
||||||
|
await page.evaluate(()=>sunOpenClientOffer('cancel-1'));
|
||||||
|
await expect(page.locator('#sunClientOfferPreview canvas').first()).toHaveAttribute('data-sun-proposal-template','event-ticket');
|
||||||
|
const words=await page.locator('#sunClientOfferPreview canvas').evaluateAll(cs=>cs.flatMap(c=>c.__proposalLayout).map(t=>t.text||'').join(' '));
|
||||||
|
expect(words).toContain('Мероприятие 1');expect(words).not.toContain('Мероприятие 0');
|
||||||
|
await expect(page.locator('#sunOfferPdf')).toBeEnabled();await expect(page.locator('#sunOfferAssetWarning')).toBeHidden();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a render error shows a retry action instead of leaving a disabled loading button',async({page})=>{
|
||||||
|
await boot(page);
|
||||||
|
await page.evaluate(s=>{
|
||||||
|
boxes=[{id:'fixture',name:s.items[0].name,price:12000,category:0,weight:'1200 г',photo:''}];orders=[{id:'retry-offer',event:s.event,lines:[{id:'fixture',qty:2}],total:24000,status:'Новый'}];
|
||||||
|
window.realProposalPDF=CateriumProposalPDF;window.CateriumProposalPDF={...CateriumProposalPDF,renderPages:async()=>{throw new Error('Simulated render failure')}};
|
||||||
|
void sunOpenClientOffer('retry-offer');
|
||||||
|
},base);
|
||||||
|
await expect(page.locator('#sunClientOfferPreview')).toContainText('Не удалось подготовить предложение');
|
||||||
|
await expect(page.locator('#sunOfferPdf')).not.toHaveText('Готовлю просмотр…');await expect(page.locator('#sunOfferRefresh')).toBeEnabled();
|
||||||
|
await page.evaluate(()=>{window.CateriumProposalPDF=window.realProposalPDF;document.getElementById('sunOfferRefresh').click()});
|
||||||
|
await expect(page.locator('#sunClientOfferPreview canvas').first()).toBeVisible();await expect(page.locator('#sunOfferPdf')).toBeEnabled();
|
||||||
|
});
|
||||||
|
|
||||||
async function boot(page){
|
async function boot(page){
|
||||||
await page.route('https://**',r=>r.abort());
|
await page.route('https://**',r=>r.abort());
|
||||||
@ -76,6 +144,7 @@ test('an actual offer downloads the same A4 pages shown in its preview',async({p
|
|||||||
await page.evaluate(async s=>{
|
await page.evaluate(async s=>{
|
||||||
boxes=[{id:'pdf-local',name:s.items[0].name,price:12000,category:0,weight:'1200 г',composition:s.items[0].composition,photo:''}];
|
boxes=[{id:'pdf-local',name:s.items[0].name,price:12000,category:0,weight:'1200 г',composition:s.items[0].composition,photo:''}];
|
||||||
orders=[{id:'pdf-local-order',event:s.event,contact:s.client,date:s.date,time:'18:00',guestsCount:20,lines:[{id:'pdf-local',qty:2}],total:24000,status:'Новый',clientOfferTemplateId:'event-ticket'}];
|
orders=[{id:'pdf-local-order',event:s.event,contact:s.client,date:s.date,time:'18:00',guestsCount:20,lines:[{id:'pdf-local',qty:2}],total:24000,status:'Новый',clientOfferTemplateId:'event-ticket'}];
|
||||||
|
orders[0].clientOfferSnapshot=await sunClientOfferDebugCreateSnapshot(orders[0]);delete orders[0].clientOfferSnapshot.offerTemplateId;
|
||||||
await window.sunOpenClientOffer('pdf-local-order');
|
await window.sunOpenClientOffer('pdf-local-order');
|
||||||
},base);
|
},base);
|
||||||
await expect(page.locator('#sunClientOfferPreview canvas').first()).toHaveAttribute('data-sun-proposal-template','event-ticket');
|
await expect(page.locator('#sunClientOfferPreview canvas').first()).toHaveAttribute('data-sun-proposal-template','event-ticket');
|
||||||
@ -89,4 +158,12 @@ test('an actual offer downloads the same A4 pages shown in its preview',async({p
|
|||||||
expect((body.match(/\/Type \/Page\b/g)||[]).length).toBe(pageCount);
|
expect((body.match(/\/Type \/Page\b/g)||[]).length).toBe(pageCount);
|
||||||
expect((body.match(/\/MediaBox \[0 0 595\.28 841\.89\]/g)||[]).length).toBe(pageCount);
|
expect((body.match(/\/MediaBox \[0 0 595\.28 841\.89\]/g)||[]).length).toBe(pageCount);
|
||||||
expect(body).toContain('/Width 2400');
|
expect(body).toContain('/Width 2400');
|
||||||
|
// Cloud sync re-emits the default template even when this order's design is unchanged.
|
||||||
|
const stable=await page.evaluate(()=>{
|
||||||
|
const canvas=document.querySelector('#sunClientOfferPreview canvas');
|
||||||
|
window.dispatchEvent(new CustomEvent('sunoffertemplatechange',{detail:{id:'cream-elegance'}}));
|
||||||
|
return canvas===document.querySelector('#sunClientOfferPreview canvas')&&!document.getElementById('sunOfferPdf').disabled;
|
||||||
|
});expect(stable).toBe(true);
|
||||||
|
await page.evaluate(()=>SunUXFixV1764.persistOfferTemplate('pdf-local-order','light'));
|
||||||
|
await expect(page.locator('#sunClientOfferPreview canvas').first()).toHaveAttribute('data-sun-proposal-template','light');
|
||||||
});
|
});
|
||||||
|
|||||||
@ -14,8 +14,8 @@ check(!index.includes('offer-gallery-data.js'),'blocking Base64 gallery absent')
|
|||||||
check((runtime.match(/\/Type \/Catalog/g)||[]).length===0,'runtime contains no PDF binary writer');
|
check((runtime.match(/\/Type \/Catalog/g)||[]).length===0,'runtime contains no PDF binary writer');
|
||||||
check(read('core/pdf-engine.js').includes('595.28')&&read('core/pdf-engine.js').includes('841.89'),'PDF engine uses A4 MediaBox');
|
check(read('core/pdf-engine.js').includes('595.28')&&read('core/pdf-engine.js').includes('841.89'),'PDF engine uses A4 MediaBox');
|
||||||
check([...index.matchAll(/@page\{([^}]*)\}/g)].every(m=>/size:A4/i.test(m[1])),'compact @page rules use A4');
|
check([...index.matchAll(/@page\{([^}]*)\}/g)].every(m=>/size:A4/i.test(m[1])),'compact @page rules use A4');
|
||||||
check(sw.includes('v94-20260918-sync-demo-images-2')&&sw.includes('data-layer-v1773.js')&&sw.includes('server-automation-v1770.js')&&sw.includes('offer-workspace-v1769.js'),'service worker cache is v17.7.3');
|
check(sw.includes('v95-20260918-proposal-loading')&&sw.includes('data-layer-v1773.js')&&sw.includes('server-automation-v1770.js')&&sw.includes('offer-workspace-v1769.js'),'service worker cache is v17.7.3');
|
||||||
check(index.includes('20260918-sync-demo-images-2')&&index.includes('classic-offer-pdf-v1767.js')&&!index.includes('20260907-v17-6-0-stability-security'),'index cache-busting points to v17.7.3');
|
check(index.includes('20260918-proposal-loading')&&index.includes('classic-offer-pdf-v1767.js')&&!index.includes('20260907-v17-6-0-stability-security'),'index cache-busting points to v17.7.3');
|
||||||
check(performance.includes('SunAttachmentGuard')&&performance.includes('TARGET=2*1024*1024'),'chat photo auto-compression is versioned');
|
check(performance.includes('SunAttachmentGuard')&&performance.includes('TARGET=2*1024*1024'),'chat photo auto-compression is versioned');
|
||||||
check(performance.includes("rpc('sun_dev_dashboard')")&&performance.includes('server_size')&&performance.includes('storage_size'),'Developer Console server/storage counters are versioned');
|
check(performance.includes("rpc('sun_dev_dashboard')")&&performance.includes('server_size')&&performance.includes('storage_size'),'Developer Console server/storage counters are versioned');
|
||||||
check(performance.includes('MEMORY_REFRESH_MS=30000')&&performance.includes('MEMORY_TIMEOUT_MS=8000')&&performance.includes('memoryPromise'),'Developer Console memory refresh is bounded');
|
check(performance.includes('MEMORY_REFRESH_MS=30000')&&performance.includes('MEMORY_TIMEOUT_MS=8000')&&performance.includes('memoryPromise'),'Developer Console memory refresh is bounded');
|
||||||
@ -39,7 +39,7 @@ check(!/sb_secret_[A-Za-z0-9_-]{20,}|service_role\s*[:=]\s*["'][A-Za-z0-9._-]{30
|
|||||||
check(lock.version===pkg.version&&lock.packages?.['']?.version===pkg.version,'package.json and package-lock.json versions match');
|
check(lock.version===pkg.version&&lock.packages?.['']?.version===pkg.version,'package.json and package-lock.json versions match');
|
||||||
check(releaseManifest.version===`v${pkg.version}`,'release manifest version matches package.json');
|
check(releaseManifest.version===`v${pkg.version}`,'release manifest version matches package.json');
|
||||||
check(releaseManifest.channel==='production','release manifest channel is production');
|
check(releaseManifest.channel==='production','release manifest channel is production');
|
||||||
check(String(releaseManifest.pwaCache||'').includes('v94-20260918-sync-demo-images-2'),'release manifest points to current PWA cache');
|
check(String(releaseManifest.pwaCache||'').includes('v95-20260918-proposal-loading'),'release manifest points to current PWA cache');
|
||||||
check(['17.6.2','17.6.3','17.6.4','17.6.5','17.6.6','17.6.7','17.6.8','17.6.9','17.7.0','17.7.1','17.7.2','17.7.3'].every(v=>fs.existsSync(path.join(root,`docs/releases/V${v}-CHANGES.txt`))),'release notes exist through v17.7.3');
|
check(['17.6.2','17.6.3','17.6.4','17.6.5','17.6.6','17.6.7','17.6.8','17.6.9','17.7.0','17.7.1','17.7.2','17.7.3'].every(v=>fs.existsSync(path.join(root,`docs/releases/V${v}-CHANGES.txt`))),'release notes exist through v17.7.3');
|
||||||
check(runtime.includes('CLOUD_RPC_TIMEOUT_MS=45000')&&runtime.includes('CLOUD_CONFLICT_MAX_RETRIES=4')&&runtime.includes('retryCount'),'cloud sync has timeout and capped exponential conflict retries');
|
check(runtime.includes('CLOUD_RPC_TIMEOUT_MS=45000')&&runtime.includes('CLOUD_CONFLICT_MAX_RETRIES=4')&&runtime.includes('retryCount'),'cloud sync has timeout and capped exponential conflict retries');
|
||||||
check(runtime.includes("const VERSION = '17.7.3'")&&runtime.includes('ERROR_DEDUPE_MS=5*60*1000')&&runtime.includes('mirrorBusy=false')&&runtime.includes('backupBusy=false'),'stability logger uses current version, dedupe and single-flight guards');
|
check(runtime.includes("const VERSION = '17.7.3'")&&runtime.includes('ERROR_DEDUPE_MS=5*60*1000')&&runtime.includes('mirrorBusy=false')&&runtime.includes('backupBusy=false'),'stability logger uses current version, dedupe and single-flight guards');
|
||||||
@ -65,8 +65,8 @@ check(offerWorkspace.includes('PDF и предпросмотр')&&offerWorkspace
|
|||||||
check(offerWorkspace.includes('SunClassicOfferPDFV1767')&&offerWorkspace.includes('finalGallery=galleryFor'),'custom gallery is injected into PDF renderer');
|
check(offerWorkspace.includes('SunClassicOfferPDFV1767')&&offerWorkspace.includes('finalGallery=galleryFor'),'custom gallery is injected into PDF renderer');
|
||||||
check(releaseManifest.offerWorkspaceTabs===true&&releaseManifest.offerTemplatesSeparateTab===true&&releaseManifest.offerTwoCustomGalleryPhotos===true,'release manifest records offer workspace changes');
|
check(releaseManifest.offerWorkspaceTabs===true&&releaseManifest.offerTemplatesSeparateTab===true&&releaseManifest.offerTwoCustomGalleryPhotos===true,'release manifest records offer workspace changes');
|
||||||
check(pkg.version==='17.7.3','package version is v17.7.3');
|
check(pkg.version==='17.7.3','package version is v17.7.3');
|
||||||
check(index.includes('20260918-sync-demo-images-2'),'index cache bust is v17.7.3');
|
check(index.includes('20260918-proposal-loading'),'index cache bust is v17.7.3');
|
||||||
check(sw.includes('v94-20260918-sync-demo-images-2')&&sw.includes('data-layer-v1773.js')&&sw.includes('server-automation-v1770.js'),'PWA caches v17.7.3 client foundation modules');
|
check(sw.includes('v95-20260918-proposal-loading')&&sw.includes('data-layer-v1773.js')&&sw.includes('server-automation-v1770.js'),'PWA caches v17.7.3 client foundation modules');
|
||||||
check(fs.existsSync(path.join(root,'public/core/data-layer-v1773.js'))&&fs.existsSync(path.join(root,'public/core/server-automation-v1770.js')),'data layer and server automation modules exist');
|
check(fs.existsSync(path.join(root,'public/core/data-layer-v1773.js'))&&fs.existsSync(path.join(root,'public/core/server-automation-v1770.js')),'data layer and server automation modules exist');
|
||||||
check(ux.includes('CateriumServerAutomationV1770?.enabled'),'cloud browser auto completion is disabled when server automation is active');
|
check(ux.includes('CateriumServerAutomationV1770?.enabled'),'cloud browser auto completion is disabled when server automation is active');
|
||||||
check(runtime.includes("const VERSION = '17.7.3'")&&runtime.includes("v17.7.3 Clients Server Read"),'stability logger reports v17.7.3');
|
check(runtime.includes("const VERSION = '17.7.3'")&&runtime.includes("v17.7.3 Clients Server Read"),'stability logger reports v17.7.3');
|
||||||
|
|||||||
@ -28,8 +28,8 @@ if(current!==113)fail(`current catalog photo count ${current}, expected 113`);el
|
|||||||
if(legacyCount!==60)fail(`legacy catalog photo count ${legacyCount}, expected 60`);else ok('60 legacy catalog photos');
|
if(legacyCount!==60)fail(`legacy catalog photo count ${legacyCount}, expected 60`);else ok('60 legacy catalog photos');
|
||||||
const gallery=fs.readdirSync(path.join(pub,'offer-gallery')).filter(x=>/\.jpg$/i.test(x));
|
const gallery=fs.readdirSync(path.join(pub,'offer-gallery')).filter(x=>/\.jpg$/i.test(x));
|
||||||
if(gallery.length!==2)fail(`offer gallery contains ${gallery.length} jpg files, expected 2`);else ok('offer gallery trimmed');
|
if(gallery.length!==2)fail(`offer gallery contains ${gallery.length} jpg files, expected 2`);else ok('offer gallery trimmed');
|
||||||
if(!sw.includes('20260918-sync-demo-images-2')||!sw.includes('login-signature-v1776.js')||!sw.includes('data-layer-v1773.js')||!sw.includes('server-automation-v1770.js')||!sw.includes('offer-workspace-v1769.js')||sw.includes('offer-gallery-data.js'))fail('service worker cache is stale');else ok('PWA cache updated for login refresh');
|
if(!sw.includes('20260918-proposal-loading')||!sw.includes('login-signature-v1776.js')||!sw.includes('data-layer-v1773.js')||!sw.includes('server-automation-v1770.js')||!sw.includes('offer-workspace-v1769.js')||sw.includes('offer-gallery-data.js'))fail('service worker cache is stale');else ok('PWA cache updated for login refresh');
|
||||||
if(html.includes('20260907-v17-6-0-stability-security')||html.includes('20260909-v17-7-3-clients-server-read')||!html.includes('20260918-sync-demo-images-2')||!html.includes('classic-offer-pdf-v1767.js'))fail('index still serves stale core asset version');else ok('index cache-busting is current');
|
if(html.includes('20260907-v17-6-0-stability-security')||html.includes('20260909-v17-7-3-clients-server-read')||!html.includes('20260918-proposal-loading')||!html.includes('classic-offer-pdf-v1767.js'))fail('index still serves stale core asset version');else ok('index cache-busting is current');
|
||||||
if(!performance.includes('SunAttachmentGuard')||!performance.includes('MAX_SIDE=2048'))fail('chat photo compression guard missing');else ok('chat photo compression guard present');
|
if(!performance.includes('SunAttachmentGuard')||!performance.includes('MAX_SIDE=2048'))fail('chat photo compression guard missing');else ok('chat photo compression guard present');
|
||||||
if(!performance.includes("rpc('sun_dev_dashboard')")||!performance.includes('storage_size')||!performance.includes('server_size'))fail('Developer Console memory counters missing');else ok('Developer Console memory counters present');
|
if(!performance.includes("rpc('sun_dev_dashboard')")||!performance.includes('storage_size')||!performance.includes('server_size'))fail('Developer Console memory counters missing');else ok('Developer Console memory counters present');
|
||||||
if(performance.includes('records.forEach(r=>r.addedNodes.forEach(n=>{if(n.nodeType===1)scan(n)}));enhanceDeveloperMemory()'))fail('Developer Console memory refresh is still coupled to MutationObserver');else ok('Developer Console memory refresh loop removed');
|
if(performance.includes('records.forEach(r=>r.addedNodes.forEach(n=>{if(n.nodeType===1)scan(n)}));enhanceDeveloperMemory()'))fail('Developer Console memory refresh is still coupled to MutationObserver');else ok('Developer Console memory refresh loop removed');
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user