/* 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)} })); // Other app/UI webfonts may still be loading indefinitely. Only the // three awaited proposal faces determine this canvas layout. })(); 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[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;indexmeasure(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;index1080){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&§ions.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}); })();