caterium-app/public/core/proposal-layout.js

248 lines
28 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

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

/* Client proposals. A4 layouts inspired by the original v17.6.7 covers.
* Coordinates are shared by preview and download; text is measured after local fonts load.
*/
(()=>{
'use strict';
const W=1000,H=1414,M=70,BOTTOM=1310,SCALE=2.4;
const SANS='"Caterium Manrope",Arial,sans-serif';
const SERIF='"Caterium Playfair",Georgia,serif';
const ITALIC='"Caterium Playfair Italic",Georgia,serif';
const definitions=[
['light','Минимализм','split','#fffdf8','#20362d','#98713b','serif','rows'],
['midnight-glass','Luxury Dark','night','#111916','#f6f1e5','#d4b272','serif','rows'],
['editorial-grid','Editorial Magazine','editorial','#f6f3ec','#183d30','#94743c','serif','cards'],
['warm-sun','Warm Sun','sun','#fff4d8','#51361e','#9a5b1b','serif','rows'],
['bento-cards','Bento Cards','bento','#f3f2eb','#27382b','#8b6022','sans','cards'],
['event-story','Event Story','story','#f4ece0','#523a2b','#92643c','serif','rows'],
['black-gold','Food First','photo','#10100e','#fff7e9','#d8b671','serif','cards'],
['personal-letter','Personal Letter','letter','#f7f1e6','#473b2d','#8b673b','italic','rows'],
['event-ticket','Event Ticket','ticket','#eef2f3','#203f53','#38677a','sans','rows'],
['solar-experience','Solar Experience','solar','#ffd54f','#28392c','#77521f','sans','cards'],
['midnight-compact','Ночной минимализм','compact','#122630','#edf4f4','#9cbec4','sans','compact'],
['emerald-gold','Изумрудная классика','frame','#123b31','#f8f2df','#d1b578','serif','rows'],
['neon-emerald','Изумрудный акцент','neon-vertical','#071f1d','#edfff6','#7debaf','sans','compact'],
['cream-elegance','Кремовая классика','arch','#faf3e6','#35432e','#917445','serif','cards'],
['neon-menu','Неоновое меню','neon','#111a15','#f1ffef','#a1ed92','sans','compact'],
['emerald-circles','Изумрудные круги','circles','#102e2c','#f0f6ee','#d3b779','serif','cards'],
['midnight-checklist','Тёмный чек-лист','checklist','#12241d','#f1f5ea','#d7c593','sans','rows'],
['gourmet-hero','Гастро-витрина','gallery','#181611','#fff6e6','#dbb775','serif','cards'],
['diamond-gold','Изумруд и золото','diamond','#133329','#fff6de','#dabe7b','serif','rows'],
['premium-dark','Премиум тёмный','panorama','#141820','#f7f2e9','#cfb18a','sans','rows'],
['premium-emerald','Премиум изумрудный','botanical','#0e382e','#f5f2df','#b8d19c','italic','cards']
];
const THEMES=Object.fromEntries(definitions.map(([id,name,cover,bg,ink,accent,type,menu])=>[id,{id,name,cover,bg,ink,accent,type,menu,dark:!['light','editorial-grid','warm-sun','bento-cards','event-story','personal-letter','event-ticket','solar-experience','cream-elegance'].includes(id)}]));
const IDs=Object.keys(THEMES);
const number=v=>Number.isFinite(Number(v))?Number(v):0;
const money=v=>Math.round(number(v)).toLocaleString('ru-RU')+' ₽';
const qty=v=>number(v).toLocaleString('ru-RU',{maximumFractionDigits:3});
const date=v=>{if(!v)return '';const d=new Date(String(v)+'T12:00:00');return Number.isNaN(d.getTime())?String(v):d.toLocaleDateString('ru-RU',{day:'numeric',month:'long',year:'numeric'})};
let fontsPromise;
async function ready(){
if(!fontsPromise)fontsPromise=(async()=>{
if(!window.FontFace||!document.fonts)return;
const defs=[['Caterium Manrope','Manrope.ttf','200 800'],['Caterium Playfair','PlayfairDisplay.ttf','400 900'],['Caterium Playfair Italic','PlayfairDisplay-Italic.ttf','400 900']];
await Promise.all(defs.map(async([family,file,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)}
}));
await document.fonts.ready;
})();
return fontsPromise;
}
function loadImage(src){return new Promise(resolve=>{
if(!/^data:image\/(?:png|jpe?g|webp);base64,/i.test(String(src||'')))return resolve(null);
const image=new Image();let done=false;
const finish=v=>{if(done)return;done=true;clearTimeout(timer);resolve(v)};
const timer=setTimeout(()=>finish(null),4000);image.onload=()=>finish(image);image.onerror=()=>finish(null);image.src=src;
})}
function split(ctx,value,width){
const out=[];
for(const para of String(value??'').split(/\r?\n/)){
if(!para.trim()){out.push('');continue}
let line='';
for(const word of para.trim().split(/\s+/)){
if(ctx.measureText((line?line+' ':'')+word).width<=width){line+=(line?' ':'')+word;continue}
if(line){out.push(line);line=''}
for(const char of word){if(line&&ctx.measureText(line+char).width>width){out.push(line);line=''}line+=char}
}
if(line)out.push(line);
}
return out;
}
const defaults={showClientName:true,showDate:true,showGuests:true,showGallery:true,showPriceBreakdown:true,showAmountPerGuest:true,showBoxCount:true,showControl:true,showExtras:true,showFooterNote:true,showFinalPhoto:true,metricMode:'weight',texts:{}};
function settings(s){return {...defaults,...s.pdfSettings,texts:{...defaults.texts,...s.pdfSettings?.texts,...s.offerOverrides?.texts}}}
function painter(canvas,theme){
const ctx=canvas.getContext('2d',{alpha:false});const scale=canvas.width/W;ctx.setTransform(scale,0,0,scale,0,0);
const boxes=canvas.__proposalLayout||[];canvas.__proposalLayout=boxes;
const body=(size=19,weight=400)=>`${weight} ${size}px ${SANS}`;
const display=(size=54)=>`${theme.type==='sans'?700:500} ${size}px ${theme.type==='sans'?SANS:theme.type==='italic'?ITALIC:SERIF}`;
function rect(x,y,w,h,color,r=0){ctx.fillStyle=color;ctx.beginPath();ctx.roundRect(x,y,w,h,r);ctx.fill()}
function line(x,y,w,color=theme.accent){rect(x,y,w,1,color)}
function lines(value,w,font){ctx.font=font;return split(ctx,value,w)}
function write(value,x,y,w,{size=19,weight=400,color=theme.ink,font=body(size,weight),lh=size*1.42,align='left',role='text'}={}){
const values=Array.isArray(value)?value:lines(value,w,font);ctx.save();ctx.font=font;ctx.textBaseline='top';ctx.textAlign=align;ctx.fillStyle=color;
const xx=align==='right'?x+w:align==='center'?x+w/2:x;
values.forEach((v,i)=>ctx.fillText(v,xx,y+i*lh));ctx.restore();
boxes.push({x,y,w,h:values.length*lh,role,text:values.join('\n'),font});return values.length*lh;
}
function fit(value,x,y,w,h,{size=54,min=18,font,displayFont=true,...opts}={}){
let f,ls,lh;
do{f=font|| (displayFont?display(size):body(size,opts.weight||500));lh=size*1.27;ls=lines(value,w,f);if(ls.length*lh<=h)break;size-=1}while(size>=min);
// Cover copy is repeated in the flowing details when unusually long.
if(ls.length*lh>h){canvas.__proposalOverflow=canvas.__proposalOverflow||[];canvas.__proposalOverflow.push(String(value));return write('Подробности на следующих страницах',x,y,w,{size:18,lh:25,...opts})}
return write(ls,x,y,w,{...opts,font:f,size,lh});
}
function photo(img,x,y,w,h,{radius=0,shape='rect',contain=false}={}){
ctx.save();ctx.beginPath();if(shape==='circle')ctx.ellipse(x+w/2,y+h/2,w/2,h/2,0,0,Math.PI*2);else if(shape==='diamond'){ctx.moveTo(x+w/2,y);ctx.lineTo(x+w,y+h/2);ctx.lineTo(x+w/2,y+h);ctx.lineTo(x,y+h/2);ctx.closePath()}else ctx.roundRect(x,y,w,h,radius);ctx.clip();
rect(x,y,w,h,theme.dark?'#263e34':'#e9e3d7');
if(img){const scale=(contain?Math.min:Math.max)(w/img.width,h/img.height);ctx.drawImage(img,x+(w-img.width*scale)/2,y+(h-img.height*scale)/2,img.width*scale,img.height*scale)}
ctx.restore();
}
return {ctx,body,display,rect,line,lines,write,fit,photo};
}
function page(t,kind){const c=document.createElement('canvas');c.width=W*(t.scale||SCALE);c.height=Math.round(H*(t.scale||SCALE));c.dataset.sunProposalTemplate=t.id;c.dataset.sunProposalPage=kind;const d=painter(c,t);d.rect(0,0,W,H,t.bg);return {c,...d}}
function brand(d,s,t,logo,{x=M,y=60,w=860}={}){
if(logo){d.rect(x,y-4,78,58,'#fffdf6',8);d.photo(logo,x+6,y,66,50,{contain:true});x+=94;w-=94}
d.fit(s.brandName||'Моя компания',x,y+6,w,56,{size:24,min:15,displayFont:false,weight:700});
}
function facts(s,cfg){return [cfg.showDate?[date(s.date),s.time].filter(Boolean).join(' · '):'',cfg.showGuests&&number(s.guests)?`${qty(s.guests)} гостей`:''].filter(Boolean)}
function cover(s,t,images,logo,cfg){
const d=page(t,'cover'),hero=images.find(Boolean),hero2=images.filter(Boolean)[1]||hero,hero3=images.filter(Boolean)[2]||hero;
const title=s.event||'Ваше мероприятие',client=cfg.showClientName?s.client||'':'',eyebrow=cfg.texts.proposalLabel||'Индивидуальное предложение';
const ink=t.ink,accent=t.accent;
const heading=(x,y,w,h=250,size=64)=>d.fit(title,x,y,w,h,{size});
const sub=(x,y,w,h=94)=>d.fit(client,x,y,w,h,{size:25,displayFont:false});
const label=(x,y,w=860)=>d.fit(eyebrow,x,y,w,54,{size:18,displayFont:false,color:accent});
const fact=(x,y,w=860)=>d.fit(facts(s,cfg).join(' / '),x,y,w,65,{size:21,displayFont:false});
const note=(x,y,w,h=140)=>d.fit(cfg.texts.heroNote||'Меню, подобранное для вашего события.',x,y,w,h,{size:22,displayFont:false,color:ink});
const total=(x,y,w=400)=>{d.fit(cfg.texts.totalPriceTitle||'Итоговая стоимость',x,y,w,50,{size:17,displayFont:false,color:accent});d.fit(money(s.pricing?.total),x,y+50,w,100,{size:52})};
if(!['night','photo'].includes(t.cover))brand(d,s,t,logo);
switch(t.cover){
case 'split':
label(M,198,400);heading(M,265,414,290,62);sub(M,582,395);d.line(M,718,370);note(M,758,380);d.photo(hero,520,195,410,760,{radius:180});fact(M,1010);total(M,1120);break;
case 'night':
d.photo(hero,482,0,518,H);d.rect(0,0,482,H,t.bg);brand(d,s,t,logo,{w:340});label(M,235,350);heading(M,310,350,310,58);sub(M,650,345);fact(M,835,345);d.line(M,1000,340);total(M,1050,350);break;
case 'editorial':
label(M,180);heading(M,244,840,200,72);d.line(M,478,860);d.photo(hero,414,526,516,610);d.write('01',M,530,230,{size:54,font:d.display(54),color:accent});sub(M,638,280,145);fact(M,825,290);total(M,999,310);break;
case 'sun':
d.rect(758,180,172,172,'#f2c55b',86);label(M,200,550);heading(M,280,630,250,64);sub(M,562,780);d.photo(hero,320,698,610,390,{radius:180});fact(M,649,850);total(M,1140,530);break;
case 'bento':
label(M,180);heading(M,255,850,200,67);sub(M,480,800);d.photo(hero,M,610,556,430,{radius:26});d.photo(hero2,650,610,280,202,{radius:24});d.photo(hero3,650,838,280,202,{radius:24});fact(M,1080);total(M,1160,740);break;
case 'story':
label(M,182);heading(M,260,835,200,68);sub(M,485,820);d.line(M,635,860);['Меню','Подготовка','Ваше событие'].forEach((v,i)=>{const x=M+i*310;d.rect(x,627,16,16,accent,8);d.write(v,x,664,240,{size:18})});d.photo(hero,M,735,860,360);fact(M,1140,410);total(553,1130,377);break;
case 'photo':{
d.photo(hero,0,0,W,H);const g=d.ctx.createLinearGradient(0,0,0,H);g.addColorStop(0,'rgba(0,0,0,.82)');g.addColorStop(.48,'rgba(0,0,0,.24)');g.addColorStop(1,'rgba(0,0,0,.95)');d.ctx.fillStyle=g;d.ctx.fillRect(0,0,W,H);brand(d,s,t,logo);label(M,215);heading(M,290,800,270,74);sub(M,620,800);fact(M,1010);total(M,1120,820);break;}
case 'letter':
d.rect(44,162,912,1125,'#fffcf5',4);label(94,218,700);d.fit(client?`${client},`:'Дорогие гости,',94,308,760,165,{size:56});d.write('Предложение для вашего мероприятия',94,496,730,{size:22});heading(94,549,730,170,44);note(94,761,470,155);d.photo(hero,690,769,210,210,{shape:'circle'});fact(94,1010,790);total(94,1100,570);break;
case 'ticket':
d.rect(M,190,860,1080,'#fbfcfa',18);d.rect(740,190,190,1080,'#dbe8ea',18);d.ctx.save();d.ctx.setLineDash([7,9]);d.ctx.strokeStyle=accent;d.ctx.beginPath();d.ctx.moveTo(716,215);d.ctx.lineTo(716,1240);d.ctx.stroke();d.ctx.restore();label(106,246,570);heading(106,329,560,270,59);sub(106,632,550,115);fact(106,790,550);d.photo(hero,106,899,255,200,{radius:10});total(390,946,280);d.write('МЕНЮ',762,268,150,{size:17,weight:700});if(cfg.showDate){d.fit(date(s.date),765,341,140,180,{size:26,displayFont:false});d.fit(s.time||'',765,570,140,80,{size:34,displayFont:false})}break;
case 'solar':
label(M,205);heading(M,276,460,300,64);d.photo(hero,571,390,359,359,{shape:'circle'});sub(M,649,435,150);d.line(M,860,860);fact(M,913);d.rect(M,1040,860,238,'#fff1b5',30);total(106,1080,780);break;
case 'compact':
label(M,212);heading(M,300,850,250,70);sub(M,590,820);fact(M,760);d.line(M,867,860);d.photo(hero,550,935,380,330);total(M,980,420);break;
case 'frame':
d.ctx.strokeStyle=accent;d.ctx.lineWidth=1;d.ctx.strokeRect(38,158,924,1140);label(96,227,808);d.fit(title,96,313,808,255,{size:67,align:'center'});d.fit(client,96,604,808,90,{size:24,displayFont:false,align:'center'});d.photo(hero,240,750,520,300,{radius:160});fact(96,1103,810);total(96,1175,810);break;
case 'neon-vertical':
d.rect(M,218,12,910,accent);label(114,218,815);heading(114,300,800,240,72);sub(114,571,780);d.photo(hero,114,750,390,390,{radius:195});d.photo(hero2,536,750,390,390,{radius:195});fact(114,668,800);total(114,1170,790);break;
case 'arch':
label(M,189);d.fit(title,M,265,860,205,{size:65,align:'center'});d.fit(client,M,490,860,94,{size:24,displayFont:false,align:'center'});d.photo(hero,244,648,512,445,{radius:[250,250,10,10]});fact(M,1140,420);total(555,1136,375);break;
case 'neon':
d.rect(M,190,860,328,accent,24);d.fit(eyebrow,105,222,790,54,{size:18,displayFont:false,color:'#142019'});d.fit(title,105,290,790,200,{size:62,color:'#142019'});sub(M,558,800);fact(M,660);d.photo(hero,M,785,520,425,{radius:24});d.photo(hero2,616,785,314,205,{radius:24});total(616,1040,314);break;
case 'circles':
label(M,213);heading(M,291,820,225,68);sub(M,550,820);d.photo(hero,M,731,400,400,{shape:'circle'});d.photo(hero2,513,711,256,256,{shape:'circle'});d.photo(hero3,701,919,229,229,{shape:'circle'});fact(M,646);total(M,1165,800);break;
case 'checklist':
label(M,212);heading(M,294,850,240,67);sub(M,568,810);fact(M,680);d.line(M,786,860);(s.items||[]).slice(0,3).forEach((it,i)=>{const y=843+i*99;d.write(String(i+1).padStart(2,'0'),M,y,70,{size:24,color:accent});d.fit(it.name,170,y,750,77,{size:24,displayFont:false})});total(M,1170,810);break;
case 'gallery':
label(M,203);heading(M,280,840,210,68);sub(M,528,810);[hero,hero2,hero3].forEach((im,i)=>d.photo(im,M+i*294,707+i*37,272,355,{radius:136}));fact(M,626);total(M,1160,830);break;
case 'diamond':
label(M,200);heading(M,284,840,220,66);sub(M,537,810);fact(M,648);d.photo(hero,430,735,500,455,{shape:'diamond'});d.write('МЕНЮ',M,879,225,{size:18,color:accent});total(M,969,330);break;
case 'panorama':
d.photo(hero,0,176,W,467);label(M,701);heading(M,778,840,220,65);sub(M,1040,430,140);fact(M,1209,410);total(550,1135,380);break;
case 'botanical':
label(M,199,460);heading(M,284,470,350,64);d.photo(hero,590,205,340,590,{radius:170});sub(M,686,460,155);d.line(M,901,860);note(M,958,475,140);fact(M,1150,445);total(568,1119,362);break;
}
return d.c;
}
function composition(item){const values=item.compositionTotal?.length?item.compositionTotal:item.composition||[];return (Array.isArray(values)?values:[values]).map(v=>typeof v==='string'?v:v?.name||'').filter(Boolean).join(' · ')}
async function renderPages(s){
const cfg=settings(s),items=s.items||[],t={...(THEMES[s.offerTemplateId]||THEMES.light),scale:items.length>24?2:SCALE},cache=new Map();
const img=src=>{if(!src)return Promise.resolve(null);if(!cache.has(src))cache.set(src,loadImage(src));return cache.get(src)};
await ready();
const [logo,images,gallery]=await Promise.all([img(s.logo),Promise.all(items.map(i=>img((i.photoData||i.photo)===s.logo?'':i.photoData||i.photo))),Promise.all((s.finalGallery||[]).map(img))]);
const covers=[cover(s,t,images,logo,cfg)],pages=[...covers];let d,y;
const muted=t.dark?'#c0cabf':'#686a5f',lineColor=t.dark?'#426055':'#d9d2c4',panel=t.dark?'#203b30':'#ffffff';
// A sunny cover is paired with warm paper inside, making long menus easier to read.
const inner={...t,bg:t.id==='solar-experience'?'#fff9e8':t.bg};
function newPage(title){d=page(inner,'content');pages.push(d.c);brand(d,s,t,logo,{y:48});d.line(M,135,860,lineColor);y=172;if(title){y+=d.write(title,M,y,860,{size:36,font:d.display(36),lh:46});y+=28}}
function ensure(h,title){if(!d||y+h>BOTTOM)newPage(title)}
function flow(value,{size=19,color=t.ink,font,role='text',gap=20}={}){
if(!String(value||'').trim())return;
ensure(size*1.45+gap);const lines=d.lines(value,860,font||d.body(size));let pos=0;const lh=size*1.45;
while(pos<lines.length){let count=Math.floor((BOTTOM-y-gap)/lh);if(count<1){newPage();count=Math.floor((BOTTOM-y-gap)/lh)}const part=lines.slice(pos,pos+count);y+=d.write(part,M,y,860,{font:font||d.body(size),size,lh,color,role});pos+=part.length;if(pos<lines.length)newPage()}
y+=gap;
}
const title=cfg.texts.menuTitle||'Меню';newPage(title);
for(const value of covers[0].__proposalOverflow||[])flow(value,{size:22});
const meta=it=>[it.categoryName,it.weight?`Вес: ${it.weight}`:'',number(it.pieces)>0?`${qty(it.pieces)} шт. в порции`:''].filter(Boolean).join(' · ');
function priceLine(it){return `${qty(it.qty)} × ${money(it.unitPrice)} = ${money(it.sum)}`}
function measure(it,width,card){return d.lines(it.name||'Позиция меню',width,d.body(card?25:24,700)).length*(card?33:32)+d.lines(meta(it),width,d.body(16)).length*23+(composition(it)?d.lines(composition(it),width,d.body(18)).length*26+12:0)}
function details(it,x,yy,width,card){
yy+=d.write(it.name||'Позиция меню',x,yy,width,{size:card?25:24,weight:700,lh:card?33:32,role:'item-name'})+10;
if(meta(it))yy+=d.write(meta(it),x,yy,width,{size:16,lh:23,color:muted,role:'item-meta'})+10;
if(composition(it))yy+=d.write(composition(it),x,yy,width,{size:18,lh:26,color:muted,role:'composition'});
return yy;
}
function tallItem(it,index){
if(y>280)newPage(title+' · продолжение');
flow(it.name||'Позиция меню',{size:25,font:d.body(25,700),role:'item-name'});flow(meta(it),{size:16,color:muted});flow(composition(it),{size:18,color:muted,role:'composition'});flow(priceLine(it),{size:21,role:'item-price'});d.line(M,y-4,860,lineColor);y+=24;
}
if(!items.length)flow('Позиции меню пока не добавлены.',{color:muted});
if(t.menu==='cards'){
const cw=415,gap=30,photoH=cfg.showGallery?160:0;
for(let index=0;index<items.length;){
const pair=items.slice(index,index+2),heights=pair.map(it=>measure(it,cw-40,true)+photoH+132);
if(heights.some(h=>h>1080)){tallItem(items[index],index);index++;continue}
const h=Math.max(...heights);ensure(h+24,title+' · продолжение');
pair.forEach((it,k)=>{const x=M+k*(cw+gap);d.rect(x,y,cw,h,panel,12);if(photoH)d.photo(images[index+k],x,y,cw,photoH,{radius:[12,12,0,0]});details(it,x+20,y+photoH+24,cw-40,true);const priceY=y+h-75;d.line(x+20,priceY-15,cw-40,lineColor);d.write(`${qty(it.qty)} × ${money(it.unitPrice)}`,x+20,priceY,cw-40,{size:16,color:muted,role:'quantity-price'});d.write(money(it.sum),x+20,priceY+28,cw-40,{size:23,weight:700,align:'right',role:'item-price'})});y+=h+24;index+=pair.length;
}
}else{
const photoW=cfg.showGallery?(t.menu==='compact'?98:145):0,tx=M+(photoW?photoW+25:0),tw=860-(tx-M)-185;
for(let index=0;index<items.length;index++){
const it=items[index],amountLines=d.lines(money(it.sum),176,d.body(23,700)),amountH=amountLines.length*32,unitLines=d.lines(`${qty(it.qty)} × ${money(it.unitPrice)}`,176,d.body(16)),h=Math.max(photoW,measure(it,tw,false)+40,amountH+12+unitLines.length*23)+42;
if(h>1080){tallItem(it,index);continue}
ensure(h,title+' · продолжение');if(photoW)d.photo(images[index],M,y,photoW,photoW,{radius:t.menu==='compact'?49:9});details(it,tx,y,tw,false);
d.write(amountLines,754,y,176,{size:23,lh:32,weight:700,align:'right',role:'item-price'});
d.write(unitLines,754,y+amountH+12,176,{size:16,lh:23,color:muted,align:'right',role:'quantity-price'});
d.line(M,y+h-23,860,lineColor);y+=h;
}
}
const p=s.pricing||{},text=cfg.texts;
const rows=cfg.showPriceBreakdown?[[text.priceItemsLabel||'Стоимость позиций',p.base],...(number(p.manual)?[[text.discountLabel||'Скидка',-number(p.manual)]]:[]),...(number(p.promo)?[[`${text.promoLabel||'Промокод'}${s.promoCode?' '+s.promoCode:''}`,-number(p.promo)]]:[]),[text.menuAfterDiscountLabel||'Меню после скидок',p.itemsTotal],[text.deliveryLabel||'Доставка',p.delivery]]:[];
ensure(200);
y+=22;flow(text.pricingTitle||'Расчёт стоимости',{size:34,font:d.display(34),gap:24});
for(const [label,value] of rows){
const ls=d.lines(label,610,d.body(19)),rs=d.lines(money(value),215,d.body(21,700)),h=Math.max(ls.length*28,rs.length*29)+22;ensure(h+95);
d.write(ls,M,y,610,{size:19,lh:28,color:muted,role:'pricing-label'});d.write(rs,715,y,215,{size:21,weight:700,lh:29,align:'right',role:'pricing-value'});y+=h;
}
ensure(124);d.line(M,y,860,lineColor);y+=28;const totalLabel=d.lines(text.summaryTitle||text.totalLabel||'Итого',380,d.display(29));d.write(totalLabel,M,y,380,{size:29,font:d.display(29),lh:39,role:'total-label'});const amount=d.lines(money(p.total),460,d.body(35,700));y+=Math.max(60,totalLabel.length*39,d.write(amount,470,y,460,{size:35,weight:700,lh:45,align:'right',role:'total-value'}))+26;
const metrics=[];const guests=number(s.guests);
if(cfg.showBoxCount)metrics.push(`${text.boxCountLabel||'Количество боксов'}: ${qty(items.filter(i=>[0,5].includes(number(i.categoryId))).reduce((n,i)=>n+number(i.qty),0))}`);
if(cfg.showGuests&&guests){if(cfg.showAmountPerGuest)metrics.push(`${text.amountPerGuestLabel||'Сумма на гостя'}: ${money(number(p.itemsTotal)/guests)}`);if(cfg.metricMode==='pieces'){if(number(s.foodPieces))metrics.push(`${text.piecesMetricLabel||'Канапе на гостя'}: ${qty(Math.ceil(number(s.foodPieces)/guests))} шт.`)}else if(number(s.foodGrams))metrics.push(`${text.weightMetricLabel||'Вес еды на гостя'}: ${qty(Math.ceil(number(s.foodGrams)/guests/5)*5)} г`)}
if(metrics.length)flow(metrics.join(' · '),{size:17,color:muted});
if(cfg.showFooterNote&&text.footerNote)flow(text.footerNote,{size:17,color:muted,gap:28});
if(text.salesTitle&&text.salesNote){ensure(155);flow(text.salesTitle,{size:27,font:d.display(27),gap:16});flow(text.salesNote,{size:19,gap:28})}
const sections=[[cfg.showControl,text.controlTitle||s.controlTitle||'Организация мероприятия',cfg.controlLines||s.controlLines],[cfg.showExtras,text.extrasTitle||s.extrasTitle||'Дополнительно',cfg.extraServices||s.extraServices]].filter(([enabled,,values])=>enabled&&Array.isArray(values)&&values.length);
const columnH=([,heading,values])=>d.lines(heading,405,d.display(27)).length*36+24+values.reduce((sum,v)=>sum+d.lines('• '+String(v),405,d.body(18)).length*26+12,0);
if(sections.length===2&&sections.every(section=>columnH(section)<650)){
const h=Math.max(...sections.map(columnH));ensure(h+35);sections.forEach(([,heading,values],index)=>{const x=M+index*455;let yy=y;d.line(x,yy,405,lineColor);yy+=18;yy+=d.write(heading,x,yy,405,{size:27,font:d.display(27),lh:36})+18;for(const value of values)yy+=d.write('• '+String(value),x,yy,405,{size:18,lh:26})+12});y+=h+35;
}else for(const [,heading,values] of sections){ensure(135);flow(heading,{size:27,font:d.display(27),gap:16});for(const value of values)flow('• '+String(value),{size:18,gap:10});y+=18}
const pics=gallery.filter(Boolean).slice(0,2);
// Gallery fills existing space; it never creates a nearly empty extra page.
if(cfg.showGallery&&cfg.showFinalPhoto&&pics.length&&BOTTOM-y>=260){const label=text.finalGalleryTitle||'Сервировка вашего события',h=d.lines(label,860,d.display(27)).length*38;const available=BOTTOM-y-h-18;if(available>=180){y+=d.write(label,M,y,860,{size:27,font:d.display(27),lh:38})+18;const w=(860-22*(pics.length-1))/pics.length;pics.forEach((im,i)=>d.photo(im,M+i*(w+22),y,w,Math.min(340,available),{radius:8}))}}
pages.forEach((c,i)=>{const fd=painter(c,i?inner:t);const darkCover=i===0&&t.cover==='photo';if(i===0&&['night','photo'].includes(t.cover))fd.rect(0,1330,W,84,t.bg);fd.line(M,1343,860,darkCover?'#8b8b78':lineColor);fd.fit([s.brandName||'Моя компания',s.brandCity].filter(Boolean).join(' · '),M,s.brandContacts?1351:1361,720,26,{size:14,min:12,displayFont:false,color:darkCover?'#fff5e6':muted});if(s.brandContacts)fd.fit(s.brandContacts,M,1376,720,26,{size:12,min:10,displayFont:false,color:darkCover?'#fff5e6':muted});fd.write(`${i+1} / ${pages.length}`,850,1361,80,{size:14,align:'right',color:darkCover?'#fff5e6':muted,role:'page-number'});});
return pages;
}
const descriptions={split:'Светлая обложка, вертикальное фото и спокойная типографика.',night:'Тёмная полоса с текстом и фотография на всю высоту.',editorial:'Журнальная сетка, выразительные заголовки и карточки меню.',sun:'Тёплая бумага, солнечный акцент и широкая фотография.',bento:'Модульная фотокомпозиция и меню в двух колонках.',story:'История события с этапами подготовки и панорамным фото.',photo:'Фотография на всю обложку и золотистые акценты.',letter:'Личное обращение на светлой бумаге и курсивные заголовки.',ticket:'Обложка в форме приглашения с отдельной полосой даты.',solar:'Яркая солнечная обложка и светлые внутренние страницы.',compact:'Лаконичная ночная палитра и компактный список меню.',frame:'Тонкая золотистая рамка и симметричная композиция.', 'neon-vertical':'Вертикальный акцент и пара круглых фотографий.',arch:'Кремовая бумага, арочное фото и классические заголовки.',neon:'Яркий заголовок на тёмной бумаге и модульные фото.',circles:'Круглые фотографии разных размеров и изумрудная палитра.',checklist:'Нумерованные акценты меню на строгой тёмной обложке.',gallery:'Три фотографии в арках и крупная журнальная типографика.',diamond:'Фотография в ромбе и изумрудно-золотая палитра.',panorama:'Широкая панорама и сдержанные современные заголовки.',botanical:'Высокая арка, курсивные заголовки и глубокий изумрудный фон.'};
window.CateriumProposalPDF=Object.freeze({VERSION:'20260918-proposal-quality',IDS:IDs,templates:definitions.map(([id,name,style])=>({id,name,desc:descriptions[style],thumb:`offer-templates/quality-${id}.jpg`})),ready,renderPages});
})();