From 51e2fa4c3353a47b8091f49d6081d5dcbd166ef1 Mon Sep 17 00:00:00 2001 From: pavlov346346-source Date: Wed, 16 Sep 2026 09:58:07 +0300 Subject: [PATCH] feat: give cream-elegance and neon-menu their own matching inner pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cover for these 2 templates was fully custom, but the item-list and pricing pages behind it still came from the old generic base renderer (Arial, different card style, different page chrome) — so a downloaded offer read as a nice cover stapled to a plain, differently styled document ("два предложения"). Both templates now render their own item-list pages (photo, name, category/weight, qty and line total, paginated ~10 rows/page) and a matching pricing-breakdown page (base cost, discount, promo, delivery, total, per-guest), using the same fonts, palette and card language as the cover, with a shared footer/page-number treatment across every page. The other 4 signature templates and the 13 classic ones are untouched and keep using the original base renderer. Verified locally: 8 items -> 3 pages (cover + 1 menu + pricing), 15 items -> 4 pages (cover + 2 menu + pricing), 0 items -> 3 pages, no errors, base renderer confirmed never invoked for these 2 ids. Co-Authored-By: Claude Sonnet 5 --- public/core/signature-offer-pdf-v18.js | 74 ++++++++++++++++++++++++-- 1 file changed, 71 insertions(+), 3 deletions(-) diff --git a/public/core/signature-offer-pdf-v18.js b/public/core/signature-offer-pdf-v18.js index 03d5d6d..e0197ad 100644 --- a/public/core/signature-offer-pdf-v18.js +++ b/public/core/signature-offer-pdf-v18.js @@ -151,14 +151,82 @@ } function correctPageNumbers(pages,start,total,innerId){const foot=(PALETTES[innerId]||{}).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:'#888',align:'right',maxLines:1});ctx.restore()}catch(_){}})} + + // ---- Fully-styled inner pages for the 2 polished templates, so the whole document reads as one design instead of a nice cover glued to the generic base pages. ---- + function newPage(p,dark){const 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);if(dark){ctx.save();roundRect(ctx,20,20,W-40,H-40,0,null,p.accent,1.4);ctx.restore()}return{canvas,ctx}} + function contentHeader(ctx,p,f,s,label,dark){text(ctx,String(s.brandName||'Солнце Кейтеринг').toUpperCase(),M,50,320,16,{font:`700 11px ${f.label}`,color:p.accent,maxLines:1});text(ctx,eventTitle(s),M,70,520,26,{font:`600 20px ${f.display}`,color:dark?'#fff':p.ink,maxLines:1});text(ctx,label,W-M,58,260,16,{font:`600 11px ${f.label}`,color:p.muted,align:'right',maxLines:1});line(ctx,M,108,W-M,108,p.line,1)} + async function itemRowsPage(ctx,p,f,items,y0,dark){ + const rowH=dark?96:104,gap=dark?12:14; + const imgs=await Promise.all(items.map(it=>loadImage(it.photoData||''))); + items.forEach((item,i)=>{ + const yy=y0+i*(rowH+gap),photoSize=rowH-20,px=M+10,py=yy+10,textX=px+photoSize+18,sum=Number(item.sum??(Number(item.unitPrice||0)*Number(item.qty||0))); + roundRect(ctx,M,yy,W-2*M,rowH,16,dark?'rgba(255,255,255,.04)':p.paper,dark?'rgba(255,255,255,.12)':p.line); + roundRect(ctx,px,py,photoSize,photoSize,12,p.deep,null); + if(imgs[i])coverImage(ctx,imgs[i],px,py,photoSize,photoSize,12); + text(ctx,item.name||'Позиция меню',textX,yy+14,W-2*M-(textX-M)-135,22,{font:`600 16px ${f.display}`,color:dark?'#fff':p.ink,maxLines:1}); + const meta=[item.weight?String(item.weight):'',item.categoryName?String(item.categoryName):''].filter(Boolean).join(' · '); + if(meta)text(ctx,meta,textX,yy+42,W-2*M-(textX-M)-135,16,{font:`500 12px ${f.label}`,color:p.muted,maxLines:1}); + text(ctx,'× '+Math.round(Number(item.qty||1)),W-M-16,yy+16,110,18,{font:`700 13px ${f.label}`,color:p.accent,align:'right',maxLines:1}); + text(ctx,money(sum),W-M-16,yy+rowH-36,110,18,{font:`700 16px ${f.label}`,color:dark?'#fff':p.deep,align:'right',maxLines:1}); + }); + } + function pricingPage(ctx,p,f,s,dark){ + text(ctx,'ИТОГОВАЯ СМЕТА',M,132,W-2*M,34,{font:`600 28px ${f.display}`,color:dark?'#fff':p.deep,maxLines:1}); + const pr=s.pricing||{};let y=200; + const rows=[['Стоимость позиций',Number(pr.base||0),false]]; + if(Number(pr.manual||0)>0)rows.push(['Скидка',Number(pr.manual),true]); + if(Number(pr.promo||0)>0)rows.push(['Промокод'+(s.promoCode?` «${s.promoCode}»`:''),Number(pr.promo),true]); + rows.push(['Меню после скидок',Number(pr.itemsTotal||0),false]); + rows.push(['Доставка',Number(pr.delivery||0),false]); + rows.forEach(([label,val,negative])=>{ + roundRect(ctx,M,y,W-2*M,52,14,dark?'rgba(255,255,255,.04)':p.paper,dark?'rgba(255,255,255,.12)':p.line); + text(ctx,label,M+20,y+17,520,18,{font:`500 14px ${f.label}`,color:dark?'#eef2ee':p.ink,maxLines:1}); + text(ctx,(negative?'− ':'')+money(val),W-M-20,y+17,260,18,{font:`700 16px ${f.label}`,color:negative?'#c0554a':(dark?'#fff':p.deep),align:'right',maxLines:1}); + y+=52+10; + }); + y+=8; + roundRect(ctx,M,y,W-2*M,160,20,p.accent,null); + text(ctx,'ИТОГО К ОПЛАТЕ',M+28,y+24,400,16,{font:`700 12px ${f.label}`,color:p.deep,maxLines:1}); + text(ctx,money(pr.total),M+28,y+48,W-2*M-56,56,{font:`700 44px ${f.display}`,color:p.deep,maxLines:1}); + if(s.guests)text(ctx,'на гостя: '+money(Number(pr.itemsTotal||pr.total||0)/Math.max(1,Number(s.guests))),M+28,y+118,400,20,{font:`600 14px ${f.label}`,color:p.deep,maxLines:1}); + y+=160+36; + text(ctx,'Спасибо, что выбираете нас — будем рады сделать ваше событие особенным.',M,y,W-2*M,22,{font:`500 15px ${f.label}`,color:p.muted,maxLines:2}); + } + async function renderContentPages(s,id){ + await ensureFontsReady(id); + const p=PALETTES[id],f=fontsFor(id),dark=id==='neon-menu'; + const items=s.items||[],top=120,bottom=H-90,rowH=dark?96:104,gap=dark?12:14; + const perPage=Math.max(1,Math.floor((bottom-top+gap)/(rowH+gap))); + const pageCount=Math.max(1,Math.ceil(items.length/perPage)); + const pages=[]; + for(let pi=0;pi{try{const scale=canvas.width/W,ctx=canvas.getContext('2d');ctx.save();ctx.setTransform(scale,0,0,scale,0,0);footer(ctx,p,2+i,total,snapshot?.brandName,f);ctx.restore()}catch(_){}}); + }else{ + correctPageNumbers(innerPages||[],2,total,innerId); + } return pages; }