caterium-app/public/core/company-branding.js
pavlov346346-source 1e644e6dfb
Fix employee sync and expose mobile profile/logout (#37)
Keep notification-read state personal to user/company, preserve server read-only sections during staff synchronization, and respect separate order-create/edit/delete rights. Add visible mobile header session actions and a sticky logout that survives profile RPC failure; scope asynchronous profile/branding to user and workspace. Targeted browser suites and isolated SQL recovery tests passed in run 35506145405, iPhone screenshots reviewed. Full main QA remains required before production promotion. Workspace branding RPC migration is included but has NOT been applied to production Supabase; older servers retain safe owner-only fallback. No live membership/business-data repair is claimed without identifying the reported employee.
2026-09-20 13:55:56 +03:00

102 lines
8.5 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.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='';
const sidebarScope=()=>JSON.stringify([window.SunCloudV2?.getSession?.()?.user?.id||'',window.SunCloudV2?.getWorkspace?.()?.id||'']);
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;}
const ws=cloud.getWorkspace?.()?.id||'',scope=sidebarScope();
if(checkedUser===scope||pendingUser===scope){renderSidebar();return;}
resetSidebar();pendingUser=scope;const current=request;
try{
// New RPC verifies active membership and the selected company. An older
// database keeps its owner-only branding until the additive migration runs.
let result=ws?await cloud.getClient()?.rpc('caterium_workspace_sidebar_brand',{p_workspace:ws}):null;
if(!ws||['PGRST202','42883'].includes(result?.error?.code))result=await cloud.getClient()?.rpc('caterium_my_sidebar_brand');
if(current!==request||sidebarScope()!==scope)return;
if(result?.error||!result?.data)return;
if(result.data.variant==='solnce')sidebar={name:'Солнце Кейтеринг',logo:'sun-logo.png',special:true};
checkedUser=scope;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('sun:cloud-tenant-changing',resetSidebar);
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();
})();