caterium-app/public/core/company-branding.js

96 lines
7.9 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.

(()=>{
'use strict';
if(window.CateriumBranding)return;
const NAME_KEY='sunPdfBrandNameV1',LOGO_KEY='sunPdfBrandLogoV1';
const PIXEL='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIHWP4z8DwHwAFgAI/ScLbtAAAAABJRU5ErkJggg==';
const esc=value=>String(value??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
const read=key=>{try{return localStorage.getItem(key)||''}catch(_){return ''}};
const object=key=>{try{const v=JSON.parse(read(key)||'{}');return v&&typeof v==='object'&&!Array.isArray(v)?v:{}}catch(_){return {}}};
const safeLogo=src=>/^data:image\/(?:png|jpeg|webp);base64,[a-z0-9+/=\s]+$/i.test(String(src||''))&&String(src).length<=2000000?String(src):'';
const clean=value=>String(value||'').trim().slice(0,160);
function identity(){
const profile=object('sunCompanyProfileV1');
const storedName=read(NAME_KEY);
// An explicitly cleared logo must not reappear from an older profile.
let hasLogo=false;try{hasLogo=localStorage.getItem(LOGO_KEY)!==null}catch(_){}
return {
name:clean(storedName||profile.shortName||profile.name)||'Моя компания',
logo:safeLogo(hasLogo?read(LOGO_KEY):profile.logo),
city:clean(profile.city),
contacts:[profile.phone,profile.email,profile.website].map(clean).filter(Boolean).join(' · ')
};
}
function changed(){window.dispatchEvent(new CustomEvent('caterium:document-brand-changed'));}
function saveName(value){localStorage.setItem(NAME_KEY,clean(value));changed();}
function clearLogo(){localStorage.setItem(LOGO_KEY,'');changed();}
// Remove empty PNG margins, keeping a small transparent breathing space.
// Used both on upload and on older logos already saved in offer snapshots.
function prepareLogoImage(image){
const width=image.naturalWidth||image.width,height=image.naturalHeight||image.height;
const scale=Math.min(1,960/Math.max(width,height));
const source=document.createElement('canvas');source.width=Math.max(1,Math.round(width*scale));source.height=Math.max(1,Math.round(height*scale));
const ctx=source.getContext('2d',{willReadFrequently:true});ctx.drawImage(image,0,0,source.width,source.height);
const pixels=ctx.getImageData(0,0,source.width,source.height).data;
let left=source.width,top=source.height,right=-1,bottom=-1;
for(let y=0;y<source.height;y++)for(let x=0;x<source.width;x++)if(pixels[(y*source.width+x)*4+3]>8){left=Math.min(left,x);top=Math.min(top,y);right=Math.max(right,x);bottom=Math.max(bottom,y)}
if(right<left)throw new Error('Логотип полностью прозрачный. Выберите файл с изображением.');
const w=right-left+1,h=bottom-top+1,pad=Math.max(1,Math.round(Math.max(w,h)*.02));
const out=document.createElement('canvas');out.width=w+pad*2;out.height=h+pad*2;
out.getContext('2d').drawImage(source,left,top,w,h,pad,pad,w,h);return out;
}
async function saveLogo(file){
const scope=()=>JSON.stringify([window.SunCloudV2?.getSession?.()?.user?.id,window.SunCloudV2?.getWorkspace?.()?.id]);
const startedIn=scope();
if(!file||!/^image\/(?:png|jpeg|webp)$/i.test(file.type))throw new Error('Выберите PNG, JPEG или WebP.');
if(file.size>15*1024*1024)throw new Error('Логотип должен быть меньше 15 МБ.');
const src=await new Promise((resolve,reject)=>{const reader=new FileReader();reader.onload=()=>resolve(String(reader.result));reader.onerror=()=>reject(new Error('Не удалось прочитать файл.'));reader.readAsDataURL(file)});
const logo=await new Promise((resolve,reject)=>{const img=new Image();img.onload=()=>resolve(img);img.onerror=()=>reject(new Error('Не удалось открыть изображение.'));img.src=src});
const canvas=prepareLogoImage(logo);
const data=canvas.toDataURL('image/png');
if(!safeLogo(data))throw new Error('Изображение слишком сложное. Выберите логотип меньшего размера.');
if(startedIn!==scope())throw new Error('Аккаунт или компания изменились. Загрузите логотип ещё раз.');
localStorage.setItem(LOGO_KEY,data);changed();return data;
}
const documentName=()=>identity().name;
const documentLogo=()=>identity().logo;
const documentImage=()=>documentLogo()||PIXEL;
const documentContacts=()=>identity().contacts;
const documentCity=()=>identity().city;
function snapshot(value){const brand=identity();return {...value,brandName:brand.name,logo:brand.logo,brandContacts:brand.contacts,brandCity:brand.city};}
let sidebar={name:'Caterium',logo:'caterium-mark-light.svg',special:false};
let request=0,checkedUser='',pendingUser='';
function sidebarHTML(){return `<img class="real-logo${sidebar.special?'':' caterium-product-mark'}" src="${esc(sidebar.logo)}" alt="${esc(sidebar.name)}"><span>${esc(sidebar.name)}</span>`;}
function renderSidebar(){
const node=document.querySelector('body > header .brand');if(!node)return;
const html=sidebarHTML();if(node.innerHTML!==html)node.innerHTML=html;
node.dataset.cateriumBrand=sidebar.special?'solnce':'caterium';
}
function resetSidebar(){request++;checkedUser='';pendingUser='';sidebar={name:'Caterium',logo:'caterium-mark-light.svg',special:false};renderSidebar();}
async function refreshSidebar(){
const cloud=window.SunCloudV2,session=cloud?.getSession?.(),uid=session?.user?.id||'';
if(!uid){resetSidebar();return;}
if(checkedUser===uid||pendingUser===uid){renderSidebar();return;}
resetSidebar();pendingUser=uid;const current=request;
try{
const result=await cloud.getClient()?.rpc('caterium_my_sidebar_brand');
if(current!==request||window.SunCloudV2?.getSession?.()?.user?.id!==uid)return;
if(result?.error||!result?.data)return;
if(result.data.variant==='solnce')sidebar={name:'Солнце Кейтеринг',logo:'sun-logo.png',special:true};
checkedUser=uid;renderSidebar();
}catch(_){/* Until identity is confirmed, show the shared product brand. */}
finally{if(current===request)pendingUser='';}
}
function boot(){
const style=document.createElement('style');style.id='caterium-company-branding-style';
style.textContent='#sunPdfBrandCard .sun-document-brand-setting{display:grid;grid-template-columns:minmax(180px,300px) minmax(0,1fr);gap:24px;align-items:center}#sunPdfBrandPreview{height:140px;display:flex;align-items:center;justify-content:center;padding:16px;border-radius:12px;background:repeating-conic-gradient(#edf0eb 0% 25%,#fff 0% 50%) 0/20px 20px;border:1px solid #dde2d9;color:#475349;font-size:13px}#sunPdfBrandPreview img{width:auto!important;height:auto!important;max-width:100%!important;max-height:108px!important;object-fit:contain;border:0!important;border-radius:0!important}@media(max-width:640px){#sunPdfBrandCard .sun-document-brand-setting{grid-template-columns:1fr}}body > header .brand .caterium-product-mark{background:#fff7e6;border-radius:14px;padding:6px;filter:none!important}';document.head.appendChild(style);
renderSidebar();refreshSidebar();
}
window.CateriumBranding=Object.freeze({identity,documentName,documentLogo,documentImage,documentContacts,documentCity,
nameHTML:()=>esc(documentName()),logoHTML:()=>esc(documentImage()),contactsHTML:()=>esc(documentContacts()),cityHTML:()=>esc(documentCity()),
escapeHTML:esc,snapshot,saveName,saveLogo,clearLogo,prepareLogoImage,sidebarHTML,renderSidebar,refreshSidebar,resetSidebar});
for(const event of ['sun:cloud-state-applied','sun:cloud-permissions-changed','suncloudsync'])window.addEventListener(event,()=>{refreshSidebar();changed()});
window.addEventListener('storage',event=>{if([NAME_KEY,LOGO_KEY,'sunCompanyProfileV1'].includes(event.key))changed();if(/^sb-.*-auth-token$/.test(event.key||''))resetSidebar()});
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',boot,{once:true});else boot();
})();