173 lines
16 KiB
JavaScript
173 lines
16 KiB
JavaScript
(()=>{
|
|
'use strict';
|
|
if(window.CateriumDataV1772)return;
|
|
|
|
const VERSION='17.7.2';
|
|
const RELEASE='20260909-v17-7-2-clients-foundation';
|
|
const CLIENT_CACHE_KEY='cateriumClientsV1772';
|
|
const LEGACY_LOYALTY_KEY='sunClientLoyaltyV1';
|
|
const LEGACY_COMM_KEY='sunClientCommunicationV1';
|
|
const listeners=new Map();
|
|
const clone=value=>{try{return structuredClone(value)}catch(_){return JSON.parse(JSON.stringify(value))}};
|
|
const safeJson=(raw,fallback)=>{try{const value=JSON.parse(raw);return value==null?fallback:value}catch(_){return fallback}};
|
|
const emit=(topic,payload)=>{for(const fn of listeners.get(topic)||[]){try{fn(payload)}catch(error){console.error('[Caterium Data]',error)}}};
|
|
const subscribe=(topic,fn)=>{if(typeof fn!=='function')return()=>{};if(!listeners.has(topic))listeners.set(topic,new Set());listeners.get(topic).add(fn);return()=>listeners.get(topic)?.delete(fn)};
|
|
|
|
function runtimeOrders(){try{if(typeof orders!=='undefined'&&Array.isArray(orders))return orders}catch(_){}return null}
|
|
function runtimeCatalog(){try{if(typeof boxes!=='undefined'&&Array.isArray(boxes))return boxes}catch(_){}return null}
|
|
function runtimePersist(){try{return typeof persist==='function'?persist:null}catch(_){return null}}
|
|
function cloud(){return window.SunCloudV2||null}
|
|
function session(){try{return cloud()?.getSession?.()||null}catch(_){return null}}
|
|
function workspace(){try{return cloud()?.getWorkspace?.()||null}catch(_){return null}}
|
|
function supportMode(){try{return cloud()?.getSupportMode?.()||null}catch(_){return null}}
|
|
function isSignedIn(){return Boolean(session()?.user)}
|
|
function isSupportReadOnly(){return Boolean(supportMode()||cloud()?.isSupportMode?.())}
|
|
function canWrite(permission='app.write'){
|
|
if(isSupportReadOnly())return false;
|
|
if(!isSignedIn())return true;
|
|
try{const c=cloud();if(typeof c?.hasPermission==='function')return c.hasPermission(permission)||c.hasPermission('app.write')}catch(_){}
|
|
return true;
|
|
}
|
|
|
|
const storage={
|
|
read(key,fallback=null){const raw=localStorage.getItem(String(key));if(raw==null)return fallback;return safeJson(raw,fallback)},
|
|
write(key,value,{silent=false}={}){localStorage.setItem(String(key),JSON.stringify(value));if(!silent)emit(`storage:${key}`,clone(value));return value},
|
|
remove(key){localStorage.removeItem(String(key));emit(`storage:${key}`,null)},
|
|
raw(key){return localStorage.getItem(String(key))}
|
|
};
|
|
|
|
function persistDomain(key,list,persistLocal){
|
|
if(!persistLocal){storage.write(key,list,{silent:true});return}
|
|
const p=runtimePersist();
|
|
if(p){try{p();return}catch(error){console.warn('[Caterium Data] persist fallback',error)}}
|
|
storage.write(key,list,{silent:true});
|
|
}
|
|
|
|
function getOrders(){const live=runtimeOrders();return clone(live||storage.read('sunOrders',[])||[])}
|
|
function replaceOrders(next,{persistLocal=true,reason='replace'}={}){
|
|
const list=Array.isArray(next)?clone(next):[],live=runtimeOrders();
|
|
if(live){live.length=0;live.push(...clone(list))}
|
|
persistDomain('sunOrders',list,persistLocal);emit('orders',{reason,orders:clone(list)});
|
|
if(persistLocal)queueMicrotask(()=>window.CateriumDataV1772?.clients?.adoptLegacy?.({pushServer:true,reason:`orders:${reason}`}));
|
|
return list;
|
|
}
|
|
function getOrder(id){return getOrders().find(o=>String(o?.id)===String(id))||null}
|
|
function updateOrder(id,updater,{persistLocal=true,reason='update'}={}){
|
|
const list=getOrders(),index=list.findIndex(o=>String(o?.id)===String(id));if(index<0)return null;
|
|
const current=clone(list[index]),next=typeof updater==='function'?updater(current):{...current,...clone(updater||{})};if(!next)return null;
|
|
list[index]=next;replaceOrders(list,{persistLocal,reason});return clone(next);
|
|
}
|
|
function applyServerOrders(changed,{reason='server'}={}){
|
|
const patches=Array.isArray(changed)?changed:[];if(!patches.length)return {changed:0,ids:[]};
|
|
const list=getOrders(),map=new Map(list.map((o,i)=>[String(o?.id),i])),ids=[];
|
|
for(const patch of patches){const id=String(patch?.id??'');if(!id)continue;const index=map.get(id);if(index==null){list.push(clone(patch));map.set(id,list.length-1)}else list[index]=clone(patch);ids.push(id)}
|
|
replaceOrders(list,{persistLocal:false,reason});
|
|
try{window.render?.();window.renderOrders?.();window.renderStats?.();window.renderClients?.()}catch(_){}
|
|
try{window.dispatchEvent(new CustomEvent('caterium:data-orders-applied',{detail:{ids,reason}}))}catch(_){}
|
|
return {changed:ids.length,ids};
|
|
}
|
|
|
|
function getCatalog(){const live=runtimeCatalog();return clone(live||storage.read('sunBoxes',[])||[])}
|
|
function replaceCatalog(next,{persistLocal=true,reason='replace'}={}){
|
|
const list=Array.isArray(next)?clone(next):[],live=runtimeCatalog();
|
|
if(live){live.length=0;live.push(...clone(list))}
|
|
persistDomain('sunBoxes',list,persistLocal);emit('catalog',{reason,items:clone(list)});return list;
|
|
}
|
|
function getCatalogItem(id){return getCatalog().find(x=>String(x?.id)===String(id))||null}
|
|
function upsertCatalogItem(item,{persistLocal=true,reason='upsert'}={}){
|
|
if(!item?.id)return null;const list=getCatalog(),index=list.findIndex(x=>String(x?.id)===String(item.id)),next=clone(item);
|
|
if(index<0)list.push(next);else list[index]=next;replaceCatalog(list,{persistLocal,reason});return clone(next);
|
|
}
|
|
function removeCatalogItem(id,{persistLocal=true,reason='remove'}={}){
|
|
const list=getCatalog(),next=list.filter(x=>String(x?.id)!==String(id));if(next.length===list.length)return false;
|
|
replaceCatalog(next,{persistLocal,reason});return true;
|
|
}
|
|
|
|
const normalizePhone=value=>String(value||'').replace(/\D/g,'');
|
|
const normalizeName=value=>String(value||'').trim().replace(/\s+/g,' ').toLowerCase();
|
|
const clientKeyFromValues=(phone,name,orderId='')=>{const p=normalizePhone(phone);if(p)return `p:${p}`;const n=normalizeName(name);if(n)return `n:${n}`;return orderId!==''&&orderId!=null?`o:${String(orderId)}`:''};
|
|
const clientKeyFromOrder=order=>clientKeyFromValues(order?.phone,order?.contact,order?.id);
|
|
const orderStamp=order=>`${String(order?.date||'').padStart(10,'0')}T${String(order?.time||'').padStart(5,'0')}`;
|
|
function orderTotal(order){
|
|
const explicit=Number(order?.total);if(Number.isFinite(explicit))return explicit;
|
|
const catalog=getCatalog();return (Array.isArray(order?.lines)?order.lines:[]).reduce((sum,line)=>{const item=catalog.find(x=>String(x?.id)===String(line?.id));return sum+Number(item?.price||0)*Number(line?.qty||0)},0);
|
|
}
|
|
function readObject(key){const value=storage.read(key,{});return value&&typeof value==='object'&&!Array.isArray(value)?value:{}}
|
|
function cacheCore(profile){return {key:profile.key,name:String(profile.name||''),phone:String(profile.phone||''),latestAddress:String(profile.latestAddress||''),loyalty:profile.loyalty?clone(profile.loyalty):null,communication:profile.communication?clone(profile.communication):null,serverVersion:Number(profile.serverVersion||0)||null,serverUpdatedAt:String(profile.serverUpdatedAt||'')}}
|
|
function readClientCache(){return readObject(CLIENT_CACHE_KEY)}
|
|
function writeClientCache(map){localStorage.setItem(CLIENT_CACHE_KEY,JSON.stringify(map||{}));return map}
|
|
function buildClients(){
|
|
const cached=readClientCache(),loyalty=readObject(LEGACY_LOYALTY_KEY),communication=readObject(LEGACY_COMM_KEY),people=new Map();
|
|
const ensure=key=>{if(!people.has(key)){const old=cached[key]||{};people.set(key,{key,name:String(old.name||''),phone:String(old.phone||''),latestAddress:String(old.latestAddress||''),addresses:new Set(),orderIds:[],orderCount:0,totalSpent:0,latestStamp:'',loyalty:null,communication:null,serverVersion:Number(old.serverVersion||0)||null,serverUpdatedAt:String(old.serverUpdatedAt||'')})}return people.get(key)};
|
|
for(const order of getOrders()){
|
|
const key=clientKeyFromOrder(order);if(!key)continue;const person=ensure(key),stamp=orderStamp(order),address=String(order?.address||'').trim();
|
|
person.orderIds.push(String(order?.id??''));person.orderCount++;if(String(order?.status||'')!=='Отменён')person.totalSpent+=Math.max(0,Number(orderTotal(order)||0));if(address)person.addresses.add(address);
|
|
if(!person.latestStamp||stamp>=person.latestStamp){person.latestStamp=stamp;person.name=String(order?.contact||person.name||'');person.phone=String(order?.phone||person.phone||'');person.latestAddress=address||person.latestAddress;}
|
|
}
|
|
const allKeys=new Set([...people.keys(),...Object.keys(cached),...Object.keys(loyalty),...Object.keys(communication)]);
|
|
for(const key of allKeys){const person=ensure(key);person.loyalty=Object.prototype.hasOwnProperty.call(loyalty,key)?clone(loyalty[key]):null;person.communication=Object.prototype.hasOwnProperty.call(communication,key)?clone(communication[key]):null;if(person.latestAddress)person.addresses.add(person.latestAddress)}
|
|
return [...people.values()].map(person=>({...person,addresses:[...person.addresses],orderIds:person.orderIds.filter(Boolean),totalSpent:Math.round(person.totalSpent*100)/100,latestStamp:undefined})).sort((a,b)=>(b.orderCount-a.orderCount)||String(a.name||a.key).localeCompare(String(b.name||b.key),'ru'));
|
|
}
|
|
function listClients(){return clone(buildClients())}
|
|
function getClient(key){return listClients().find(x=>x.key===String(key))||null}
|
|
function writeLegacyMap(storageKey,key,value){
|
|
const map=readObject(storageKey);if(value==null)delete map[key];else map[key]=clone(value);
|
|
bridgeSuppressed=true;try{localStorage.setItem(storageKey,JSON.stringify(map))}finally{bridgeSuppressed=false}
|
|
return map;
|
|
}
|
|
function persistClientCacheFromList(list){const map={};for(const item of list||[])map[item.key]=cacheCore(item);writeClientCache(map);return map}
|
|
function changedClientKeys(before,next){const keys=new Set([...Object.keys(before||{}),...Object.keys(next||{})]),changed=[];for(const key of keys){if(JSON.stringify(before?.[key]||null)!==JSON.stringify(next?.[key]||null))changed.push(key)}return changed}
|
|
function adoptLegacy({pushServer=false,reason='legacy-adopt'}={}){
|
|
const before=readClientCache(),list=buildClients(),after={};for(const item of list)after[item.key]=cacheCore(item);const changed=changedClientKeys(before,after);writeClientCache(after);emit('clients',{reason,changedKeys:changed,clients:clone(list)});if(pushServer&&changed.length)scheduleServerPush(changed);return {changed:changed.length,keys:changed,clients:clone(list)};
|
|
}
|
|
function upsertClient(profile,{pushServer=true,reason='client.upsert'}={}){
|
|
const key=String(profile?.key||clientKeyFromValues(profile?.phone,profile?.name,profile?.orderId)||'');if(!key)return null;
|
|
const current=getClient(key)||{key,name:'',phone:'',latestAddress:'',loyalty:null,communication:null};const next={...current,...clone(profile),key};
|
|
if(Object.prototype.hasOwnProperty.call(profile||{},'loyalty'))writeLegacyMap(LEGACY_LOYALTY_KEY,key,next.loyalty);
|
|
if(Object.prototype.hasOwnProperty.call(profile||{},'communication'))writeLegacyMap(LEGACY_COMM_KEY,key,next.communication);
|
|
const cache=readClientCache();cache[key]=cacheCore(next);writeClientCache(cache);emit('clients',{reason,changedKeys:[key],clients:listClients()});if(pushServer)scheduleServerPush([key]);return clone(getClient(key)||next);
|
|
}
|
|
function setClientLoyalty(key,value,{pushServer=true}={}){writeLegacyMap(LEGACY_LOYALTY_KEY,String(key),value);adoptLegacy({pushServer:false,reason:'client.loyalty'});if(pushServer)scheduleServerPush([String(key)]);return clone(getClient(key)?.loyalty||null)}
|
|
function setClientCommunication(key,value,{pushServer=true}={}){writeLegacyMap(LEGACY_COMM_KEY,String(key),value);adoptLegacy({pushServer:false,reason:'client.communication'});if(pushServer)scheduleServerPush([String(key)]);return clone(getClient(key)?.communication||null)}
|
|
function serverPayload(profile){return {schemaVersion:1,key:profile.key,identity:{name:String(profile.name||''),phone:String(profile.phone||''),latestAddress:String(profile.latestAddress||'')},loyalty:profile.loyalty?clone(profile.loyalty):null,communication:profile.communication?clone(profile.communication):null,source:'data-layer-v1772',updatedAt:new Date().toISOString()}}
|
|
async function pushClient(key){
|
|
const profile=getClient(key);if(!profile)return {status:'missing',key};if(!isSignedIn())return {status:'local',key};if(isSupportReadOnly()||!canWrite('clients.edit'))return {status:'read-only',key};
|
|
const c=cloud()?.getClient?.(),ws=workspace();if(!c?.rpc||!ws?.id)return {status:'offline',key};
|
|
const cached=readClientCache()[key]||{},expected=Number(cached.serverVersion||0)||null;
|
|
const {data,error}=await c.rpc('sun_v17_save_client_v1772',{p_workspace:ws.id,p_client_key:key,p_profile:serverPayload(profile),p_expected_version:expected,p_client_id:cloud()?.getClientId?.()||'browser'});if(error)throw error;
|
|
const row=Array.isArray(data)?data[0]:data;if(row){const map=readClientCache();map[key]={...(map[key]||cacheCore(profile)),serverVersion:Number(row.version||0)||null,serverUpdatedAt:String(row.updated_at||'')};writeClientCache(map)}
|
|
return {status:'saved',key,version:Number(row?.version||0)||null};
|
|
}
|
|
const pendingServerKeys=new Set();let serverPushTimer=0;
|
|
function scheduleServerPush(keys){for(const key of keys||[])if(key)pendingServerKeys.add(String(key));if(serverPushTimer)return;serverPushTimer=setTimeout(async()=>{serverPushTimer=0;const keys=[...pendingServerKeys];pendingServerKeys.clear();for(const key of keys){try{await pushClient(key)}catch(error){console.warn('[Caterium Clients] server save failed',key,error?.message||error)}}},250)}
|
|
async function pushAllClients(){const result=[];for(const profile of listClients()){try{result.push(await pushClient(profile.key))}catch(error){result.push({status:'error',key:profile.key,error:String(error?.message||error)})}}return result}
|
|
|
|
let bridgeSuppressed=false;
|
|
function installLegacyClientBridge(){
|
|
if(window.__cateriumClientStorageBridgeV1772||typeof Storage==='undefined')return;window.__cateriumClientStorageBridgeV1772=true;
|
|
const proto=Storage.prototype,rawSet=proto.setItem,rawRemove=proto.removeItem;
|
|
proto.setItem=function(key,value){const result=rawSet.call(this,key,value);if(!bridgeSuppressed&&this===localStorage&&(key===LEGACY_LOYALTY_KEY||key===LEGACY_COMM_KEY))queueMicrotask(()=>window.CateriumDataV1772?.clients?.adoptLegacy?.({pushServer:true,reason:`legacy-write:${key}`}));return result};
|
|
proto.removeItem=function(key){const result=rawRemove.call(this,key);if(!bridgeSuppressed&&this===localStorage&&(key===LEGACY_LOYALTY_KEY||key===LEGACY_COMM_KEY))queueMicrotask(()=>window.CateriumDataV1772?.clients?.adoptLegacy?.({pushServer:true,reason:`legacy-remove:${key}`}));return result};
|
|
}
|
|
|
|
const ordersRepo={list:getOrders,get:getOrder,replace:replaceOrders,update:updateOrder,applyServerChanges:applyServerOrders};
|
|
const catalogRepo={list:getCatalog,get:getCatalogItem,replace:replaceCatalog,upsert:upsertCatalogItem,remove:removeCatalogItem};
|
|
const clientsRepo={list:listClients,get:getClient,upsert:upsertClient,setLoyalty:setClientLoyalty,setCommunication:setClientCommunication,adoptLegacy,push:pushClient,pushAll:pushAllClients,keyFromOrder:clientKeyFromOrder,keyFromValues:clientKeyFromValues,normalizePhone,normalizeName};
|
|
const diagnostics={
|
|
snapshot(){return {version:VERSION,workspaceId:workspace()?.id||null,signedIn:isSignedIn(),supportReadOnly:isSupportReadOnly(),orderCount:getOrders().length,catalogCount:getCatalog().length,clientCount:listClients().length,listenerTopics:listeners.size,pendingClientPushes:pendingServerKeys.size}},
|
|
measure(name,fn){const started=performance.now();return Promise.resolve().then(fn).finally(()=>emit('metric',{name,durationMs:Math.round((performance.now()-started)*10)/10,at:new Date().toISOString()}))}
|
|
};
|
|
|
|
const api={VERSION,RELEASE,storage,orders:ordersRepo,catalog:catalogRepo,clients:clientsRepo,subscribe,emit,cloud,session,workspace,isSignedIn,isSupportReadOnly,canWrite,diagnostics};
|
|
window.CateriumDataV1772=api;
|
|
window.CateriumDataV1771=api;
|
|
window.CateriumDataV1770=api;
|
|
window.CateriumData=api;
|
|
installLegacyClientBridge();
|
|
adoptLegacy({pushServer:false,reason:'boot'});
|
|
window.addEventListener('sun:cloud-permissions-changed',()=>setTimeout(()=>clientsRepo.pushAll().catch(()=>{}),800));
|
|
window.addEventListener('sun:cloud-sync-complete',()=>setTimeout(()=>clientsRepo.adoptLegacy({pushServer:true,reason:'cloud-sync'}),300));
|
|
setTimeout(()=>clientsRepo.pushAll().catch(()=>{}),3500);
|
|
try{window.dispatchEvent(new CustomEvent('caterium:data-ready',{detail:{version:VERSION,phase:3,clients:true}}))}catch(_){}
|
|
})();
|