1// ÐдинÑй ÑееÑÑÑ Ð±Ð»Ð¾ÐºÐ¾Ð² UI + ÑÐµÐ·Ð¾Ð»Ð²ÐµÑ Ð²Ð¸Ð´Ð¸Ð¼Ð¾ÑÑи (поÑÑановка â3) 2// ÐÑавило: ÑÑо не Ñвно ÑазÑеÑено (minRole) â запÑеÑено. 3 4const ROLE_RANK = {user:1, admin:2, moderator:3}; 5const _warnedMissingMinRole = new Set(); 6 7/** ÐеÑолÑÑ Ð¿ÐµÑÑоналÑнÑÑ Ð½Ð°ÑÑÑоек (ÑÐ¸Ð½Ñ Ñон Ñ auth.DEFAULT_USER_SETTINGS). */ 8const SETTING_DEFAULTS = { 9 theme: 'system', 10 experimental_analytics: true, 11 exp_blocks: {}, 12 seen_app_version: '', 13 view_as: 'self', 14 mail_onboarding_done: false, 15 mail_onboarding_stores_done: false, 16}; 17 18/** ÐопÑÑÑимÑе view_as (admin â на бÑдÑÑее, пока не в UI). */ 19const VIEW_AS_VALUES = {self:1, user:1, admin:1}; 20 21/** Ð Ð¾Ð»Ñ Ð´Ð»Ñ Ð²Ð¸Ð´Ð¸Ð¼Ð¾ÑÑи UI: Ñежим пÑоÑмоÑÑа или наÑÑоÑÑÐ°Ñ CURRENT_ROLE. */ 22function effectiveRole(){ 23 const settings = (typeof CURRENT_SETTINGS !== 'undefined' && CURRENT_SETTINGS) 24 || resolvedSettings(null); 25 const va = (settings && settings.view_as) || 'self'; 26 if(va === 'self' || !VIEW_AS_VALUES[va]) return CURRENT_ROLE || 'user'; 27 return va; 28} 29 30function warnMissingMinRole(block){ 31 const id = (block && block.id) || '(unknown)'; 32 if(_warnedMissingMinRole.has(id)) return; 33 _warnedMissingMinRole.add(id); 34 try{ console.warn('BLOCKS: missing minRole for "'+id+'" â denied (deny-by-default)'); }catch(e){} 35} 36 37/** minRole обÑзаÑелен; оÑÑÑÑÑÑвие / неизвеÑÑÐ½Ð°Ñ ÑÐ¾Ð»Ñ â deny. 38 * ÐÐ»Ñ UI иÑполÑзÑÐµÑ effectiveRole() (Ñежим «ÑмоÑÑеÑÑ ÐºÐ°ÐºÂ»). */ 39function roleAtLeast(minRole){ 40 if(minRole == null || minRole === '') return false; 41 const need = ROLE_RANK[minRole]; 42 if(need == null) return false; 43 return (ROLE_RANK[effectiveRole()]||0) >= need; 44} 45 46/** ÐаÑÑоÑÑÐ°Ñ ÑÐ¾Ð»Ñ ÑеÑÑии â Ð´Ð»Ñ ÑÐµÐ´ÐºÐ¸Ñ Ð¿ÑовеÑок вне ÑезолвеÑа видимоÑÑи. */ 47function realRoleAtLeast(minRole){ 48 if(minRole == null || minRole === '') return false; 49 const need = ROLE_RANK[minRole]; 50 if(need == null) return false; 51 return (ROLE_RANK[CURRENT_ROLE]||0) >= need; 52} 53 54function resolvedSettings(ctx){ 55 const base = Object.assign({}, SETTING_DEFAULTS); 56 const fromCtx = (ctx && ctx.settings) || (typeof CURRENT_SETTINGS !== 'undefined' ? CURRENT_SETTINGS : null) || {}; 57 return Object.assign(base, fromCtx); 58} 59 60function settingEnabled(key, ctx){ 61 const s = resolvedSettings(ctx); 62 if(s[key] === undefined || s[key] === null) return !!SETTING_DEFAULTS[key]; 63 return !!s[key]; 64} 65 66/** ÐкÑпеÑименÑалÑнÑй оÑÑÑÑ: обÑий ÑÑÐ¼Ð±Ð»ÐµÑ + лиÑнÑй клÑÑ (Ð½ÐµÑ ÐºÐ»ÑÑа = вкл). */ 67function expEnabled(id, s){ 68 s = s || resolvedSettings(null); 69 if(!s.experimental_analytics) return false; 70 const m = s.exp_blocks || {}; 71 return m[id] !== false; 72} 73 74/** Ðлоки-оÑÑÑÑÑ Ñ requiresSetting (= id блока) Ð´Ð»Ñ ÑпиÑка в «ÐÑÑ». */ 75function experimentalReportBlocks(){ 76 return BLOCKS.filter(b => b && b.requiresSetting && b.requiresSetting === b.id); 77} 78 79function currentContext(){ 80 return { 81 narrow: window.innerWidth <= 600, 82 role: effectiveRole(), 83 settings: resolvedSettings(null), 84 }; 85} 86 87function isBlockVisible(block, ctx, data){ 88 if(!block) return false; 89 if(!block.minRole){ warnMissingMinRole(block); return false; } 90 const need = ROLE_RANK[block.minRole]; 91 if(need == null){ warnMissingMinRole(block); return false; } 92 const role = (ctx && ctx.role) || effectiveRole(); 93 if((ROLE_RANK[role]||0) < need) return false; 94 const surface = (ctx && ctx.narrow) ? 'narrow' : 'wide'; 95 if(!(block.surfaces || []).includes(surface)) return false; 96 if(block.requiresSetting){ 97 const s = resolvedSettings(ctx); 98 // клÑÑ Ð±Ð»Ð¾ÐºÐ° (st.*) â ÑеÑез expEnabled; инаÑе обÑÑÐ½Ð°Ñ Ð½Ð°ÑÑÑойка 99 if(block.requiresSetting === block.id){ 100 if(!expEnabled(block.requiresSetting, s)) return false; 101 } else if(!settingEnabled(block.requiresSetting, ctx)){ 102 return false; 103 } 104 } 105 if(block.hasData && data != null && !block.hasData(data)) return false; 106 return true; 107} 108 109/** ÐидимоÑÑÑ Ð´Ð»Ñ Ð·Ð°Ð´Ð°Ð½Ð½Ð¾Ð¹ Ñоли/повеÑÑ Ð½Ð¾ÑÑи (ÑкÑан аÑдиÑа, без hasData). */ 110function isBlockVisibleFor(block, role, narrow, settings){ 111 if(!block) return false; 112 if(!block.minRole){ warnMissingMinRole(block); return false; } 113 const need = ROLE_RANK[block.minRole]; 114 if(need == null){ warnMissingMinRole(block); return false; } 115 if((ROLE_RANK[role]||0) < need) return false; 116 const surface = narrow ? 'narrow' : 'wide'; 117 if(!(block.surfaces || []).includes(surface)) return false; 118 if(block.requiresSetting){ 119 const s = settings != null ? Object.assign({}, SETTING_DEFAULTS, settings) : SETTING_DEFAULTS; 120 if(block.requiresSetting === block.id){ 121 if(!expEnabled(block.requiresSetting, s)) return false; 122 } else if(!settingEnabled(block.requiresSetting, {settings: s})){ 123 return false; 124 } 125 } 126 return true; 127} 128 129/** СкÑÑÑÑ ÑекÑÐ¸Ñ Ð¿Ð¾Ð²ÐµÑÑ .tab-block{display:block!important} на деÑкÑопе. */ 130function setRoleHidden(el, hidden){ 131 if(!el) return;
132 el.classList.toggle('role-hidden', !!hidden); 133 el.style.display = hidden ? 'none' : ''; 134} 135 136function blockOptions(block, ctx){ 137 const opts = block.options || {}; 138 const surface = (ctx && ctx.narrow) ? 'narrow' : 'wide'; 139 return Object.assign({}, opts[surface] || {}); 140} 141 142function blockCollapsed(block, ctx){ 143 const c = block.collapsed; 144 if(c && typeof c === 'object' && !Array.isArray(c)){ 145 const surface = (ctx && ctx.narrow) ? 'narrow' : 'wide'; 146 return !!c[surface]; 147 } 148 return !!c; 149} 150 151function getBlock(id){ 152 return BLOCKS.find(b => b.id === id) || null; 153} 154 155function blocksForSection(section){ 156 return BLOCKS.filter(b => b.section === section); 157} 158 159/** Ð Ð°Ð·Ð´ÐµÐ»Ñ Ð½Ð°Ð²Ð¸Ð³Ð°Ñии (поÑÑдок вкладок). ÐÑÐ´Ð¸Ñ â оÑделÑнÑй /audit, не в app-nav. */ 160const SECTION_DEFS = [ 161 {id:'purchases', title:'ÐокÑпки', icon:'shopping-bag'}, 162 {id:'stats', title:'СÑаÑиÑÑика', icon:'bar-chart-3'}, 163 {id:'shopping', title:'ÐополниÑÑ', icon:'shopping-cart'}, 164 {id:'more', title:'ÐÑÑ', icon:'more-vertical'}, 165]; 166 167/** 168 * РееÑÑÑ Ð±Ð»Ð¾ÐºÐ¾Ð². section:'more' â вкладка «ÐÑÑ» (бÑвÑие receipts/service + пÑнкÑÑ Ð¼ÐµÐ½Ñ). 169 */ 170const BLOCKS = [ 171 // --- ÐокÑпки --- 172 {id:'pur.total', title:'СÑмма за пеÑиод и пеÑеклÑÑаÑелÑ', section:'purchases', minRole:'user', surfaces:['narrow','wide']}, 173 {id:'pur.stores', title:'ЧипÑÑ Ð¼Ð°Ð³Ð°Ð·Ð¸Ð½Ð¾Ð²', section:'purchases', minRole:'user', surfaces:['narrow','wide']}, 174 {id:'pur.buyers', title:'ЧипÑÑ Ð¿Ð¾ÐºÑпаÑелей', section:'purchases', minRole:'user', surfaces:['narrow','wide']}, 175 {id:'pur.receipts', title:'СпиÑок Ñеков пеÑиода', section:'purchases', minRole:'user', surfaces:['narrow','wide'], collapsed:false}, 176 {id:'pur.buyer_set', title:'ÐÑÐ±Ð¾Ñ Â«ÐºÑо кÑпил» Ñ Ñека', section:'purchases', minRole:'user', surfaces:['narrow','wide'], 177 options:{ ownNameOnly: () => !roleAtLeast('admin') }}, 178 179 // --- СÑаÑиÑÑика --- 180 {id:'st.toolbar', title:'СвеÑнÑÑÑ Ð²ÑÑ', section:'stats', minRole:'user', surfaces:['narrow','wide']}, 181 {id:'st.buyers', title:'ÐÑо ÑколÑко поÑÑаÑил', section:'stats', minRole:'user', surfaces:['narrow','wide'], collapsed:true, 182 hasData:(d)=>{ 183 if(!d || !d.overall) return false; 184 const bs = d.overall.buyer_stats || {}; 185 const membersN = (ACCOUNT && ACCOUNT.members && ACCOUNT.members.length) || 0; 186 return membersN >= 2 || !!(bs.by_buyer && bs.by_buyer.length >= 1); 187 }}, 188 {id:'st.freqcat', title:'Ðде ÑÑаÑÑ Ð²ÑÑоÑли', section:'stats', minRole:'user', surfaces:['narrow','wide'], collapsed:true, 189 hasData:(d)=>{ 190 if(!d || !d.overall) return false; 191 const rc = d.overall.rising_categories || d.overall.frequent_categories || {}; 192 return !!(rc.has_base && rc.categories && rc.categories.length); 193 }, 194 mobCards:null}, 195 {id:'st.activity', title:'ÐагазинÑ', section:'stats', minRole:'user', surfaces:['narrow','wide'], collapsed:true, 196 hasData:(d)=> !!(d && d.overall && d.overall.count)}, 197 {id:'st.months', title:'ТоваÑÑ Ð¿Ð¾ меÑÑÑам', section:'stats', minRole:'user', surfaces:['narrow','wide'], collapsed:false, 198 hasData:(d)=> !!(d && d.months && d.months.length), 199 mobCards:{primary:0}, 200 options:{narrow:{itemsMode:'button'}, wide:{itemsMode:'button'}}}, 201 {id:'st.months.catedit', title:'ÐÑавка каÑегоÑий и ÑÑенки в позиÑиÑÑ Ð¼ÐµÑÑÑа', section:'stats', minRole:'moderator', surfaces:['narrow','wide']}, 202 {id:'st.total', title:'ÐÑого за вÑÑ Ð²ÑемÑ', section:'stats', minRole:'user', surfaces:['narrow','wide'], collapsed:true, 203 hasData:(d)=> !!(d && d.overall && d.overall.count), 204 mobCards:{primary:1, hide:[0]}, 205 options:{narrow:{flatDrill:false}, wide:{flatDrill:false}}}, 206 {id:'st.events', title:'СобÑÑиÑ', section:'stats', minRole:'user', surfaces:['narrow','wide'], collapsed:true, 207 hasData:(d)=> !!(d && d.processed && d.processed.some(r => r && r.is_event)), 208 mobCards:null}, 209 // ÐкÑпеÑименÑалÑÐ½Ð°Ñ Ð°Ð½Ð°Ð»Ð¸Ñика (â16/â30) â requiresSetting = id блока (клÑÑ Ð² exp_blocks) 210 {id:'st.mergecand', title:'Ðозможно, ÑÑо один ÑоваÑ', section:'stats', minRole:'moderator', surfaces:['narrow','wide'], collapsed:true, 211 requiresSetting:'st.mergecand', 212 hasData:(d)=> !!(d && d.overall && ((d.overall.merge_candidates||[]).length || (typeof HEAVY_PENDING!=='undefined' && HEAVY_PENDING))), 213 mobCards:null}, 214 {id:'st.dynamics', title:'Цена ÑоваÑа', section:'stats', minRole:'user', surfaces:['narrow','wide'], collapsed:true, 215 hasData:(d)=> !!(d && ((d.products && d.products.length) || (d.overall && d.overall.count))), 216 mobCards:null}, 217 {id:'st.pricecmp', title:'СÑавнение Ñен по магазинам', section:'stats', minRole:'moderator', surfaces:['narrow','wide'], collapsed:true, 218 requiresSetting:'st.pricecmp', 219 hasData:(d)=> !!(d && d.overall && d.overall.count), 220 mobCards:{primary:0}}, 221 {id:'st.mondyn', title:'Ðинамика по меÑÑÑам', section:'stats', minRole:'moderator', surfaces:['narrow','wide'], collapsed:true, 222 requiresSetting:'st.mondyn', 223 hasData:(d)=> !!(d && d.overall && d.overall.store_activity && (d.overall.store_activity.months||[]).length), 224 mobCards:null}, 225 {id:'st.season', title:'СезонноÑÑÑ ÐºÐ°ÑегоÑий', section:'stats', minRole:'moderator', surfaces:['narrow','wide'], collapsed:true, 226 requiresSetting:'st.season', 227 hasData:(d)=>{ 228 const s = d && d.overall && d.overall.category_seasonality; 229 return !!(s && s.enough_history && (s.categories||[]).length); 230 }, 231 mobCards:null}, 232
233 // --- Ðагазин --- 234 {id:'shop.basket', title:'ÐоÑа пополниÑÑ', section:'shopping', minRole:'user', surfaces:['narrow','wide']}, 235 {id:'shop.history', title:'ÐÑÑоÑÐ¸Ñ Ð¿Ð¾Ñ Ð¾Ð´Ð¾Ð²', section:'shopping', minRole:'user', surfaces:['narrow','wide'], collapsed:true}, 236 237 // --- ÐÑÑ: пÑнкÑÑ Ð¼ÐµÐ½Ñ (поÑÑановка â15) --- 238 {id:'more.me', title:'ÐаÑÑоÑка полÑзоваÑелÑ', section:'more', minRole:'user', surfaces:['narrow','wide']}, 239 {id:'more.my_mail', title:'ÐÐ¾Ñ Ð¿Ð¾ÑÑа Ð´Ð»Ñ Ñеков', section:'more', minRole:'user', surfaces:['narrow','wide']}, 240 {id:'more.theme', title:'ТÑÐ¼Ð½Ð°Ñ Ñема', section:'more', minRole:'user', surfaces:['narrow','wide']}, 241 {id:'add.search', title:'ÐоиÑк по ÑоваÑам', section:'more', minRole:'user', surfaces:['narrow','wide']}, 242 {id:'add.excel', title:'ÐÑгÑÑзиÑÑ Ð² Excel', section:'more', minRole:'user', surfaces:['narrow','wide']}, 243 {id:'hdr.members', title:'УÑаÑÑники', section:'more', minRole:'admin', surfaces:['narrow','wide']}, 244 {id:'reg.card', title:'РееÑÑÑ Ñеков', section:'more', minRole:'admin', surfaces:['narrow','wide']}, 245 {id:'reg.trash', title:'УдалÑннÑе Ñеки', section:'more', minRole:'admin', surfaces:['narrow','wide'], 246 hasData:(d)=> !!(typeof TRASH_COUNT === 'number' ? TRASH_COUNT > 0 : true)}, 247 {id:'more.group_mail', title:'ÐоÑÑа гÑÑппÑ', section:'more', minRole:'admin', surfaces:['narrow','wide']}, 248 {id:'more.home_work', title:'Ðом и ÑабоÑа', section:'more', minRole:'admin', surfaces:['narrow','wide']}, 249 {id:'add.manual', title:'ÐобавиÑÑ Ñеки вÑÑÑнÑÑ', section:'more', minRole:'moderator', surfaces:['narrow','wide']}, 250 {id:'more.price_check', title:'ÐÑовеÑиÑÑ ÑенÑ', section:'more', minRole:'moderator', surfaces:['narrow','wide']}, 251 {id:'more.cats_rules', title:'ÐаÑегоÑии и пÑавила', section:'more', minRole:'moderator', surfaces:['narrow','wide']}, 252 {id:'more.merge', title:'ÐбÑединение ÑоваÑов', section:'more', minRole:'moderator', surfaces:['narrow','wide']}, 253 {id:'hdr.groups', title:'ÐÑÑппÑ', section:'more', minRole:'moderator', surfaces:['narrow','wide']}, 254 {id:'more.experimental', title:'ÐкÑпеÑименÑалÑÐ½Ð°Ñ Ð°Ð½Ð°Ð»Ð¸Ñика', section:'more', minRole:'moderator', surfaces:['narrow','wide']}, 255 {id:'more.view_as', title:'СмоÑÑеÑÑ ÐºÐ°Ðº ÐолÑзоваÑелÑ', section:'more', minRole:'moderator', surfaces:['narrow','wide']}, 256 {id:'audit.table', title:'ÐÑÐ´Ð¸Ñ Ð±Ð»Ð¾ÐºÐ¾Ð²', section:'more', minRole:'moderator', surfaces:['narrow','wide']}, 257 {id:'more.update', title:'Ðбновление', section:'more', minRole:'user', surfaces:['narrow','wide'], collapsed:true}, 258 {id:'more.logout', title:'ÐÑйÑи', section:'more', minRole:'user', surfaces:['narrow','wide']}, 259 260 // --- ÐÑÑ: ÑабоÑие блоки (бÑвÑие receipts/service), id без изменений --- 261 {id:'add.mail', title:'ÐÑовеÑиÑÑ Ð¿Ð¾ÑÑÑ Ñ Ð²ÑбоÑом пеÑиода', section:'more', minRole:'moderator', surfaces:['narrow','wide'], 262 options:{narrow:{periodMode:'choices'}, wide:{periodMode:'slider'}}}, 263 {id:'add.collapse', title:'ÑвеÑнÑÑÑ', section:'more', minRole:'user', surfaces:['narrow','wide']}, 264 {id:'add.status', title:'СÑаÑÑÑÑ Ð¸ оÑибки анализа', section:'more', minRole:'admin', surfaces:['narrow','wide']}, 265 {id:'reg.filters', title:'ФилÑÑÑÑ ÑееÑÑÑа', section:'more', minRole:'admin', surfaces:['narrow','wide']}, 266 {id:'reg.list', title:'СпиÑок Ñеков ÑееÑÑÑа', section:'more', minRole:'admin', surfaces:['narrow','wide'], 267 mobCards:null}, 268 {id:'reg.edit', title:'РедакÑÐ¾Ñ Ð¿Ð¾Ð·Ð¸Ñий', section:'more', minRole:'moderator', surfaces:['narrow','wide']}, 269 {id:'reg.delete', title:'УдалиÑÑ Ñек', section:'more', minRole:'admin', surfaces:['narrow','wide']}, 270 {id:'reg.store_set', title:'Смена магазина Ñ Ñека', section:'more', minRole:'moderator', surfaces:['narrow','wide']}, 271 {id:'reg.points', title:'СпиÑаннÑе баллÑ', section:'more', minRole:'admin', surfaces:['narrow','wide']}, 272 {id:'reg.event_set', title:'ÐомеÑка Ñека как ÑобÑÑиÑ', section:'more', minRole:'admin', surfaces:['narrow','wide']}, 273 {id:'srv.cat_manage', title:'каÑегоÑии', section:'more', minRole:'moderator', surfaces:['narrow','wide']}, 274 {id:'srv.wordrules', title:'Ñлова-пÑавила', section:'more', minRole:'moderator', surfaces:['narrow','wide']}, 275 {id:'srv.merge_pick', title:'обÑединиÑÑ', section:'more', minRole:'moderator', surfaces:['narrow','wide']}, 276 {id:'srv.merge_groups', title:'обÑединÑннÑе', section:'more', minRole:'moderator', surfaces:['narrow','wide']}, 277 {id:'srv.clear', title:'УдалиÑÑ Ð²Ñе Ñеки', section:'more', minRole:'moderator', surfaces:['narrow','wide']}, 278 279 // --- Шапка / конÑекÑÑ Ð³ÑÑÐ¿Ð¿Ñ --- 280 {id:'hdr.profile', title:'ÐÑоÑÐ¸Ð»Ñ (Ð²Ñ Ð¾Ð´ в ÐÑÑ)', section:'more', minRole:'user', surfaces:['wide']}, 281 {id:'hdr.groupctx', title:'ÐлаÑка «ÐÑ ÑабоÑаеÑе в гÑÑппе â¦Â»', section:'groups', minRole:'moderator', surfaces:['narrow','wide']}, 282]; 283 284// --- ÐкÑан аÑдиÑа (Ñаг 2) --- 285 286const AUDIT_ROLES = ['user', 'admin', 'moderator']; 287const AUDIT_SURFACES = [ 288 {key:'wide', narrow:false, label:'wide'}, 289 {key:'narrow', narrow:true, label:'narrow'}, 290]; 291 292function renderAuditSection(){
293 const el = document.getElementById('auditSection'); 294 if(!el) return; 295 // доÑÑÑп по наÑÑоÑÑей Ñоли; маÑÑиÑа â по ÑикÑиÑованнÑм ÑолÑм, не по view_as 296 if(!realRoleAtLeast('moderator')){ 297 el.innerHTML = '<div class="muted" style="padding:24px">ÐоÑÑÑп ÑолÑко Ð´Ð»Ñ Ð¼Ð¾Ð´ÐµÑаÑоÑа. <a href="/">'+icon('arrow-left')+' на главнÑÑ</a></div>'; 298 return; 299 } 300 const liveSettings = resolvedSettings(null); 301 let h = '<div data-block-id="audit.table">'; 302 h += '<h2>'+icon('flask-conical')+' ÐÑÐ´Ð¸Ñ Ð±Ð»Ð¾ÐºÐ¾Ð²</h2>'; 303 h += '<div class="muted" style="font-size:12px;margin-bottom:10px;line-height:1.4">'+ 304 'ÐаÑÑиÑа видимоÑÑи по ÑееÑÑÑÑ (без ÑÑÑÑа hasData). Ðолонка «req» â requiresSetting. '+ 305 'ЯÑейки moderator: on/off Ð´Ð»Ñ experimental_analytics (обÑий ÑÑмблеÑ). '+ 306 'СейÑÐ°Ñ Ñ Ð²Ð°Ñ: experimental_analytics='+ 307 (liveSettings.experimental_analytics ? 'true' : 'false')+ 308 ', вÑкл. оÑÑÑÑов: '+Object.keys(liveSettings.exp_blocks||{}).filter(k=>liveSettings.exp_blocks[k]===false).join(', ')+'.</div>'; 309 h += '<div style="margin-bottom:10px"><button type="button" class="ghost" onclick="runBlockAuditDiff()">ÐÑовеÑиÑÑ ÑаÑÑ Ð¾Ð¶Ð´ÐµÐ½Ð¸Ñ</button> '+ 310 '<a class="ghost" href="/" style="display:inline-block;padding:8px 12px;text-decoration:none">'+icon('arrow-left')+' кÑÑаÑ</a></div>'; 311 h += '<div id="auditDiffResult" class="muted" style="font-size:12px;margin-bottom:10px"></div>'; 312 h += '<div style="overflow:auto;max-width:100%"><table class="audit-matrix" style="font-size:11px;border-collapse:collapse;width:100%">'; 313 h += '<tr><th style="text-align:left;padding:4px 6px;border-bottom:1px solid var(--line)">Ðлок</th>'; 314 h += '<th style="padding:4px 6px;border-bottom:1px solid var(--line)">req</th>'; 315 AUDIT_ROLES.forEach(role=>{ 316 AUDIT_SURFACES.forEach(sf=>{ 317 h += '<th style="padding:4px 6px;border-bottom:1px solid var(--line);white-space:nowrap">'+role+'/'+sf.label+'</th>'; 318 }); 319 }); 320 h += '<th style="padding:4px 6px;border-bottom:1px solid var(--line);white-space:nowrap">mod/exp off</th>'; 321 h += '</tr>'; 322 BLOCKS.forEach(b=>{ 323 h += '<tr><td style="padding:4px 6px;border-bottom:1px solid #eee;white-space:nowrap" title="'+escHtml(b.id)+'">'+ 324 '<code style="font-size:10px">'+escHtml(b.id)+'</code> '+escHtml(b.title)+'</td>'; 325 h += '<td style="padding:4px 6px;border-bottom:1px solid #eee;font-size:10px;color:var(--muted)">'+ 326 (b.requiresSetting ? escHtml(b.requiresSetting) : '·')+'</td>'; 327 AUDIT_ROLES.forEach(role=>{ 328 AUDIT_SURFACES.forEach(sf=>{ 329 const vis = isBlockVisibleFor(b, role, sf.narrow, SETTING_DEFAULTS); 330 h += '<td style="text-align:center;padding:4px;border-bottom:1px solid #eee;color:'+(vis?'#0a8a3a':'#bbb')+'">'+(vis?icon('check','sm'):'·')+'</td>'; 331 }); 332 }); 333 const visOff = isBlockVisibleFor(b, 'moderator', false, Object.assign({}, SETTING_DEFAULTS, {experimental_analytics:false})); 334 h += '<td style="text-align:center;padding:4px;border-bottom:1px solid #eee;color:'+(visOff?'#0a8a3a':'#bbb')+'">'+(visOff?icon('check','sm'):'·')+'</td>'; 335 h += '</tr>'; 336 }); 337 h += '</table></div>'; 338 const reqN = BLOCKS.filter(b=>b.requiresSetting).length; 339 h += '<div class="muted" style="font-size:11px;margin-top:8px">ÐÑего блоков: '+BLOCKS.length+ 340 ' · Ñ requiresSetting: '+reqN+'</div>'; 341 h += '</div>'; 342 el.innerHTML = h; 343} 344 345function runBlockAuditDiff(){ 346 const out = document.getElementById('auditDiffResult'); 347 if(!realRoleAtLeast('moderator')){ 348 if(out) out.textContent = 'ÐоÑÑÑп ÑолÑко Ð´Ð»Ñ Ð¼Ð¾Ð´ÐµÑаÑоÑа.'; 349 return; 350 } 351 // ÑвеÑка Ñ Ð½Ð°ÑÑоÑÑей ÑолÑÑ, не Ñ view_as 352 const ctx = { 353 narrow: window.innerWidth <= 600, 354 role: CURRENT_ROLE || 'user', 355 settings: resolvedSettings(null), 356 }; 357 const data = (typeof LAST !== 'undefined') ? LAST : null; 358 const diffs = []; 359 BLOCKS.forEach(b=>{ 360 const expect = isBlockVisible(b, ctx, data === null ? undefined : data); 361 const nodes = document.querySelectorAll('[data-block-id="'+CSS.escape(b.id)+'"]'); 362 const inDom = nodes.length > 0;
363 let shown = false; 364 nodes.forEach(n=>{ 365 if(n.offsetParent !== null || (n.getClientRects && n.getClientRects().length)) 366 shown = true; 367 // tab-block ÑкÑÑÑ ÑеÑез CSS на Ñзком â ÑÑиÑаем «оÑÑиÑован», еÑли еÑÑÑ Ð² DOM и tab-show / не narrow 368 if(n.classList.contains('tab-block')){ 369 if(!ctx.narrow || n.classList.contains('tab-show')) shown = true; 370 } 371 }); 372 // Ðлоки еÑÑ Ð½Ðµ помеÑеннÑе data-block-id â пÑопÑÑкаем Ñ Ð¿Ð¾Ð¼ÐµÑкой pending 373 if(!inDom){ 374 diffs.push({id:b.id, kind:'no-marker', expect, note:'Ð½ÐµÑ data-block-id в DOM (еÑÑ Ð½Ðµ мигÑиÑован)'}); 375 return; 376 } 377 if(expect && !shown) 378 diffs.push({id:b.id, kind:'missing', expect, note:'ÑееÑÑÑ: видно, DOM: ÑкÑÑÑо'}); 379 if(!expect && shown) 380 diffs.push({id:b.id, kind:'extra', expect, note:'ÑееÑÑÑ: ÑкÑÑÑо, DOM: видно'}); 381 }); 382 if(!out) return; 383 const pending = diffs.filter(d=>d.kind==='no-marker').length; 384 const real = diffs.filter(d=>d.kind!=='no-marker'); 385 if(!real.length){ 386 out.innerHTML = '<span style="color:#0a8a3a">РаÑÑ Ð¾Ð¶Ð´ÐµÐ½Ð¸Ð¹ по ÑазмеÑеннÑм блокам неÑ.</span> '+ 387 '<span class="muted">Ðез маÑкеÑа (еÑÑ Ð½Ðµ мигÑиÑованÑ): '+pending+'</span>'; 388 } else { 389 out.innerHTML = '<div style="color:#b00020">РаÑÑ Ð¾Ð¶Ð´ÐµÐ½Ð¸Ð¹: '+real.length+'</div><ul style="margin:6px 0;padding-left:18px">'+ 390 real.slice(0,40).map(d=>'<li><code>'+escHtml(d.id)+'</code> â '+escHtml(d.note)+'</li>').join('')+ 391 (real.length>40?'<li>â¦Ð¸ еÑÑ '+(real.length-40)+'</li>':'')+ 392 '</ul><div class="muted">Ðез маÑкеÑа: '+pending+'</div>'; 393 } 394} 395 396try{ console.log('BLOCKS.length =', BLOCKS.length); }catch(e){} 397 398/** HTML пеÑиода Ð´Ð»Ñ add.mail (slider / choices). */ 399function renderMailPeriodControl(opts){ 400 const mode = (opts && opts.periodMode) || 'slider'; 401 if(mode === 'hidden') return ''; 402 if(mode === 'choices'){ 403 return '<div class="mail-days-wrap" data-block-id="add.mail" id="mailDaysWrap">'+ 404 '<button type="button" class="ghost mail-days-choice" data-days="2" onclick="setMailDaysChoice(2)">за 2 днÑ</button>'+ 405 '<button type="button" class="ghost mail-days-choice" data-days="240" onclick="setMailDaysChoice(240)">вÑÑ Ð¸ÑÑоÑиÑ</button>'+ 406 '<input type="hidden" id="mailDays" value="2">'+ 407 '<span id="mailFirstHint" class="mail-first-hint" style="display:none">ÐеÑвÑй Ñаз: вÑбеÑиÑе «вÑÑ Ð¸ÑÑоÑиÑ»</span>'+ 408 '</div>'; 409 } 410 return '<div class="mail-days-wrap" data-block-id="add.mail" id="mailDaysWrap">'+ 411 '<span>ÐеÑиод: <b id="mailDaysVal">2</b> дн.</span>'+ 412 '<input type="range" id="mailDays" min="2" max="240" value="2" oninput="updateMailDaysLabel()">'+ 413 '<span id="mailFirstHint" class="mail-first-hint" style="display:none">ÐеÑвÑй Ñаз: поÑÑавÑÑе 240 дн â подÑÑнÑÑÑ Ð²ÑÑ Ð¸ÑÑоÑÐ¸Ñ Ñеков</span>'+ 414 '</div>'; 415} 416 417function setMailDaysChoice(days){ 418 const inp = document.getElementById('mailDays'); 419 if(inp) inp.value = String(days); 420 document.querySelectorAll('.mail-days-choice').forEach(btn=>{ 421 btn.classList.toggle('active', Number(btn.dataset.days) === Number(days)); 422 }); 423} 424 425function renderToolbarBlockHtml(id, ctx){ 426 const b = getBlock(id); 427 if(!b || !isBlockVisible(b, ctx)) return ''; 428 const opts = blockOptions(b, ctx); 429 switch(id){ 430 case 'add.manual': 431 return '<button class="primary" id="go" data-block-id="add.manual" onclick="addAndRun()">ÐобавиÑÑ Ð¸ анализ <span class="spin">â¦</span></button>'; 432 case 'add.mail': 433 return '<span data-block-id="add.mail" style="display:inline-flex;align-items:center;gap:8px;flex-wrap:wrap">'+ 434 '<button class="ghost" onclick="checkMail()">'+icon('mail')+' ÐÑовеÑиÑÑ Ð¿Ð¾ÑÑÑ</button>'+ 435 renderMailPeriodControl(opts)+'</span>'; 436 case 'add.search': 437 return '<button class="ghost" data-block-id="add.search" onclick="openProductSearch()">'+icon('search')+' ÑоваÑÑ</button>'; 438 case 'add.collapse': 439 return '<button class="ghost" data-block-id="add.collapse" onclick="collapseAll()">'+icon('chevron-up')+' ÑвеÑнÑÑÑ</button>'; 440 case 'add.excel': 441 return '<button class="green" id="xls" data-block-id="add.excel" onclick="downloadXlsx()" disabled>Excel</button>'; 442 case 'srv.cat_manage': 443 return '<button class="ghost" data-block-id="srv.cat_manage" onclick="manageCategories()">'+icon('pencil')+' каÑегоÑии</button>'; 444 case 'srv.wordrules':
445 return '<button class="ghost" data-block-id="srv.wordrules" onclick="manageWordRules()">'+icon('file-text')+' Ñлова-пÑавила</button>'; 446 case 'srv.merge_pick': 447 return '<button class="ghost" data-block-id="srv.merge_pick" id="btnMergePick" onclick="mergeSelected()" disabled>'+icon('link-2')+' обÑединиÑÑ</button>'; 448 case 'srv.merge_groups': 449 return '<button class="ghost" data-block-id="srv.merge_groups" onclick="manageMergeGroups()">'+icon('link-2')+' обÑединÑннÑе</button>'; 450 case 'srv.clear': 451 return '<button class="ghost danger" data-block-id="srv.clear" onclick="clearAll()">УдалиÑÑ Ð²Ñе Ñеки</button>'; 452 default: 453 return ''; 454 } 455} 456 457function renderReceiptsToolbar(){ 458 const ctx = currentContext(); 459 const bar = document.getElementById('receiptsToolbar'); 460 const manual = document.getElementById('addManualWrap'); 461 const card = document.getElementById('receiptsCard'); 462 const narrow = ctx.narrow; 463 const onMoreTab = !narrow || (typeof mobileTab !== 'undefined' && mobileTab === 'more'); 464 const inAdd = (typeof MORE_VIEW !== 'undefined' && MORE_VIEW === 'add' && onMoreTab); 465 if(manual){ 466 const b = getBlock('add.manual'); 467 manual.style.display = (inAdd && b && isBlockVisible(b, ctx)) ? '' : 'none'; 468 } 469 ['saveinfo','analyzeStatus','errors'].forEach(id=>{ 470 const el = document.getElementById(id); 471 const b = getBlock('add.status'); 472 if(el) el.style.display = (inAdd && b && isBlockVisible(b, ctx)) ? '' : 'none'; 473 }); 474 if(!bar) return; 475 const order = ['add.manual','add.mail','add.collapse']; 476 bar.innerHTML = inAdd ? order.map(id=> renderToolbarBlockHtml(id, ctx)).join('') : ''; 477 // hint ÑолÑко когда оÑкÑÑÑ ÑкÑан Ð´Ð¾Ð±Ð°Ð²Ð»ÐµÐ½Ð¸Ñ Ñеков â не пÑи каждом bootstrap 478 if(inAdd && typeof refreshMailScanHint === 'function') refreshMailScanHint(); 479 if(typeof updateMailDaysLabel === 'function') updateMailDaysLabel(); 480 if(card){ 481 const any = inAdd && ['add.manual','add.mail','add.collapse','add.status'] 482 .some(id=>{ const b=getBlock(id); return b && isBlockVisible(b, ctx); }); 483 card.style.display = any ? '' : 'none'; 484 card.classList.toggle('more-drill-open', !!any); 485 } 486} 487
488function renderServiceToolbar(){ 489 // ÐÐ°Ð½ÐµÐ»Ñ Â«ÐбÑлÑживание» ÑбÑана из ленÑÑ â дейÑÑÐ²Ð¸Ñ Ð¸Ð· Ð¼ÐµÐ½Ñ Â«ÐÑÑ» (модалки). 490 const sec = document.getElementById('serviceSection'); 491 const bar = document.getElementById('serviceToolbar'); 492 if(bar) bar.innerHTML = ''; 493 if(sec) setRoleHidden(sec, true); 494} 495 496function applyRegistryPanels(){ 497 renderReceiptsToolbar(); 498 renderServiceToolbar(); 499 if(typeof applyMoreView === 'function') applyMoreView(); 500 if(typeof rebuildMobileTabBar === 'function'){ 501 rebuildMobileTabBar(); 502 applyMobileTabs(); 503 } 504}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.