From 4265a88c3463461db2be8382fb2d7a838cd06936 Mon Sep 17 00:00:00 2001 From: pavlov346346-source Date: Tue, 22 Sep 2026 22:57:23 +0800 Subject: [PATCH] Add single catalog item PDF export --- public/core/single-item-pdf.js | 233 +++++++++++++++++++++++++++++++++ 1 file changed, 233 insertions(+) create mode 100644 public/core/single-item-pdf.js diff --git a/public/core/single-item-pdf.js b/public/core/single-item-pdf.js new file mode 100644 index 0000000..b602251 --- /dev/null +++ b/public/core/single-item-pdf.js @@ -0,0 +1,233 @@ +(()=>{ + 'use strict'; + if(window.CateriumSingleItemPdf)return; + + const VERSION='20260922-single-item-pdf-v1'; + const $=id=>document.getElementById(id); + const esc=v=>window.SunSafe?.escapeHTML?window.SunSafe.escapeHTML(String(v??'')):String(v??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); + const CATEGORY_NAMES={0:'Боксы',1:'Посуда',2:'Дополнения',3:'Напитки',4:'Доставка',5:'Премиум',6:'Банкетное меню'}; + let currentItemId='',lastPdfUrl='',wrappedEditBox=null; + + function toast(message,type='success'){ + try{return window.SunEnterprise?.toast?.(message,type)}catch(_){} + if(type==='warn'||type==='error')console.warn(message);else console.log(message); + } + + function catalog(){ + try{ + const rows=window.CateriumDataV1773?.catalog?.list?.(); + if(Array.isArray(rows))return rows; + }catch(_){} + try{ + const rows=JSON.parse(localStorage.getItem('sunBoxes')||'[]'); + return Array.isArray(rows)?rows:[]; + }catch(_){return[]} + } + function itemById(id){return catalog().find(x=>String(x?.id)===String(id))||null} + function categoryName(item){return String(item?.catalogSection||CATEGORY_NAMES[Number(item?.category||0)]||'Каталог').trim()} + function price(item){try{return Number(window.CateriumPricing?.price?.(item)??item?.price??0)||0}catch(_){return Number(item?.price||0)||0}} + function money(value){return `${Number(value||0).toLocaleString('ru-RU',{maximumFractionDigits:2})} ₽`} + function brand(){ + try{ + const b=window.CateriumBranding?.identity?.(); + if(b)return b; + }catch(_){} + return {name:'Caterium',logo:'',contacts:''}; + } + function cleanIngredientName(name){return String(name||'').replace(/\s*[—–-]\s*\d+(?:[.,]\d+)?\s*г(?:\s*\/\s*шт\.?)?\s*$/i,'').trim()} + function composition(item){ + if(Array.isArray(item?.composition)&&item.composition.some(Boolean))return item.composition.map(x=>String(x||'').trim()).filter(Boolean); + if(Array.isArray(item?.ingredients))return item.ingredients.filter(x=>Array.isArray(x)&&x[0]).map(row=>{ + const qty=Math.max(0,Number(row[1]||0)),unit=String(row[2]||'').trim(),name=cleanIngredientName(row[0]); + if(!qty||(/^поз\.?$/i.test(unit)&&qty<=1))return name; + const shown=Number.isInteger(qty)?String(qty):qty.toLocaleString('ru-RU',{maximumFractionDigits:2}); + return `${name}${unit?` — ${shown} ${unit}`:''}`; + }).filter(Boolean); + return []; + } + function safeImageSrc(src){ + const raw=String(src||'').trim();if(!raw)return''; + try{return window.SunSafe?.imageAssetSrc?window.SunSafe.imageAssetSrc(raw):raw}catch(_){return raw} + } + function loadImage(src){ + return new Promise(resolve=>{ + const raw=safeImageSrc(src);if(!raw)return resolve(null); + const img=new Image();let done=false; + const finish=v=>{if(done)return;done=true;clearTimeout(timer);resolve(v)}; + try{const u=new URL(raw,document.baseURI||location.href);if(/^https?:$/i.test(u.protocol)&&u.origin!==location.origin)img.crossOrigin='anonymous'}catch(_){} + img.onload=()=>finish(img);img.onerror=()=>finish(null); + const timer=setTimeout(()=>finish(null),15000);img.src=raw; + }); + } + function drawCover(ctx,img,x,y,w,h){ + if(!img)return; + const iw=img.naturalWidth||img.width,ih=img.naturalHeight||img.height;if(!iw||!ih)return; + const scale=Math.max(w/iw,h/ih),sw=w/scale,sh=h/scale,sx=(iw-sw)/2,sy=(ih-sh)/2; + ctx.drawImage(img,sx,sy,sw,sh,x,y,w,h); + } + function drawContain(ctx,img,x,y,w,h){ + if(!img)return; + const iw=img.naturalWidth||img.width,ih=img.naturalHeight||img.height;if(!iw||!ih)return; + const scale=Math.min(w/iw,h/ih),dw=iw*scale,dh=ih*scale; + ctx.drawImage(img,x+(w-dw)/2,y+(h-dh)/2,dw,dh); + } + function wrap(ctx,text,width,maxLines=20){ + const words=String(text||'').replace(/\s+/g,' ').trim().split(' ').filter(Boolean),lines=[];let line=''; + for(const word of words){ + const test=line?`${line} ${word}`:word; + if(!line||ctx.measureText(test).width<=width)line=test; + else{lines.push(line);line=word;if(lines.length>=maxLines-1)break} + } + if(line&&lines.length2&&ctx.measureText(last+'…').width>width)last=last.slice(0,-1); + lines[maxLines-1]=last.replace(/[\s,.;:-]+$/,'')+'…'; + } + return lines; + } + function filename(name){ + const base=String(name||'Бокс').trim().replace(/[\\/:*?"<>|]+/g,' ').replace(/\s+/g,' ').slice(0,80)||'Бокс'; + return `${base}.pdf`; + } + function canvasJpeg(canvas){ + const data=canvas.toDataURL('image/jpeg',.94),base64=String(data).split(',')[1]||''; + if(!base64)throw new Error('Не удалось подготовить страницу PDF.'); + const raw=atob(base64),bytes=new Uint8Array(raw.length);for(let i=0;ictx.fillText(line,38,titleY+i*58)); + + ctx.textAlign='right';ctx.fillStyle='#171719';ctx.font='400 50px Arial,sans-serif';ctx.fillText(money(price(item)),1197,priceY); + ctx.strokeStyle='#c94f67';ctx.lineWidth=5;ctx.beginPath();ctx.moveTo(970,1212);ctx.lineTo(1197,1212);ctx.stroke();ctx.textAlign='left'; + + const metaY=1260; + ctx.textAlign='right';ctx.fillStyle='#242426';ctx.font='500 25px Arial,sans-serif'; + const pieces=Math.max(0,Math.round(Number(item.pieces||0))); + if(pieces)ctx.fillText(`${pieces} шт.`,1197,metaY); + if(item.weight)ctx.fillText(`Вес: ${String(item.weight)}`,1197,metaY+(pieces?38:0)); + ctx.textAlign='left'; + + const lines=composition(item); + ctx.fillStyle='#55565a';ctx.font='400 24px Arial,sans-serif'; + let y=1328,rendered=0; + for(const raw of lines){ + const parts=wrap(ctx,raw,760,2); + for(const part of parts){ + if(rendered>=8)break; + ctx.fillText(part,38,y);y+=34;rendered++; + } + if(rendered>=8)break; + } + if(!rendered){ctx.fillStyle='#83858a';ctx.fillText('Состав не указан.',38,y)} + + // Company mark in the lower-right, similar to the reference watermark. + ctx.textAlign='right';ctx.fillStyle='#76b9b4';ctx.font='500 22px Arial,sans-serif'; + ctx.fillText(String(b.name||window.CateriumBranding?.documentName?.()||'Caterium'),1197,1708); + ctx.textAlign='left'; + + return canvasJpeg(canvas); + } + + async function buildItemPdfBlob(id){ + const item=itemById(id);if(!item)throw new Error('Позиция не найдена в каталоге.'); + if(!window.SunPdfEngine?.fromJpegs)throw new Error('PDF-движок ещё не загружен.'); + const page=await renderItemPage(item); + return window.SunPdfEngine.fromJpegs([page]); + } + function loadingPage(win,item){ + try{ + win.document.open();win.document.write(`${esc(item?.name||'PDF бокса')}
Формируем PDF
Один бокс · одна страница A4
`);win.document.close(); + }catch(_){} + } + async function openItemPdf(id){ + const item=itemById(id);if(!item){toast('Позиция не найдена в каталоге.','warn');return} + const viewer=window.open('about:blank','_blank');if(viewer)loadingPage(viewer,item); + try{ + const blob=await buildItemPdfBlob(id); + if(lastPdfUrl)URL.revokeObjectURL(lastPdfUrl);lastPdfUrl=URL.createObjectURL(blob); + if(viewer)viewer.location.replace(lastPdfUrl); + else{ + const a=document.createElement('a');a.href=lastPdfUrl;a.download=filename(item.name);a.style.display='none';document.body.appendChild(a);a.click();a.remove(); + toast('PDF бокса подготовлен.','success'); + } + }catch(error){ + try{if(viewer)viewer.document.body.innerHTML=`
Не удалось сформировать PDF.

${esc(error?.message||error)}

`}catch(_){} + toast(error?.message||'Не удалось сформировать PDF.','warn'); + } + } + + function ensureEditorButton(){ + const actions=document.querySelector('#editor .dialog .actions');if(!actions)return null; + let btn=$('sunSingleItemPdfButton'); + if(!btn){ + btn=document.createElement('button');btn.id='sunSingleItemPdfButton';btn.type='button';btn.className='outline';btn.textContent='PDF бокса'; + btn.title='Открыть одностраничный PDF выбранного бокса'; + btn.onclick=()=>{const id=String(btn.dataset.itemId||'');if(id)void openItemPdf(id)}; + const danger=actions.querySelector('.danger');if(danger)danger.before(btn);else actions.appendChild(btn); + } + return btn; + } + function syncEditorButton(id){ + currentItemId=String(id||'');const btn=ensureEditorButton();if(!btn)return; + const item=currentItemId?itemById(currentItemId):null; + btn.dataset.itemId=currentItemId; + btn.hidden=!item;btn.style.display=item?'inline-flex':'none'; + btn.textContent=item&&[0,5].includes(Number(item.category||0))?'PDF бокса':'PDF позиции'; + } + function injectReadOnlyButton(){ + const host=document.querySelector('#sunMenuDetailV1762 .sun-menu-readonly');if(!host||!currentItemId||host.querySelector('[data-single-item-pdf]'))return; + const item=itemById(currentItemId);if(!item)return; + const btn=document.createElement('button');btn.type='button';btn.className='outline';btn.dataset.singleItemPdf='1';btn.textContent=[0,5].includes(Number(item.category||0))?'PDF бокса':'PDF позиции'; + btn.style.margin='0 0 14px';btn.onclick=()=>void openItemPdf(currentItemId); + host.insertBefore(btn,host.firstElementChild?.nextSibling||host.firstChild); + } + function wrapEditor(){ + if(typeof window.editBox!=='function'||window.editBox===wrappedEditBox)return; + const previous=window.editBox; + wrappedEditBox=function(id,...args){ + currentItemId=String(id||''); + const result=previous.call(this,id,...args); + setTimeout(()=>syncEditorButton(currentItemId),0); + return result; + }; + wrappedEditBox.__singlePdfWrapped=true;window.editBox=wrappedEditBox; + } + function boot(){ + ensureEditorButton();syncEditorButton(''); + wrapEditor(); + document.addEventListener('click',event=>{ + const row=event.target.closest?.('[data-menu-item-v1762]');if(row){currentItemId=String(row.dataset.menuItemV1762||'');setTimeout(injectReadOnlyButton,0)} + },true); + new MutationObserver(()=>{if(window.editBox!==wrappedEditBox)wrapEditor();if(document.querySelector('#editor .dialog .actions')&&!$('sunSingleItemPdfButton'))syncEditorButton(currentItemId);injectReadOnlyButton();}).observe(document.documentElement,{childList:true,subtree:true}); + } + + window.CateriumSingleItemPdf=Object.freeze({VERSION,buildItemPdfBlob,openItemPdf,renderItemPage}); + window.sunBuildSingleCatalogItemPdfBlob=buildItemPdfBlob; + window.sunOpenSingleCatalogItemPdf=openItemPdf; + window.sunSyncSingleCatalogPdfButton=syncEditorButton; + if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',boot,{once:true});else boot(); +})();