Add single catalog item PDF export
This commit is contained in:
parent
923ea64650
commit
4265a88c34
233
public/core/single-item-pdf.js
Normal file
233
public/core/single-item-pdf.js
Normal file
@ -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.length<maxLines)lines.push(line);
|
||||
if(words.length&&lines.length===maxLines){
|
||||
let last=lines[maxLines-1]||'';
|
||||
while(last.length>2&&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;i<raw.length;i++)bytes[i]=raw.charCodeAt(i);
|
||||
return {bytes,width:canvas.width,height:canvas.height};
|
||||
}
|
||||
|
||||
async function renderItemPage(item){
|
||||
const W=1240,H=1754,heroH=1110,canvas=document.createElement('canvas');canvas.width=W;canvas.height=H;
|
||||
const ctx=canvas.getContext('2d',{alpha:false});ctx.textBaseline='alphabetic';ctx.textAlign='left';
|
||||
ctx.fillStyle='#fff';ctx.fillRect(0,0,W,H);
|
||||
|
||||
const b=brand(),photo=await loadImage(item.photo||'');
|
||||
if(photo)drawCover(ctx,photo,0,0,W,heroH);
|
||||
else{
|
||||
ctx.fillStyle='#f3f0e9';ctx.fillRect(0,0,W,heroH);
|
||||
const logo=await loadImage(b.logo||'');if(logo)drawContain(ctx,logo,370,280,500,500);
|
||||
else{ctx.fillStyle='#d7d1c6';ctx.font='700 72px Arial,sans-serif';ctx.textAlign='center';ctx.fillText(String(b.name||'Caterium'),W/2,heroH/2);ctx.textAlign='left'}
|
||||
}
|
||||
|
||||
// Subtle photo readability veil at the very top, matching the reference's clean label.
|
||||
const topGrad=ctx.createLinearGradient(0,0,0,170);topGrad.addColorStop(0,'rgba(255,255,255,.72)');topGrad.addColorStop(1,'rgba(255,255,255,0)');
|
||||
ctx.fillStyle=topGrad;ctx.fillRect(0,0,W,180);
|
||||
ctx.fillStyle='#c45f73';ctx.font='500 30px Arial,sans-serif';ctx.fillText(categoryName(item).toLowerCase(),42,70);
|
||||
|
||||
const panelY=heroH;ctx.fillStyle='#fff';ctx.fillRect(0,panelY,W,H-panelY);
|
||||
const titleY=1195,priceY=1195;
|
||||
ctx.fillStyle='#171719';ctx.font='400 52px Arial,sans-serif';
|
||||
const titleLines=wrap(ctx,String(item.name||'').toUpperCase(),710,2);
|
||||
titleLines.forEach((line,i)=>ctx.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(`<!doctype html><meta charset="utf-8"><title>${esc(item?.name||'PDF бокса')}</title><style>body{margin:0;display:grid;place-items:center;min-height:100vh;background:#f5f2eb;font:16px Arial;color:#15364c}.box{text-align:center;background:#fff;padding:28px 34px;border-radius:14px;box-shadow:0 10px 35px #0002}.mark{font-size:32px;color:#c99a32}.p{margin-top:9px;color:#68717a}</style><div class="box"><div class="mark">☼</div><b>Формируем PDF</b><div class="p">Один бокс · одна страница A4</div></div>`);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=`<div style="font:16px Arial;padding:30px;color:#7a2e2e"><b>Не удалось сформировать PDF.</b><p>${esc(error?.message||error)}</p></div>`}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();
|
||||
})();
|
||||
Loading…
Reference in New Issue
Block a user