caterium-app/public/core/single-item-pdf.js
2026-09-22 22:57:23 +08:00

234 lines
13 KiB
JavaScript
Raw Permalink 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.

(()=>{
'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=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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();
})();