Caterium v17.6.7 — restore classic client proposal PDFs
Restore presentation-style client proposal PDFs with 10 classic designs, 3 hidden archive templates, and exact preview/download canvas parity.
This commit is contained in:
parent
cd776d5647
commit
a3616b7387
@ -1,6 +1,6 @@
|
||||
{
|
||||
"app": "Caterium",
|
||||
"version": "v17.6.6",
|
||||
"version": "v17.6.7",
|
||||
"channel": "production",
|
||||
"schema": 17,
|
||||
"legacyStateKept": true,
|
||||
@ -11,7 +11,7 @@
|
||||
"serverReady": true,
|
||||
"workspaceAutoDiscovery": true,
|
||||
"invitesTemporarilyDisabled": false,
|
||||
"pwaCache": "v71-20260908-v17-6-6-offer-templates",
|
||||
"pwaCache": "v72-20260908-v17-6-7-classic-offer-pdf",
|
||||
"fullOfferDescriptions": true,
|
||||
"dynamicOfferRows": true,
|
||||
"pdfOfferDescriptionFix": true,
|
||||
@ -49,9 +49,18 @@
|
||||
"settingsCloudMovedToBottom": false,
|
||||
"clientOfferTemplates": [
|
||||
"light",
|
||||
"editorial-grid",
|
||||
"midnight-glass",
|
||||
"emerald-gold"
|
||||
"editorial-grid",
|
||||
"warm-sun",
|
||||
"bento-cards",
|
||||
"event-story",
|
||||
"black-gold",
|
||||
"personal-letter",
|
||||
"event-ticket",
|
||||
"solar-experience",
|
||||
"midnight-compact",
|
||||
"emerald-gold",
|
||||
"neon-emerald"
|
||||
],
|
||||
"addressCandidateSelection": true,
|
||||
"addressMapConfirmation": true,
|
||||
@ -99,7 +108,7 @@
|
||||
"catalogCompositionTildaEndpoint": "getproduct",
|
||||
"catalogCompositionPremiumForceRefresh": true,
|
||||
"catalogCompositionCacheRequiresPremium": true,
|
||||
"notes": "Four proposal templates now keep the per-order choice and use distinct PDF compositions. Menu sidebar icon uses the native mask system without the yellow square.",
|
||||
"notes": "Restored the earlier presentation-style client PDF experience: 10 classic visual concepts plus 3 hidden archive templates. Preview and download are built from the exact same canvas pages.",
|
||||
"catalogPhotoSources": 113,
|
||||
"catalogPhotosStoredInCatalog": true,
|
||||
"catalogLegacyPhotosInCatalog": 60,
|
||||
@ -237,7 +246,7 @@
|
||||
"signupTrial": "14-day Full",
|
||||
"developerMfaInputSelectorFixed": true,
|
||||
"developerMfaRepeatedValidationToastsFixed": true,
|
||||
"release": "20260908-v17-6-6-offer-templates",
|
||||
"release": "20260908-v17-6-7-classic-offer-pdf",
|
||||
"registrationFlow": "email-password-confirm-company-auto-login",
|
||||
"emailConfirmationRequired": false,
|
||||
"employeeInviteLinks": false,
|
||||
@ -325,9 +334,16 @@
|
||||
"indexCacheBustCurrent": true,
|
||||
"backgroundImageMutationBatching": true,
|
||||
"opsBootWaitBounded": true,
|
||||
"offerTemplates": 4,
|
||||
"offerTemplates": 13,
|
||||
"offerTemplatePerOrder": true,
|
||||
"offerTemplateStructuralPreviews": true,
|
||||
"offerPdfDistinctLayouts": true,
|
||||
"menuNavMaskFixed": true
|
||||
"menuNavMaskFixed": true,
|
||||
"classicOfferTemplates": 10,
|
||||
"restoredHiddenOfferTemplates": [
|
||||
"midnight-compact",
|
||||
"emerald-gold",
|
||||
"neon-emerald"
|
||||
],
|
||||
"classicOfferPdfCoverPages": true
|
||||
}
|
||||
|
||||
10
docs/releases/V17.6.7-CHANGES.txt
Normal file
10
docs/releases/V17.6.7-CHANGES.txt
Normal file
@ -0,0 +1,10 @@
|
||||
Caterium v17.6.7 — Classic client proposal PDFs
|
||||
Date: 2026-09-08
|
||||
|
||||
- Restored presentation-style client proposal PDFs based on the earlier beautiful proposal references.
|
||||
- 10 classic designs: Минимализм, Luxury Dark, Editorial Magazine, Warm Sun, Bento Cards, Event Story, Food First, Personal Letter, Event Ticket, Solar Experience.
|
||||
- Restored hidden archive templates: Midnight Compact, Emerald Gold, Neon Emerald.
|
||||
- Every classic template gets a truly different A4 cover composition, not just another palette.
|
||||
- Preview and downloaded PDF use the exact same canvas page objects.
|
||||
- Existing per-order template selection remains authoritative.
|
||||
- PWA cache v72 / 20260908-v17-6-7-classic-offer-pdf.
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "caterium-app",
|
||||
"version": "17.6.6",
|
||||
"version": "17.6.7",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "caterium-app",
|
||||
"version": "17.6.6",
|
||||
"version": "17.6.7",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.51.0",
|
||||
"http-server": "^14.1.1",
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
{
|
||||
"name": "caterium-app",
|
||||
"private": true,
|
||||
"version": "17.6.6",
|
||||
"version": "17.6.7",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"check:syntax": "node --check public/app-runtime.js && node --check public/service-worker.js && node --check public/legacy/bootstrap.js && node --check public/core/sun-safe.js && node --check public/core/performance.js && node --check public/core/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",
|
||||
"check:syntax": "node --check public/app-runtime.js && node --check public/service-worker.js && node --check public/legacy/bootstrap.js && node --check public/core/sun-safe.js && node --check public/core/performance.js && node --check public/core/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",
|
||||
"test:static": "node tests/static-security.mjs",
|
||||
"check:release": "node tests/release-check.mjs",
|
||||
"check:deploy": "npm run check:syntax && npm run test:static && npm run check:release",
|
||||
|
||||
@ -1026,7 +1026,7 @@ window.SUN_LEGACY_CATALOG_V175=[{"id":"1","name":"Фуршетный бокс
|
||||
}
|
||||
|
||||
const OFFER_TEMPLATE_KEY='sunOfferTemplateV1';
|
||||
const OFFER_TEMPLATE_IDS=new Set(['light','editorial-grid','midnight-glass','emerald-gold']);
|
||||
const OFFER_TEMPLATE_IDS=new Set(['light','midnight-glass','editorial-grid','warm-sun','bento-cards','event-story','black-gold','personal-letter','event-ticket','solar-experience','midnight-compact','emerald-gold','neon-emerald']);
|
||||
function offerTemplateId(){
|
||||
try{
|
||||
const api=window.SunOfferTemplate?.get?.();
|
||||
@ -1291,7 +1291,7 @@ window.SUN_LEGACY_CATALOG_V175=[{"id":"1","name":"Фуршетный бокс
|
||||
};
|
||||
return themes[id]||themes.light;
|
||||
}
|
||||
async function renderOfferPdfPages(s){
|
||||
async function renderOfferPdfPagesBase(s){
|
||||
const id=s.offerTemplateId||offerTemplateId(),W=1000,H=1414,SCALE=1.6,M=52,T=pdfThemeFor(id),{BG,PANEL,LINE,TEXT,MUTED,GOLD,GOLD2,GREEN,GREEN_DEEP,GREEN_SOFT,FOOT}=T;
|
||||
const cfg=offerSettingsForSnapshot(s),items=s.items||[],imageMap=new Map();
|
||||
for(const item of items)imageMap.set(item.id,await pdfLoadImage(item.photoData||s.logo||''));
|
||||
@ -1538,6 +1538,11 @@ window.SUN_LEGACY_CATALOG_V175=[{"id":"1","name":"Фуршетный бокс
|
||||
canvases.forEach((c,i)=>{const x=c.getContext('2d');x.setTransform(SCALE,0,0,SCALE,0,0);x.save();x.fillStyle=FOOT;x.fillRect(0,H-52,W,52);x.strokeStyle=LINE;x.lineWidth=1;x.beginPath();x.moveTo(0,H-52.5);x.lineTo(W,H-52.5);x.stroke();pdfText(x,'Солнце Кейтеринг · Санкт-Петербург',M,H-36,620,22,{font:'12px Arial',color:MUTED,maxLines:1});pdfText(x,`${i+1} / ${canvases.length}`,W-M,H-36,100,22,{font:'12px Arial',color:MUTED,align:'right',maxLines:1});x.restore()});
|
||||
return canvases;
|
||||
}
|
||||
async function renderOfferPdfPages(s){
|
||||
const classic=window.SunClassicOfferPDFV1767;
|
||||
if(classic?.renderPages)return classic.renderPages(s,renderOfferPdfPagesBase);
|
||||
return renderOfferPdfPagesBase(s);
|
||||
}
|
||||
async function pdfBlobFromPages(canvases){const jpeg=[];for(const c of (canvases||[]))jpeg.push(await pdfCanvasJpeg(c));return pdfFromJpegs(jpeg)}
|
||||
async function buildOfferPdfBlob(s){return pdfBlobFromPages(await renderOfferPdfPages(s))}
|
||||
async function triggerPdfDownload(blob,name){
|
||||
@ -4100,10 +4105,19 @@ window.SUN_LEGACY_CATALOG_V175=[{"id":"1","name":"Фуршетный бокс
|
||||
const KEY='sunOfferTemplateV1';
|
||||
const DEFAULT_ID='light';
|
||||
const TEMPLATES=[
|
||||
{id:'light',name:'Светлый фирменный',desc:'Классическая A4-композиция: обложка, карточки меню, итог и условия.',thumb:'offer-templates/thumb-light.jpg'},
|
||||
{id:'editorial-grid',name:'Солнце Editorial',desc:'Журнальная композиция: крупный первый экран, меню-сетка, фотоблоки и редакционная подача.',thumb:'offer-templates/thumb-editorial-grid.jpg'},
|
||||
{id:'midnight-glass',name:'Midnight Glass',desc:'Тёмная презентация: стеклянные панели, итог до меню и контрастная двухколоночная композиция.',thumb:'offer-templates/thumb-midnight-glass.jpg'},
|
||||
{id:'emerald-gold',name:'Emerald Gold',desc:'Премиальный буклет: золотой итог на первом экране, условия и меню как финальная часть.',thumb:'offer-templates/thumb-emerald-gold.jpg'}
|
||||
{id:'light',name:'Минимализм',desc:'Светлая премиальная классика: крупная обложка, фотография, факты события и чистая сетка.',thumb:'offer-templates/thumb-light.jpg'},
|
||||
{id:'midnight-glass',name:'Luxury Dark',desc:'Тёмная ресторанная презентация с золотыми акцентами и крупной food-фотографией.',thumb:'offer-templates/thumb-midnight-glass.jpg'},
|
||||
{id:'editorial-grid',name:'Editorial Magazine',desc:'Редакционный журнальный макет с нумерацией разделов, крупной типографикой и фотополосой.',thumb:'offer-templates/thumb-editorial-grid.jpg'},
|
||||
{id:'warm-sun',name:'Warm Sun',desc:'Тёплый фирменный дизайн в солнечной палитре с мягкими формами и сценарием подготовки.',thumb:'offer-templates/thumb-light.jpg'},
|
||||
{id:'bento-cards',name:'Bento Cards',desc:'Современная карточная композиция: метрики, фото, преимущества и итог собраны в bento-сетку.',thumb:'offer-templates/thumb-light.jpg'},
|
||||
{id:'event-story',name:'Event Story',desc:'Предложение как история события: таймлайн от заявки до готового стола и большая фотография.',thumb:'offer-templates/thumb-editorial-grid.jpg'},
|
||||
{id:'black-gold',name:'Food First',desc:'Максимальный акцент на еде: полноэкранное фото, чёрный фон и золотая ресторанная подача.',thumb:'offer-templates/thumb-midnight-glass.jpg'},
|
||||
{id:'personal-letter',name:'Personal Letter',desc:'Персональное предложение в форме красивого письма клиенту с мягкой типографикой.',thumb:'offer-templates/thumb-light.jpg'},
|
||||
{id:'event-ticket',name:'Event Ticket',desc:'Дизайн приглашения/билета: данные события, меню, итог и декоративный ticket-блок.',thumb:'offer-templates/thumb-editorial-grid.jpg'},
|
||||
{id:'solar-experience',name:'Solar Experience',desc:'Яркий фирменный солнечный макет с орбитальной композицией и сильным первым экраном.',thumb:'offer-templates/thumb-light.jpg'},
|
||||
{id:'midnight-compact',name:'Midnight Compact · архив',desc:'Возвращённый скрытый компактный тёмный шаблон.',thumb:'offer-templates/thumb-midnight-glass.jpg'},
|
||||
{id:'emerald-gold',name:'Emerald Gold · архив',desc:'Возвращённый премиальный изумрудно-золотой шаблон.',thumb:'offer-templates/thumb-emerald-gold.jpg'},
|
||||
{id:'neon-emerald',name:'Neon Emerald · архив',desc:'Возвращённый скрытый неоново-изумрудный шаблон.',thumb:'offer-templates/thumb-midnight-glass.jpg'}
|
||||
];
|
||||
|
||||
const esc=window.SunSafe.escapeHTML;
|
||||
|
||||
120
public/core/classic-offer-pdf-v1767.js
Normal file
120
public/core/classic-offer-pdf-v1767.js
Normal file
@ -0,0 +1,120 @@
|
||||
(()=>{
|
||||
'use strict';
|
||||
if(window.SunClassicOfferPDFV1767)return;
|
||||
|
||||
const VERSION='17.6.7';
|
||||
const RELEASE='20260908-v17-6-7-classic-offer-pdf';
|
||||
const W=1000,H=1414,SCALE=1.6,M=54;
|
||||
const CLASSIC_IDS=['light','midnight-glass','editorial-grid','warm-sun','bento-cards','event-story','black-gold','personal-letter','event-ticket','solar-experience'];
|
||||
const ARCHIVE_IDS=['midnight-compact','emerald-gold','neon-emerald'];
|
||||
const CLASSIC_SET=new Set(CLASSIC_IDS),ARCHIVE_SET=new Set(ARCHIVE_IDS);
|
||||
const INNER_THEME={
|
||||
light:'light',
|
||||
'midnight-glass':'midnight-glass',
|
||||
'editorial-grid':'editorial-grid',
|
||||
'warm-sun':'light',
|
||||
'bento-cards':'light',
|
||||
'event-story':'editorial-grid',
|
||||
'black-gold':'black-gold',
|
||||
'personal-letter':'light',
|
||||
'event-ticket':'editorial-grid',
|
||||
'solar-experience':'light'
|
||||
};
|
||||
const PALETTES={
|
||||
light:{bg:'#fffdf8',paper:'#ffffff',ink:'#17231c',muted:'#6f746e',accent:'#c99b47',deep:'#28533a',line:'#eadfca',foot:'#fffdf8'},
|
||||
'midnight-glass':{bg:'#090b0b',paper:'#111513',ink:'#f6f1e7',muted:'#b5aea1',accent:'#d4ad62',deep:'#e3c176',line:'#5d4a2a',foot:'#090b0b'},
|
||||
'editorial-grid':{bg:'#fbf8f1',paper:'#fffdf8',ink:'#15251c',muted:'#746f64',accent:'#b79155',deep:'#1f5a42',line:'#ddd2bd',foot:'#fbf8f1'},
|
||||
'warm-sun':{bg:'#fff5d8',paper:'#fffdf5',ink:'#442d19',muted:'#8a6747',accent:'#f3ad17',deep:'#8a4d13',line:'#f0d9a6',foot:'#fff5d8'},
|
||||
'bento-cards':{bg:'#f8f7f1',paper:'#ffffff',ink:'#1d251f',muted:'#788079',accent:'#f4b423',deep:'#263f32',line:'#e5e3d9',foot:'#f8f7f1'},
|
||||
'event-story':{bg:'#f6efe4',paper:'#fffaf2',ink:'#3a2d22',muted:'#7f7064',accent:'#c78c42',deep:'#5f4a37',line:'#dfccb4',foot:'#f6efe4'},
|
||||
'black-gold':{bg:'#080807',paper:'#151412',ink:'#f6f0e5',muted:'#b9b0a2',accent:'#d0a345',deep:'#e4bd68',line:'#5a4928',foot:'#080807'},
|
||||
'personal-letter':{bg:'#f6efe0',paper:'#fffaf0',ink:'#42362a',muted:'#817365',accent:'#b9844e',deep:'#604832',line:'#dfcfb5',foot:'#f6efe0'},
|
||||
'event-ticket':{bg:'#f8f3e7',paper:'#fffdf7',ink:'#172331',muted:'#68717d',accent:'#e5a91b',deep:'#173a50',line:'#d9cdbb',foot:'#f8f3e7'},
|
||||
'solar-experience':{bg:'#f6ad00',paper:'#ffd13b',ink:'#172022',muted:'#584929',accent:'#fff0a3',deep:'#102f33',line:'#e79a00',foot:'#f6ad00'}
|
||||
};
|
||||
|
||||
const money=v=>Math.round(Number(v||0)).toLocaleString('ru-RU')+' ₽';
|
||||
const classicBoxCount=s=>(s?.items||[]).filter(i=>[0,5].includes(Number(i?.categoryId))).reduce((sum,i)=>sum+Math.max(0,Number(i?.qty||0)),0);
|
||||
const dateText=value=>{if(!value)return'';const d=new Date(String(value)+'T12:00:00');return Number.isNaN(d.getTime())?String(value):d.toLocaleDateString('ru-RU',{day:'numeric',month:'long',year:'numeric'})};
|
||||
const plural=(n,a,b,c)=>{const x=Math.abs(Number(n)||0)%100,y=x%10;return x>10&&x<20?c:y===1?a:y>=2&&y<=4?b:c};
|
||||
const safeImage=src=>/^data:image\/(?:png|jpe?g|webp);base64,/i.test(String(src||''));
|
||||
function loadImage(src){return new Promise(resolve=>{if(!safeImage(src))return resolve(null);const im=new Image();let done=false;const finish=v=>{if(done)return;done=true;clearTimeout(timer);resolve(v)};const timer=setTimeout(()=>finish(null),3500);im.onload=()=>finish(im);im.onerror=()=>finish(null);im.src=String(src)})}
|
||||
function roundRect(ctx,x,y,w,h,r,fill,stroke,width=1){const rr=Math.min(r,w/2,h/2);ctx.beginPath();ctx.moveTo(x+rr,y);ctx.arcTo(x+w,y,x+w,y+h,rr);ctx.arcTo(x+w,y+h,x,y+h,rr);ctx.arcTo(x,y+h,x,y,rr);ctx.arcTo(x,y,x+w,y,rr);ctx.closePath();if(fill){ctx.fillStyle=fill;ctx.fill()}if(stroke){ctx.strokeStyle=stroke;ctx.lineWidth=width;ctx.stroke()}}
|
||||
function wrap(ctx,text,maxWidth){const words=String(text||'').trim().split(/\s+/).filter(Boolean),lines=[];let line='';for(const word of words){const test=line?line+' '+word:word;if(ctx.measureText(test).width<=maxWidth)line=test;else{if(line)lines.push(line);line=word}}if(line)lines.push(line);return lines}
|
||||
function text(ctx,value,x,y,width,lineHeight,{font='16px Arial',color='#111',align='left',maxLines=0}={}){ctx.save();ctx.font=font;ctx.fillStyle=color;ctx.textAlign=align;ctx.textBaseline='top';let lines=wrap(ctx,value,width);if(maxLines&&lines.length>maxLines){lines=lines.slice(0,maxLines);let last=lines[lines.length-1]||'';while(last&&ctx.measureText(last+'…').width>width)last=last.slice(0,-1);lines[lines.length-1]=last+'…'}lines.forEach((line,i)=>ctx.fillText(line,x,y+i*lineHeight));ctx.restore();return lines.length*lineHeight}
|
||||
function line(ctx,x1,y1,x2,y2,color,width=1,dash=[]){ctx.save();ctx.strokeStyle=color;ctx.lineWidth=width;ctx.setLineDash(dash);ctx.beginPath();ctx.moveTo(x1,y1);ctx.lineTo(x2,y2);ctx.stroke();ctx.restore()}
|
||||
function coverImage(ctx,img,x,y,w,h,r=24){if(!img)return;const s=Math.max(w/img.naturalWidth,h/img.naturalHeight),sw=w/s,sh=h/s,sx=(img.naturalWidth-sw)/2,sy=(img.naturalHeight-sh)/2;ctx.save();roundRect(ctx,x,y,w,h,r);ctx.clip();ctx.drawImage(img,sx,sy,sw,sh,x,y,w,h);ctx.restore()}
|
||||
function circleImage(ctx,img,cx,cy,r){if(!img)return;const d=r*2,s=Math.max(d/img.naturalWidth,d/img.naturalHeight),sw=d/s,sh=d/s,sx=(img.naturalWidth-sw)/2,sy=(img.naturalHeight-sh)/2;ctx.save();ctx.beginPath();ctx.arc(cx,cy,r,0,Math.PI*2);ctx.clip();ctx.drawImage(img,sx,sy,sw,sh,cx-r,cy-r,d,d);ctx.restore()}
|
||||
function drawContain(ctx,img,x,y,w,h){if(!img)return;const s=Math.min(w/img.naturalWidth,h/img.naturalHeight),dw=img.naturalWidth*s,dh=img.naturalHeight*s;ctx.drawImage(img,x+(w-dw)/2,y+(h-dh)/2,dw,dh)}
|
||||
function sun(ctx,cx,cy,r,color,rays=18){ctx.save();ctx.strokeStyle=color;ctx.fillStyle=color;ctx.lineWidth=2;for(let i=0;i<rays;i++){const a=i*Math.PI*2/rays,ri=r+8,ro=r+24+(i%2)*8;ctx.beginPath();ctx.moveTo(cx+Math.cos(a)*ri,cy+Math.sin(a)*ri);ctx.lineTo(cx+Math.cos(a)*ro,cy+Math.sin(a)*ro);ctx.stroke()}ctx.beginPath();ctx.arc(cx,cy,r,0,Math.PI*2);ctx.fill();ctx.restore()}
|
||||
function factPills(ctx,s,p,x,y,w,{dark=false}={}){const facts=[];if(s.date)facts.push(dateText(s.date));if(s.guests)facts.push(String(s.guests)+' '+plural(s.guests,'гость','гостя','гостей'));if(s.time)facts.push('Доставка '+String(s.time));facts.push(String(Math.round(classicBoxCount(s)))+' '+plural(s.boxCount,'бокс','бокса','боксов'));const gap=8,cw=(w-gap*(facts.length-1))/facts.length;facts.forEach((f,i)=>{const xx=x+i*(cw+gap);roundRect(ctx,xx,y,cw,50,14,dark?'rgba(255,255,255,.07)':p.paper,dark?'rgba(255,255,255,.22)':p.line);text(ctx,f,xx+cw/2,y+16,cw-16,18,{font:'12px Arial',color:dark?'#f7f2e7':p.ink,align:'center',maxLines:1})})}
|
||||
function totalBlock(ctx,s,p,x,y,w,h,{dark=false,accentFill=false}={}){roundRect(ctx,x,y,w,h,18,accentFill?p.accent:(dark?'rgba(10,10,10,.58)':p.paper),dark?'rgba(255,255,255,.2)':p.line);text(ctx,'ИТОГОВАЯ СТОИМОСТЬ',x+22,y+18,w-44,18,{font:'700 11px Arial',color:accentFill?p.deep:p.accent,maxLines:1});text(ctx,money(s.pricing?.total),x+22,y+48,w-44,46,{font:'500 34px Georgia',color:accentFill?p.deep:(dark?'#fff':p.deep),maxLines:1});if(s.guests)text(ctx,'на гостя: '+money((Number(s.pricing?.itemsTotal||s.pricing?.total||0))/Math.max(1,Number(s.guests))),x+22,y+96,w-44,20,{font:'12px Arial',color:accentFill?p.deep:(dark?'#ddd3c1':p.muted),maxLines:1})}
|
||||
function logoOrBrand(ctx,logo,p,{x=M,y=46,dark=false}={}){if(logo){drawContain(ctx,logo,x,y,190,64);return}sun(ctx,x+28,y+29,15,p.accent,16);text(ctx,'СОЛНЦЕ',x+60,y+11,220,28,{font:'700 24px Arial',color:dark?'#fff':p.ink,maxLines:1});text(ctx,'КЕЙТЕРИНГ',x+61,y+41,180,18,{font:'11px Arial',color:p.accent,maxLines:1})}
|
||||
function eventTitle(s){const event=String(s.event||'Персональное предложение').trim();return String(s.client||'').trim()?'Предложение для '+String(s.client).trim():event}
|
||||
function menuPreview(ctx,s,p,x,y,w,h,{dark=false,cards=4}={}){const items=(s.items||[]).slice(0,cards),gap=9,rowH=(h-gap*Math.max(0,items.length-1))/Math.max(1,items.length);items.forEach((item,i)=>{const yy=y+i*(rowH+gap);roundRect(ctx,x,yy,w,rowH,14,dark?'rgba(255,255,255,.07)':p.paper,dark?'rgba(255,255,255,.15)':p.line);text(ctx,item.name||'Позиция меню',x+16,yy+12,w-92,21,{font:dark?'600 13px Arial':'500 14px Georgia',color:dark?'#fff':p.ink,maxLines:2});text(ctx,'× '+Math.round(Number(item.qty||1)),x+w-16,yy+14,60,18,{font:'700 11px Arial',color:p.accent,align:'right',maxLines:1})})}
|
||||
function footer(ctx,p,page,total){line(ctx,M,H-48,W-M,H-48,p.line,1);text(ctx,'Солнце Кейтеринг · предложение клиенту',M,H-34,500,16,{font:'10px Arial',color:p.muted,maxLines:1});text(ctx,String(page)+' / '+String(total),W-M,H-34,100,16,{font:'10px Arial',color:p.muted,align:'right',maxLines:1})}
|
||||
|
||||
async function renderCover(s,id){
|
||||
const p=PALETTES[id]||PALETTES.light,canvas=document.createElement('canvas');canvas.width=Math.round(W*SCALE);canvas.height=Math.round(H*SCALE);const ctx=canvas.getContext('2d',{alpha:false});ctx.setTransform(SCALE,0,0,SCALE,0,0);ctx.fillStyle=p.bg;ctx.fillRect(0,0,W,H);
|
||||
const logo=await loadImage(s.logo||''),heroSrc=(s.items||[]).find(x=>safeImage(x.photoData||x.photo))?.photoData||(s.finalGallery||[]).find(safeImage)||'',hero=await loadImage(heroSrc),title=eventTitle(s),event=String(s.event||'Персональное предложение').trim();
|
||||
|
||||
if(id==='light'){
|
||||
logoOrBrand(ctx,logo,p);text(ctx,'ПЕРСОНАЛЬНОЕ ПРЕДЛОЖЕНИЕ',M,165,390,20,{font:'700 11px Arial',color:p.accent,maxLines:1});text(ctx,title,M,202,405,54,{font:'500 42px Georgia',color:p.ink,maxLines:3});text(ctx,event,M,375,405,30,{font:'500 20px Georgia',color:p.deep,maxLines:2});line(ctx,M,445,445,445,p.accent,1);text(ctx,'Меню собрано под формат вашего события, количество гостей и пожелания.',M,475,385,25,{font:'14px Arial',color:p.muted,maxLines:4});if(hero)coverImage(ctx,hero,500,150,446,600,28);factPills(ctx,s,p,M,795,W-2*M);menuPreview(ctx,s,p,M,875,565,300,{cards:4});totalBlock(ctx,s,p,640,875,306,175);text(ctx,'Всё под контролем команды Солнца',640,1080,300,26,{font:'500 19px Georgia',color:p.deep,maxLines:2});text(ctx,'Доставка · свежие продукты · аккуратная подача · готовность к сервировке',640,1140,290,22,{font:'12px Arial',color:p.muted,maxLines:4});
|
||||
}else if(id==='midnight-glass'){
|
||||
ctx.fillStyle='#080907';ctx.fillRect(0,0,W,H);if(hero)coverImage(ctx,hero,485,0,515,H,0);const g=ctx.createLinearGradient(0,0,720,0);g.addColorStop(0,'rgba(5,5,4,.99)');g.addColorStop(.72,'rgba(5,5,4,.84)');g.addColorStop(1,'rgba(5,5,4,.05)');ctx.fillStyle=g;ctx.fillRect(0,0,W,H);ctx.strokeStyle=p.accent;ctx.lineWidth=1;ctx.strokeRect(27,27,W-54,H-54);logoOrBrand(ctx,logo,p,{x:58,y:58,dark:true});text(ctx,'LUXURY CATERING',58,210,380,20,{font:'700 11px Arial',color:p.accent,maxLines:1});text(ctx,title.toUpperCase(),58,252,390,60,{font:'500 43px Georgia',color:'#fff',maxLines:4});text(ctx,event,58,500,370,28,{font:'16px Arial',color:'#d9d1c4',maxLines:3});factPills(ctx,s,p,58,615,370,{dark:true});menuPreview(ctx,s,p,58,705,370,330,{dark:true,cards:4});totalBlock(ctx,s,p,58,1070,370,170,{dark:true});text(ctx,'САНКТ-ПЕТЕРБУРГ',58,1275,280,18,{font:'10px Arial',color:p.accent,maxLines:1});
|
||||
}else if(id==='editorial-grid'){
|
||||
logoOrBrand(ctx,logo,p);text(ctx,'EDITORIAL / EVENT MENU',M,155,400,18,{font:'700 10px Arial',color:p.accent,maxLines:1});text(ctx,title,M,190,500,66,{font:'500 52px Georgia',color:p.ink,maxLines:3});line(ctx,M,405,520,405,p.ink,2);const sections=[['01','СОБЫТИЕ',event],['02','ФОРМАТ',s.guests?String(s.guests)+' гостей':'Персонально'],['03','МЕНЮ',String((s.items||[]).length)+' позиций'],['04','ДЕТАЛИ',s.time?'Доставка '+s.time:'Время согласуем']];sections.forEach((it,i)=>{const yy=455+i*115;text(ctx,it[0],M,yy,50,24,{font:'700 15px Arial',color:p.accent,maxLines:1});text(ctx,it[1],M+62,yy,170,20,{font:'700 10px Arial',color:p.muted,maxLines:1});text(ctx,it[2],M+62,yy+26,270,30,{font:'500 18px Georgia',color:p.ink,maxLines:2});line(ctx,M+60,yy+86,430,yy+86,p.line,1)});if(hero)coverImage(ctx,hero,540,170,406,670,0);menuPreview(ctx,s,p,540,875,406,245,{cards:3});totalBlock(ctx,s,p,540,1150,406,160);text(ctx,'Составлено специально для вашего события',M,1050,390,33,{font:'italic 24px Georgia',color:p.deep,maxLines:2});
|
||||
}else if(id==='warm-sun'){
|
||||
logoOrBrand(ctx,logo,p);sun(ctx,810,210,78,p.accent,24);ctx.save();ctx.globalAlpha=.18;sun(ctx,810,210,150,p.accent,28);ctx.restore();text(ctx,'ТЁПЛОЕ ПРЕДЛОЖЕНИЕ',M,180,400,20,{font:'700 11px Arial',color:p.deep,maxLines:1});text(ctx,title,M,220,475,57,{font:'500 44px Georgia',color:p.ink,maxLines:3});text(ctx,event,M,395,430,28,{font:'16px Arial',color:p.muted,maxLines:2});if(hero)coverImage(ctx,hero,520,350,426,420,72);factPills(ctx,s,p,M,805,W-2*M);const steps=['Вы выбрали формат','Мы собрали меню','Готовим в день события','Доставляем к вашему времени'];steps.forEach((st,i)=>{const x=M+i*225;ctx.beginPath();ctx.arc(x+25,930,23,0,Math.PI*2);ctx.fillStyle=p.accent;ctx.fill();text(ctx,String(i+1),x+25,919,30,18,{font:'700 12px Arial',color:'#fff',align:'center',maxLines:1});text(ctx,st,x,970,190,42,{font:'500 14px Georgia',color:p.ink,align:'center',maxLines:2});if(i<steps.length-1)line(ctx,x+55,930,x+205,930,p.accent,2)});totalBlock(ctx,s,p,M,1110,W-2*M,170,{accentFill:true});
|
||||
}else if(id==='bento-cards'){
|
||||
logoOrBrand(ctx,logo,p);text(ctx,title,M,165,620,52,{font:'700 40px Arial',color:p.ink,maxLines:2});text(ctx,event,M,270,620,25,{font:'14px Arial',color:p.muted,maxLines:2});const facts=[['ГОСТИ',s.guests||'—'],['ПОЗИЦИИ',(s.items||[]).length],['БОКСЫ',classicBoxCount(s)||0],['ДОСТАВКА',s.time||'—']];facts.forEach((it,i)=>{const x=M+i*222;roundRect(ctx,x,355,205,112,20,p.paper,p.line);text(ctx,it[0],x+18,376,160,18,{font:'700 10px Arial',color:p.muted,maxLines:1});text(ctx,String(it[1]),x+18,406,165,35,{font:'700 26px Arial',color:p.deep,maxLines:1})});if(hero)coverImage(ctx,hero,M,505,W-2*M,350,26);const cols=[['МЕНЮ','Подобрано под событие'],['КАЧЕСТВО','Свежие продукты'],['СЕРВИС','Всё готово к подаче']];cols.forEach((it,i)=>{const x=M+i*298;roundRect(ctx,x,885,280,142,20,p.paper,p.line);text(ctx,it[0],x+18,907,220,18,{font:'700 10px Arial',color:p.accent,maxLines:1});text(ctx,it[1],x+18,943,230,50,{font:'600 18px Arial',color:p.ink,maxLines:2})});totalBlock(ctx,s,p,M,1060,425,200,{accentFill:true});menuPreview(ctx,s,p,505,1060,441,200,{cards:3});
|
||||
}else if(id==='event-story'){
|
||||
logoOrBrand(ctx,logo,p);text(ctx,'ИСТОРИЯ ВАШЕГО СОБЫТИЯ',M,165,450,20,{font:'700 11px Arial',color:p.accent,maxLines:1});text(ctx,title,M,205,670,57,{font:'500 44px Georgia',color:p.ink,maxLines:3});text(ctx,event,M,385,600,28,{font:'17px Arial',color:p.muted,maxLines:2});const steps=[['01','Заявка'],['02','Меню'],['03','Готовим'],['04','Доставка'],['05','Ваш стол']];line(ctx,M+50,555,W-M-50,555,p.accent,3);steps.forEach((it,i)=>{const x=M+50+i*200;ctx.beginPath();ctx.arc(x,555,30,0,Math.PI*2);ctx.fillStyle=p.paper;ctx.fill();ctx.strokeStyle=p.accent;ctx.lineWidth=3;ctx.stroke();text(ctx,it[0],x,542,44,18,{font:'700 11px Arial',color:p.accent,align:'center',maxLines:1});text(ctx,it[1],x,603,150,24,{font:'500 15px Georgia',color:p.ink,align:'center',maxLines:1})});if(hero)coverImage(ctx,hero,M,690,W-2*M,400,26);factPills(ctx,s,p,M,1120,560);totalBlock(ctx,s,p,635,1120,311,160);
|
||||
}else if(id==='black-gold'){
|
||||
if(hero)coverImage(ctx,hero,0,0,W,H,0);const grad=ctx.createLinearGradient(0,0,0,H);grad.addColorStop(0,'rgba(0,0,0,.35)');grad.addColorStop(.55,'rgba(0,0,0,.62)');grad.addColorStop(1,'rgba(0,0,0,.94)');ctx.fillStyle=grad;ctx.fillRect(0,0,W,H);logoOrBrand(ctx,logo,p,{x:M,y:52,dark:true});text(ctx,'FOOD FIRST',M,200,400,20,{font:'700 11px Arial',color:p.accent,maxLines:1});text(ctx,title,M,240,760,70,{font:'500 54px Georgia',color:'#fff',maxLines:3});text(ctx,event,M,475,600,30,{font:'18px Arial',color:'#e2d9ca',maxLines:2});factPills(ctx,s,p,M,590,W-2*M,{dark:true});menuPreview(ctx,s,p,M,690,560,330,{dark:true,cards:4});totalBlock(ctx,s,p,640,690,306,180,{dark:true});text(ctx,'Еда — главный герой вашего события.',M,1080,650,46,{font:'italic 30px Georgia',color:'#fff',maxLines:2});text(ctx,'Соберём, приготовим и доставим всё вовремя.',M,1195,600,24,{font:'14px Arial',color:'#d6ccb9',maxLines:2});
|
||||
}else if(id==='personal-letter'){
|
||||
logoOrBrand(ctx,logo,p);roundRect(ctx,M,150,W-2*M,1110,8,p.paper,p.line);text(ctx,'Персональное письмо',M+55,205,320,20,{font:'italic 15px Georgia',color:p.accent,maxLines:1});text(ctx,String(s.client||'Здравствуйте')+',',M+55,265,520,60,{font:'italic 46px Georgia',color:p.deep,maxLines:2});text(ctx,'Мы собрали для вас предложение, в котором всё уже продумано: от состава меню до удобной доставки в день события.',M+55,390,520,32,{font:'20px Georgia',color:p.ink,maxLines:5});text(ctx,event,M+55,575,520,34,{font:'italic 25px Georgia',color:p.accent,maxLines:2});if(hero)coverImage(ctx,hero,650,230,240,360,120);line(ctx,M+55,720,W-M-55,720,p.line,1);factPills(ctx,s,p,M+55,770,W-2*M-110);menuPreview(ctx,s,p,M+55,855,520,250,{cards:3});totalBlock(ctx,s,p,620,855,270,170);text(ctx,'С теплом,\nкоманда «Солнце Кейтеринг»',M+55,1140,470,35,{font:'italic 24px Georgia',color:p.deep,maxLines:2});
|
||||
}else if(id==='event-ticket'){
|
||||
logoOrBrand(ctx,logo,p);roundRect(ctx,M,160,W-2*M,1080,28,p.paper,p.deep,2);line(ctx,695,160,695,1240,p.line,2,[8,9]);text(ctx,'EVENT PASS',M+38,205,260,20,{font:'700 11px Arial',color:p.accent,maxLines:1});text(ctx,title,M+38,250,560,58,{font:'700 43px Arial',color:p.deep,maxLines:3});text(ctx,event,M+38,425,560,28,{font:'16px Arial',color:p.muted,maxLines:2});const labels=[['DATE',dateText(s.date)||'Уточняется'],['TIME',s.time||'Уточняется'],['GUESTS',s.guests||'—'],['BOXES',classicBoxCount(s)||0]];labels.forEach((it,i)=>{const x=M+38+(i%2)*286,y=535+Math.floor(i/2)*115;roundRect(ctx,x,y,264,92,12,'#faf7ee',p.line);text(ctx,it[0],x+16,y+14,100,16,{font:'700 9px Arial',color:p.accent,maxLines:1});text(ctx,String(it[1]),x+16,y+38,225,26,{font:'600 16px Arial',color:p.ink,maxLines:2})});menuPreview(ctx,s,p,M+38,785,550,270,{cards:3});totalBlock(ctx,s,p,M+38,1080,550,130,{accentFill:true});text(ctx,'SUN / CATERING / '+String(s.date||'EVENT'),725,205,185,24,{font:'700 12px Arial',color:p.deep,maxLines:3});for(let i=0;i<42;i++){const x=735+i*4.2,w=i%5===0?3:i%3===0?2:1;ctx.fillStyle=p.ink;ctx.fillRect(x,830,w,190)}text(ctx,'№ '+String(s.id||s.orderId||'0000'),725,1040,180,24,{font:'700 15px Arial',color:p.ink,maxLines:1});text(ctx,'Покажите этот билет хорошему настроению.',725,1100,170,30,{font:'12px Arial',color:p.muted,maxLines:4});
|
||||
}else if(id==='solar-experience'){
|
||||
const grad=ctx.createLinearGradient(0,0,W,H);grad.addColorStop(0,'#ffd43d');grad.addColorStop(.5,'#f7b40b');grad.addColorStop(1,'#ef8800');ctx.fillStyle=grad;ctx.fillRect(0,0,W,H);sun(ctx,90,90,25,'#fff5c2',24);text(ctx,'СОЛНЦЕ / EXPERIENCE',135,62,360,26,{font:'700 17px Arial',color:p.deep,maxLines:1});text(ctx,title,M,210,430,64,{font:'700 48px Arial',color:p.deep,maxLines:3});text(ctx,event,M,400,390,30,{font:'16px Arial',color:'#5d4719',maxLines:2});ctx.save();ctx.strokeStyle='rgba(255,255,255,.55)';ctx.lineWidth=2;ctx.beginPath();ctx.arc(720,430,330,0,Math.PI*2);ctx.stroke();ctx.beginPath();ctx.arc(720,430,270,0,Math.PI*2);ctx.stroke();ctx.restore();circleImage(ctx,hero,735,445,215);const steps=[['01','Меню'],['02','Готовим'],['03','Доставляем']];steps.forEach((it,i)=>{const yy=650+i*115;roundRect(ctx,M,yy,330,86,43,'rgba(255,255,255,.42)','rgba(255,255,255,.6)');text(ctx,it[0],M+26,yy+18,50,24,{font:'700 15px Arial',color:p.deep,maxLines:1});text(ctx,it[1],M+86,yy+24,190,30,{font:'700 18px Arial',color:p.deep,maxLines:1})});roundRect(ctx,515,800,431,330,28,'rgba(16,47,51,.92)',null);menuPreview(ctx,s,{...p,paper:'rgba(255,255,255,.08)',line:'rgba(255,255,255,.2)',ink:'#fff',accent:'#ffd95f'},545,835,371,220,{dark:true,cards:3});text(ctx,'ИТОГО',M,1080,220,24,{font:'700 13px Arial',color:p.deep,maxLines:1});text(ctx,money(s.pricing?.total),M,1118,420,64,{font:'700 47px Arial',color:p.deep,maxLines:1});text(ctx,'Событие, которое хочется запомнить.',M,1210,600,32,{font:'700 22px Arial',color:p.deep,maxLines:2});
|
||||
}
|
||||
canvas.dataset.sunClassicTemplate=id;canvas.dataset.sunClassicCover='1';return canvas;
|
||||
}
|
||||
|
||||
function correctPageNumbers(pages,start,total,innerId){const foot=(PALETTES[innerId]||PALETTES.light).foot||'#fff';pages.forEach((canvas,i)=>{try{const scale=canvas.width/W,ctx=canvas.getContext('2d');ctx.save();ctx.setTransform(scale,0,0,scale,0,0);ctx.fillStyle=foot;ctx.fillRect(W-190,H-50,150,40);text(ctx,String(start+i)+' / '+String(total),W-M,H-34,100,16,{font:'10px Arial',color:(PALETTES[innerId]||PALETTES.light).muted,align:'right',maxLines:1});ctx.restore()}catch(_){}})}
|
||||
async function renderPages(snapshot,baseRenderer){
|
||||
if(typeof baseRenderer!=='function')throw new Error('Base PDF renderer is unavailable');
|
||||
const id=String(snapshot?.offerTemplateId||'light');
|
||||
if(ARCHIVE_SET.has(id))return baseRenderer(snapshot);
|
||||
if(!CLASSIC_SET.has(id))return baseRenderer(snapshot);
|
||||
const innerId=INNER_THEME[id]||'light',inner={...snapshot,offerTemplateId:innerId};
|
||||
const [cover,basePages]=await Promise.all([renderCover(snapshot,id),baseRenderer(inner)]),pages=[cover,...(basePages||[])],total=pages.length;
|
||||
try{const ctx=cover.getContext('2d'),p=PALETTES[id]||PALETTES.light;ctx.save();ctx.setTransform(SCALE,0,0,SCALE,0,0);footer(ctx,p,1,total);ctx.restore()}catch(_){}
|
||||
correctPageNumbers(basePages||[],2,total,innerId);
|
||||
return pages;
|
||||
}
|
||||
|
||||
function installPreviewStyles(){if(document.getElementById('sunClassicOfferPreviewV1767'))return;const style=document.createElement('style');style.id='sunClassicOfferPreviewV1767';style.textContent=`
|
||||
.sun-v1764-offer-template-grid{grid-template-columns:repeat(auto-fill,minmax(128px,1fr))!important;max-height:330px;overflow:auto;padding:2px 3px 5px}
|
||||
.sun-v1764-offer-template-grid button{min-width:0}
|
||||
.sun-offer-template-mini[data-template-mini]{position:relative;isolation:isolate}
|
||||
.sun-offer-template-mini[data-template-mini="warm-sun"] i,.sun-offer-template-mini[data-template-mini="bento-cards"] i,.sun-offer-template-mini[data-template-mini="event-story"] i,.sun-offer-template-mini[data-template-mini="black-gold"] i,.sun-offer-template-mini[data-template-mini="personal-letter"] i,.sun-offer-template-mini[data-template-mini="event-ticket"] i,.sun-offer-template-mini[data-template-mini="solar-experience"] i,.sun-offer-template-mini[data-template-mini="midnight-compact"] i,.sun-offer-template-mini[data-template-mini="neon-emerald"] i{display:none!important}
|
||||
.sun-offer-template-mini[data-template-mini="warm-sun"]{background:radial-gradient(circle at 75% 32%,#fff 0 19%,#f0ac18 20% 22%,transparent 23%),linear-gradient(#fff4d4,#ffe3a1)!important}
|
||||
.sun-offer-template-mini[data-template-mini="bento-cards"]{background:linear-gradient(90deg,transparent 7%,#fff 7% 30%,transparent 30% 35%,#fff 35% 58%,transparent 58% 63%,#ffd05a 63% 93%,transparent 93%),#f3f2ec!important}
|
||||
.sun-offer-template-mini[data-template-mini="event-story"]{background:radial-gradient(circle at 14% 45%,#c98b40 0 5%,transparent 6%),radial-gradient(circle at 34% 45%,#c98b40 0 5%,transparent 6%),radial-gradient(circle at 54% 45%,#c98b40 0 5%,transparent 6%),radial-gradient(circle at 74% 45%,#c98b40 0 5%,transparent 6%),linear-gradient(#f6efe4 0 57%,#b88d69 57% 100%)!important}
|
||||
.sun-offer-template-mini[data-template-mini="black-gold"]{background:linear-gradient(90deg,rgba(0,0,0,.94),rgba(0,0,0,.18)),linear-gradient(135deg,#664326,#1b1710)!important;border-color:#6d5429!important}
|
||||
.sun-offer-template-mini[data-template-mini="personal-letter"]{background:repeating-linear-gradient(0deg,#fff8e9 0 13%,#eadac0 14% 15%)!important}
|
||||
.sun-offer-template-mini[data-template-mini="event-ticket"]{background:linear-gradient(90deg,#fffaf0 0 70%,#173a50 70% 72%,#f1e6d2 72% 100%)!important}
|
||||
.sun-offer-template-mini[data-template-mini="solar-experience"]{background:radial-gradient(circle at 72% 44%,#fff5b9 0 23%,transparent 24%),linear-gradient(135deg,#ffd63b,#ed8a00)!important}
|
||||
.sun-offer-template-mini[data-template-mini="midnight-compact"]{background:linear-gradient(90deg,#0a1b25 0 65%,#152f3b 65% 100%)!important;border-color:#40535e!important}
|
||||
.sun-offer-template-mini[data-template-mini="neon-emerald"]{background:radial-gradient(circle at 50% 20%,#0b5037,transparent 45%),#031419!important;border-color:#2aa85a!important}
|
||||
.sun-v1767-group-label{grid-column:1/-1;font-size:10px;font-weight:900;letter-spacing:.07em;text-transform:uppercase;color:#7d858c;padding:6px 2px 1px}
|
||||
#sunClientOfferPreview .sun-offer-page-canvas[data-sun-classic-cover="1"]{box-shadow:0 18px 50px rgba(31,35,36,.16)}
|
||||
`;document.head.appendChild(style)}
|
||||
function groupPicker(){const grid=document.querySelector('.sun-v1764-offer-template-grid');if(!grid||grid.dataset.sunV1767Grouped==='1')return;const classic=[...grid.querySelectorAll('button')].filter(b=>CLASSIC_SET.has(b.dataset.v1764OfferTemplate)),archive=[...grid.querySelectorAll('button')].filter(b=>ARCHIVE_SET.has(b.dataset.v1764OfferTemplate));if(!classic.length&&!archive.length)return;grid.dataset.sunV1767Grouped='1';const label=t=>{const d=document.createElement('div');d.className='sun-v1767-group-label';d.textContent=t;return d};grid.innerHTML='';if(classic.length){grid.appendChild(label('Классические — как раньше'));classic.forEach(b=>grid.appendChild(b))}if(archive.length){grid.appendChild(label('Архивные шаблоны'));archive.forEach(b=>grid.appendChild(b))}}
|
||||
function maintain(){installPreviewStyles();groupPicker()}
|
||||
const observer=new MutationObserver(()=>setTimeout(maintain,0));if(document.documentElement)observer.observe(document.documentElement,{childList:true,subtree:true});if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',maintain,{once:true});else maintain();
|
||||
|
||||
window.SunClassicOfferPDFV1767={VERSION,RELEASE,CLASSIC_IDS:[...CLASSIC_IDS],ARCHIVE_IDS:[...ARCHIVE_IDS],renderPages,renderCover,checks:()=>({version:VERSION,classic:CLASSIC_IDS.length,archive:ARCHIVE_IDS.length,styles:Boolean(document.getElementById('sunClassicOfferPreviewV1767'))})};
|
||||
})();
|
||||
@ -1,7 +1,7 @@
|
||||
(()=>{
|
||||
'use strict';
|
||||
const VERSION='17.6.6';
|
||||
const RELEASE='20260908-v17-6-6-offer-templates';
|
||||
const VERSION='17.6.7';
|
||||
const RELEASE='20260908-v17-6-7-classic-offer-pdf';
|
||||
const critical=img=>img.closest('header,.brand,#sunCloudAuthGate,.sun-auth-gate')||img.id==='sunLoginLogo'||img.classList.contains('sun-live-catalog-logo');
|
||||
const tune=img=>{
|
||||
if(!(img instanceof HTMLImageElement)||critical(img))return;
|
||||
|
||||
@ -2,11 +2,11 @@
|
||||
'use strict';
|
||||
if(window.SunUXFixV1764)return;
|
||||
|
||||
const VERSION='17.6.6';
|
||||
const VERSION='17.6.7';
|
||||
const RELEASE_DAY='2026-09-07';
|
||||
const AUTO_DELAY_MS=60*1000;
|
||||
const AUTO_POLL_MS=15000;
|
||||
const TEMPLATE_IDS=new Set(['light','editorial-grid','midnight-glass','emerald-gold']);
|
||||
const TEMPLATE_IDS=new Set(['light','midnight-glass','editorial-grid','warm-sun','bento-cards','event-story','black-gold','personal-letter','event-ticket','solar-experience','midnight-compact','emerald-gold','neon-emerald']);
|
||||
const $=id=>document.getElementById(id);
|
||||
const qa=(s,r=document)=>[...r.querySelectorAll(s)];
|
||||
const esc=v=>window.SunSafe?.escapeHTML?window.SunSafe.escapeHTML(String(v??'')):String(v??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||||
@ -166,7 +166,7 @@
|
||||
return '<i class="mini-head"></i><i class="mini-light-hero"></i><i class="mini-light-row a"></i><i class="mini-light-row b"></i><i class="mini-light-row c"></i><i class="mini-light-total"></i>';
|
||||
}
|
||||
function pickerHtml(){
|
||||
const current=activeTemplateId();return `<div class="sun-v1764-offer-template-head"><div><b>Оформление PDF</b><small>Шаблон закрепляется за этим предложением. Миниатюра повторяет структуру PDF.</small></div></div><div class="sun-v1764-offer-template-grid">${templateList().map(t=>`<button type="button" data-v1764-offer-template="${esc(t.id)}" class="${t.id===current?'on':''}"><div class="sun-offer-template-mini sun-offer-template-mini-${esc(t.id)}" data-template-mini="${esc(t.id)}" aria-hidden="true">${offerTemplateMini(t.id)}</div><span>${esc(t.name||t.id)}</span></button>`).join('')}</div>`;
|
||||
const current=activeTemplateId();return `<div class="sun-v1764-offer-template-head"><div><b>Оформление PDF</b><small>Классические и архивные шаблоны. Предварительный просмотр совпадает со скачиваемым PDF.</small></div></div><div class="sun-v1764-offer-template-grid">${templateList().map(t=>`<button type="button" data-v1764-offer-template="${esc(t.id)}" class="${t.id===current?'on':''}"><div class="sun-offer-template-mini sun-offer-template-mini-${esc(t.id)}" data-template-mini="${esc(t.id)}" aria-hidden="true">${offerTemplateMini(t.id)}</div><span>${esc(t.name||t.id)}</span></button>`).join('')}</div>`;
|
||||
}
|
||||
function ensureOfferPicker(){
|
||||
patchOfferTemplateApi();const modal=$('sunClientOfferModal'),dialog=modal?.querySelector('.dialog');if(!dialog||!activeOfferOrderId)return false;
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -1,8 +1,8 @@
|
||||
const CACHE='sun-catering-pwa-v71-20260908-v17-6-6-offer-templates';
|
||||
const VERSION='20260908-v17-6-6-offer-templates';
|
||||
const CACHE='sun-catering-pwa-v72-20260908-v17-6-7-classic-offer-pdf';
|
||||
const VERSION='20260908-v17-6-7-classic-offer-pdf';
|
||||
const CORE=[
|
||||
'./','./index.html',
|
||||
`./core/sun-safe.js?v=${VERSION}`,`./core/performance.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}`,`./legacy/bootstrap.js?v=${VERSION}`,`./app-runtime.js?v=${VERSION}`,
|
||||
`./core/sun-safe.js?v=${VERSION}`,`./core/performance.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}`,`./legacy/bootstrap.js?v=${VERSION}`,`./app-runtime.js?v=${VERSION}`,
|
||||
'./offer-gallery/001.jpg','./offer-gallery/002.jpg',
|
||||
'./catalog/001.jpg','./catalog/002.jpg','./catalog/003.jpg',
|
||||
'./sun-logo.png','./caterium-login-logo.png','./pwa-icon-192.png','./pwa-icon-512.png','./manifest.webmanifest',
|
||||
|
||||
@ -156,7 +156,7 @@ test('Developer Console memory refresh is bounded and does not react to its own
|
||||
const result=await page.evaluate(()=>({count:window.__rpcCount,box:document.querySelectorAll('#sunDevMemoryV1761').length,version:document.getElementById('sunDevReleaseVersion')?.textContent}));
|
||||
expect(result.count).toBeLessThanOrEqual(2);
|
||||
expect(result.box).toBe(1);
|
||||
expect(result.version).toBe('17.6.6');
|
||||
expect(result.version).toBe('17.6.7');
|
||||
});
|
||||
test('mobile body does not overflow viewport', async ({ page }, testInfo) => {
|
||||
test.skip(testInfo.project.name!=='mobile-390'); await page.goto('/index.html', { waitUntil:'domcontentloaded' }); await page.waitForTimeout(500);
|
||||
@ -209,3 +209,18 @@ test('v17.6.6 proposal templates preserve selection and expose four structural p
|
||||
expect(runtimeSource).toContain("OFFER_TEMPLATE_IDS.has(explicitTemplate)");
|
||||
for(const marker of ['mini-light-hero','mini-editorial-hero','mini-midnight-total','mini-emerald-gold'])expect(runtimeSource+uxSource).toContain(marker);
|
||||
});
|
||||
|
||||
|
||||
test('v17.6.7 classic client PDF renders distinctive cover and keeps preview/download page parity', async ({ page }) => {
|
||||
await page.goto('/index.html',{waitUntil:'domcontentloaded'});
|
||||
const source=fs.readFileSync(path.join(process.cwd(),'public','core','classic-offer-pdf-v1767.js'),'utf8');
|
||||
await page.addScriptTag({content:source});
|
||||
await page.waitForFunction(()=>Boolean(window.SunClassicOfferPDFV1767),null,{timeout:5000});
|
||||
const result=await page.evaluate(async()=>{
|
||||
const c=document.createElement('canvas');c.width=1600;c.height=1000;const x=c.getContext('2d');x.fillStyle='#d9a757';x.fillRect(0,0,c.width,c.height);x.fillStyle='#173a2d';x.fillRect(500,120,700,700);const photo=c.toDataURL('image/jpeg',.8);
|
||||
const snapshot={id:'1767',client:'Анна',event:'День рождения',date:'2026-09-20',time:'18:30',guests:24,offerTemplateId:'event-ticket',pricing:{base:23517,itemsTotal:23517,total:23517},items:[{id:'a',name:'Фуршетный бокс',qty:3,categoryId:0,photoData:photo,weight:'1500 г'}]};
|
||||
const fakeBase=async()=>{const base=document.createElement('canvas');base.width=1600;base.height=2262;return [base]};
|
||||
const pages=await window.SunClassicOfferPDFV1767.renderPages(snapshot,fakeBase);return {count:pages.length,id:pages[0].dataset.sunClassicTemplate,cover:pages[0].dataset.sunClassicCover,classic:window.SunClassicOfferPDFV1767.CLASSIC_IDS.length,archive:window.SunClassicOfferPDFV1767.ARCHIVE_IDS.length};
|
||||
});
|
||||
expect(result.count).toBe(2);expect(result.id).toBe('event-ticket');expect(result.cover).toBe('1');expect(result.classic).toBe(10);expect(result.archive).toBe(3);
|
||||
});
|
||||
|
||||
@ -4,7 +4,7 @@ const root=process.cwd(), pub=path.join(root,'public');
|
||||
const read=p=>fs.readFileSync(path.join(pub,p),'utf8');
|
||||
const readRoot=p=>fs.readFileSync(path.join(root,p),'utf8');
|
||||
let bad=0;const check=(v,m)=>{console.log(`${v?'OK':'FAIL'}: ${m}`);if(!v)bad++};
|
||||
const index=read('index.html'),runtime=read('app-runtime.js'),sw=read('service-worker.js'),css=read('core/stability-v1760.css'),performance=read('core/performance.js'),ops=read('core/ops-ux-v1762.js'),hotfix=read('core/hotfix-v1763.js'),ux=read('core/ux-fixes-v1764.js');
|
||||
const index=read('index.html'),runtime=read('app-runtime.js'),sw=read('service-worker.js'),css=read('core/stability-v1760.css'),performance=read('core/performance.js'),ops=read('core/ops-ux-v1762.js'),hotfix=read('core/hotfix-v1763.js'),ux=read('core/ux-fixes-v1764.js'),classic=read('core/classic-offer-pdf-v1767.js');
|
||||
const pkg=JSON.parse(readRoot('package.json'));
|
||||
const lock=JSON.parse(readRoot('package-lock.json'));
|
||||
const releaseManifest=JSON.parse(readRoot('docs/release-manifest.json'));
|
||||
@ -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(read('core/pdf-engine.js').includes('595.28')&&read('core/pdf-engine.js').includes('841.89'),'PDF engine uses A4 MediaBox');
|
||||
check([...index.matchAll(/@page\{([^}]*)\}/g)].every(m=>/size:A4/i.test(m[1])),'compact @page rules use A4');
|
||||
check(sw.includes('v17-6-6-offer-templates')&&sw.includes('ux-fixes-v1764.js'),'service worker cache is v17.6.6');
|
||||
check(index.includes('20260908-v17-6-6-offer-templates')&&!index.includes('20260907-v17-6-0-stability-security'),'index cache-busting points to v17.6.6');
|
||||
check(sw.includes('v72-20260908-v17-6-7-classic-offer-pdf')&&sw.includes('classic-offer-pdf-v1767.js'),'service worker cache is v17.6.7');
|
||||
check(index.includes('20260908-v17-6-7-classic-offer-pdf')&&index.includes('classic-offer-pdf-v1767.js')&&!index.includes('20260907-v17-6-0-stability-security'),'index cache-busting points to v17.6.7');
|
||||
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('MEMORY_REFRESH_MS=30000')&&performance.includes('MEMORY_TIMEOUT_MS=8000')&&performance.includes('memoryPromise'),'Developer Console memory refresh is bounded');
|
||||
@ -35,18 +35,25 @@ check(ux.includes('classificationDate')&&ux.includes('sunAutoCompletedAt'),'auto
|
||||
check(ux.includes('persistOfferTemplate')&&ux.includes('clientOfferTemplateId')&&ux.includes('offerTemplateId'),'client proposal template is stored per offer');
|
||||
check(ux.includes('sunMenuIconV1766')&&ux.includes('--sun-icon:url('),'menu uses the native sidebar mask icon without a duplicate square');
|
||||
check(fs.existsSync(path.join(root,'supabase/functions/caterium-create-employee/index.ts')),'employee Edge Function source is versioned');
|
||||
check(!/sb_secret_[A-Za-z0-9_-]{20,}|service_role\s*[:=]\s*["'][A-Za-z0-9._-]{30,}/i.test(index+runtime+performance+ops+hotfix+ux),'no client secret-like token');
|
||||
check(!/sb_secret_[A-Za-z0-9_-]{20,}|service_role\s*[:=]\s*["'][A-Za-z0-9._-]{30,}/i.test(index+runtime+performance+ops+hotfix+ux+classic),'no client secret-like token');
|
||||
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.channel==='production','release manifest channel is production');
|
||||
check(String(releaseManifest.pwaCache||'').includes('v17-6-6'),'release manifest points to current PWA cache');
|
||||
check(['17.6.2','17.6.3','17.6.4','17.6.5','17.6.6'].every(v=>fs.existsSync(path.join(root,`docs/releases/V${v}-CHANGES.txt`))),'release notes exist through v17.6.6');
|
||||
check(String(releaseManifest.pwaCache||'').includes('v72-20260908-v17-6-7-classic-offer-pdf'),'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'].every(v=>fs.existsSync(path.join(root,`docs/releases/V${v}-CHANGES.txt`))),'release notes exist through v17.6.7');
|
||||
check(runtime.includes('CLOUD_RPC_TIMEOUT_MS=12000')&&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.6.5'")&&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('refreshSupportWorkspace')&&runtime.includes('sun_dev_support_snapshot'),'cloud exposes lightweight read-only support refresh');
|
||||
check(runtime.includes('DEV_ADMIN_TTL_MS=30000')&&hotfix.includes('checkPlatformAdmin?.(false)')&&hotfix.includes('},10000);'),'developer access checks are throttled');
|
||||
check(performance.includes('pendingImageRoots')&&performance.includes('queueImageScan')&&ux.includes('},5000);'),'background DOM maintenance is batched/throttled');
|
||||
check(runtime.includes('explicitTemplate')&&runtime.includes("OFFER_TEMPLATE_IDS.has(explicitTemplate)"),'per-order proposal template survives render and PDF');
|
||||
check(runtime.includes("else if(id==='midnight-glass')")&&runtime.includes("else if(id==='emerald-gold')")&&runtime.includes("if(id==='editorial-grid')"),'four proposal templates use distinct PDF layout sequences');
|
||||
check(runtime.includes("else if(id==='midnight-glass')")&&runtime.includes("else if(id==='emerald-gold')")&&runtime.includes("if(id==='editorial-grid')"),'existing proposal layout sequences remain available');
|
||||
check(runtime.includes('data-template-mini')&&ux.includes('data-template-mini'),'global and per-offer selectors show structural PDF previews');
|
||||
check(pkg.version==='17.6.7','package version is v17.6.7');
|
||||
check(classic.includes("const VERSION='17.6.7'")&&classic.includes('CLASSIC_IDS')&&classic.includes('ARCHIVE_IDS'),'classic proposal PDF module is v17.6.7');
|
||||
check(runtime.includes("'warm-sun'")&&runtime.includes("'bento-cards'")&&runtime.includes("'event-story'")&&runtime.includes("'personal-letter'")&&runtime.includes("'event-ticket'")&&runtime.includes("'solar-experience'"),'10 classic proposal designs are available');
|
||||
check(runtime.includes("'midnight-compact'")&&runtime.includes("'black-gold'")&&runtime.includes("'neon-emerald'"),'hidden archive proposal templates are restored');
|
||||
check(runtime.includes('SunClassicOfferPDFV1767')&&runtime.includes('renderOfferPdfPagesBase'),'classic renderer wraps the same PDF pages used by preview/download');
|
||||
check(classic.includes('data-sun-classic-cover')&&classic.includes('renderPages'),'classic renderer adds real A4 cover pages');
|
||||
check(releaseManifest.offerTemplates===13&&releaseManifest.classicOfferTemplates===10,'release manifest exposes 10 classic + 3 archive templates');
|
||||
if(bad)process.exit(1);
|
||||
|
||||
@ -6,7 +6,7 @@ const readPub=p=>fs.readFileSync(path.join(pub,p),'utf8');
|
||||
const readRoot=p=>fs.readFileSync(path.join(root,p),'utf8');
|
||||
const fail=m=>{console.error('FAIL:',m);process.exitCode=1};
|
||||
const ok=m=>console.log('OK:',m);
|
||||
const html=readPub('index.html'),legacy=readPub('legacy/bootstrap.js'),runtime=readPub('app-runtime.js'),safe=readPub('core/sun-safe.js'),sw=readPub('service-worker.js'),performance=readPub('core/performance.js'),ops=readPub('core/ops-ux-v1762.js'),hotfix=readPub('core/hotfix-v1763.js'),ux=readPub('core/ux-fixes-v1764.js');
|
||||
const html=readPub('index.html'),legacy=readPub('legacy/bootstrap.js'),runtime=readPub('app-runtime.js'),safe=readPub('core/sun-safe.js'),sw=readPub('service-worker.js'),performance=readPub('core/performance.js'),ops=readPub('core/ops-ux-v1762.js'),hotfix=readPub('core/hotfix-v1763.js'),ux=readPub('core/ux-fixes-v1764.js'),classic=readPub('core/classic-offer-pdf-v1767.js');
|
||||
if(!html.includes('core/sun-safe.js'))fail('SunSafe must load before legacy modules');else ok('shared SunSafe loaded');
|
||||
if(html.includes('offer-gallery-data.js')||fs.existsSync(path.join(pub,'offer-gallery-data.js')))fail('blocking offer-gallery-data.js still present');else ok('base64 gallery removed');
|
||||
for(const raw of ['<span>${b.name}</span>','<span>${o.event}</span>','<span>${o.address||','value="${x[0]}"','value="${x[2]}"']) if(legacy.includes(raw)) fail(`legacy bootstrap contains raw HTML interpolation ${raw}`);
|
||||
@ -27,8 +27,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');
|
||||
const gallery=fs.readdirSync(path.join(pub,'offer-gallery')).filter(x=>/\.jpg$/i.test(x));
|
||||
if(gallery.length!==2)fail(`offer gallery contains ${gallery.length} jpg files, expected 2`);else ok('offer gallery trimmed');
|
||||
if(!sw.includes('v17-6-6-offer-templates')||!sw.includes('ux-fixes-v1764.js')||sw.includes('offer-gallery-data.js'))fail('service worker cache is stale');else ok('PWA cache updated to v17.6.6');
|
||||
if(html.includes('20260907-v17-6-0-stability-security')||!html.includes('20260908-v17-6-6-offer-templates'))fail('index still serves stale core asset version');else ok('index cache-busting is current');
|
||||
if(!sw.includes('v17-6-7-classic-offer-pdf')||!sw.includes('classic-offer-pdf-v1767.js')||sw.includes('offer-gallery-data.js'))fail('service worker cache is stale');else ok('PWA cache updated to v17.6.7');
|
||||
if(html.includes('20260907-v17-6-0-stability-security')||!html.includes('20260908-v17-6-7-classic-offer-pdf')||!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("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');
|
||||
@ -43,5 +43,6 @@ for(const marker of ["AUTO_DELAY_MS=60*1000","order.prepayment=total","order.sta
|
||||
for(const marker of ['CLOUD_RPC_TIMEOUT_MS=12000','CLOUD_CONFLICT_MAX_RETRIES=4','refreshSupportWorkspace',"const VERSION = '17.6.5'",'ERROR_DEDUPE_MS=5*60*1000','DEV_ADMIN_TTL_MS=30000']) if(!runtime.includes(marker))fail(`v17.6.5 stability marker missing: ${marker}`);else ok(`v17.6.5 stability marker: ${marker}`);
|
||||
if(!hotfix.includes('checkPlatformAdmin?.(false)')||!hotfix.includes('},10000);'))fail('Developer fallback polling is still aggressive');else ok('Developer fallback polling is throttled');
|
||||
if(!ux.includes('sunMenuIconV1766')||!ux.includes('sun-offer-template-mini-editorial-grid')||!runtime.includes('explicitTemplate'))fail('v17.6.6 proposal/menu markers missing');else ok('v17.6.6 proposal/menu markers present');
|
||||
if(/service_role\s*[:=]\s*['"][A-Za-z0-9._-]{20,}/i.test(ops+hotfix+ux)||/eyJ[a-zA-Z0-9_-]{30,}/.test(ops+hotfix+ux))fail('possible secret in ops/hotfix/ux module');else ok('ops/hotfix/ux modules have no hard-coded secret');
|
||||
if(!classic.includes("const VERSION='17.6.7'")||!classic.includes('CLASSIC_IDS')||!classic.includes('ARCHIVE_IDS')||!classic.includes('renderPages'))fail('v17.6.7 classic PDF module missing');else ok('v17.6.7 classic PDF module present');
|
||||
if(/service_role\s*[:=]\s*['"][A-Za-z0-9._-]{20,}/i.test(ops+hotfix+ux+classic)||/eyJ[a-zA-Z0-9_-]{30,}/.test(ops+hotfix+ux+classic))fail('possible secret in ops/hotfix/ux/classic module');else ok('ops/hotfix/ux/classic modules have no hard-coded secret');
|
||||
if(process.exitCode)process.exit(process.exitCode);
|
||||
Loading…
Reference in New Issue
Block a user