153 lines
33 KiB
JavaScript
153 lines
33 KiB
JavaScript
(()=>{
|
||
'use strict';
|
||
if(window.SunDeveloperUXV1768)return;
|
||
|
||
const VERSION='17.6.8';
|
||
const RELEASE='20260908-v17-6-8-developer-console-ux';
|
||
const TAB_KEY='sunDeveloperActiveTabV22';
|
||
const ENHANCED_TABS=new Set(['companies','accounts','plans','journal']);
|
||
const ASYNC_ROOTS={features:'sunDevFeatureBody',backups:'sunDevBackupBody'};
|
||
const FEATURES={orders:'Заказы',calendar:'Календарь',map:'Карта',clients:'Клиенты',catalog_view:'Каталог · просмотр',catalog_edit:'Каталог · редактирование',production:'Производство',shopping:'Закупки',stock:'Склад',routes:'Маршруты',mailings:'Рассылки',money:'Финансы',stats_basic:'Статистика',stats_advanced:'Расширенная статистика',team:'Команда',suppliers:'Поставщики',print:'Печать',settings:'Настройки',branding:'Фирменное оформление',client_offers:'Предложение клиенту',offer_templates:'Шаблоны предложения',backups:'Резервные копии',audit:'Журнал',users_manage:'Пользователи и права'};
|
||
const PLANS={basic:'Базовый',professional:'Профессиональный',full:'Полный'};
|
||
const KNOWN_DOM_RACE=/Cannot set properties of null \(setting 'className'\)|Cannot read properties of undefined \(reading 'add'\)/i;
|
||
const $=id=>document.getElementById(id);
|
||
const qa=(sel,root=document)=>[...root.querySelectorAll(sel)];
|
||
const esc=value=>window.SunSafe?.escapeHTML?window.SunSafe.escapeHTML(String(value??'')):String(value??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||
const cloud=()=>window.SunCloudV2||null;
|
||
const client=()=>cloud()?.getClient?.()||null;
|
||
const dev=()=>window.SunDeveloperV22||null;
|
||
const fmtDate=v=>{if(!v)return'—';try{return new Date(v).toLocaleString('ru-RU',{dateStyle:'short',timeStyle:'short'})}catch(_){return String(v)}};
|
||
const fmtDay=v=>{if(!v)return'—';try{return new Date(v).toLocaleDateString('ru-RU')}catch(_){return String(v)}};
|
||
const toast=(text,type='info',ms=4500)=>{try{return window.SunEnterprise?.toast?.(text,type,ms)}catch(_){}console[type==='error'?'error':'log'](text)};
|
||
const activeTab=()=>document.querySelector('#sun-developer-console-v22 [data-dev-tab].on')?.dataset.devTab||localStorage.getItem(TAB_KEY)||'overview';
|
||
const developerVisible=()=>Boolean($('sun-developer-console-v22')?.classList?.contains('on'));
|
||
async function rpc(name,args={}){const c=client();if(!c)throw new Error('Supabase не подключён.');const r=await c.rpc(name,args);if(r.error)throw r.error;return r.data}
|
||
async function audit(action,workspace=null,user=null,details={}){try{await rpc('sun_dev_log_event',{p_action:action,p_workspace:workspace,p_user:user,p_details:details})}catch(_){}}
|
||
|
||
function installStyles(){
|
||
if($('sunDeveloperUXV1768Style'))return;
|
||
const st=document.createElement('style');st.id='sunDeveloperUXV1768Style';st.textContent=`
|
||
.sun-dev-number{display:inline-flex;align-items:center;justify-content:center;min-width:34px;height:26px;padding:0 8px;border-radius:999px;background:#171815;color:#fff;font-size:11px;font-weight:900;white-space:nowrap}
|
||
.sun-dev-company-title{display:flex;align-items:center;gap:8px}.sun-dev-company-title b{font-size:14px}
|
||
.sun-dev-company-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:10px}.sun-dev-company-pick{border:1px solid #e0e3e7;border-radius:15px;background:#fff;padding:15px;text-align:left;cursor:pointer;box-shadow:0 6px 22px rgba(20,22,26,.035)}.sun-dev-company-pick:hover{border-color:#b99a5f;box-shadow:0 8px 28px rgba(20,22,26,.07)}.sun-dev-company-pick h3{display:flex;align-items:center;gap:8px;margin:0 0 8px;color:#252b2f}.sun-dev-company-pick p{margin:4px 0;color:#747b83;font-size:12px}.sun-dev-company-pick .count{font-size:22px;font-weight:900;color:#171815;margin-top:10px}
|
||
.sun-dev-account-head{display:flex;align-items:center;gap:10px;justify-content:space-between;flex-wrap:wrap;margin-bottom:12px}.sun-dev-account-head-left{display:flex;align-items:center;gap:9px;flex-wrap:wrap}.sun-dev-account-role{display:inline-flex;padding:4px 8px;border-radius:999px;background:#f1f3f5;font-size:11px;font-weight:800}.sun-dev-account-role.admin{background:#e9f5ec;color:#22633b}
|
||
.sun-dev-plan-top{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px;margin-bottom:12px}.sun-dev-plan-mini{background:#fff;border:1px solid #e2e4e9;border-radius:14px;padding:13px}.sun-dev-plan-mini h3{margin:0 0 3px}.sun-dev-plan-mini p{margin:0 0 9px;color:#777d86;font-size:11px;min-height:16px}.sun-dev-plan-limit{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px;align-items:end}.sun-dev-plan-limit input{width:100%;box-sizing:border-box;padding:8px 9px;border:1px solid #d8dbe0;border-radius:8px}.sun-dev-plan-matrix{min-width:620px}.sun-dev-plan-matrix th:not(:first-child),.sun-dev-plan-matrix td:not(:first-child){text-align:center;width:150px}.sun-dev-plan-matrix input[type=checkbox]{width:18px;height:18px;accent-color:#171815}.sun-dev-plan-feature{font-weight:750;color:#31373c}
|
||
.sun-dev-journal-kpis{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px;margin-bottom:12px}.sun-dev-journal-ok{border:1px solid #cae8d5;background:#f2fbf5;border-radius:15px;padding:18px}.sun-dev-journal-ok h3{margin:0 0 5px;color:#17633a}.sun-dev-error-message{max-width:560px;white-space:normal;word-break:break-word}.sun-dev-error-count{font-size:12px;font-weight:900;padding:4px 8px;border-radius:999px;background:#f1f2f4;white-space:nowrap}.sun-dev-error-level{display:inline-flex;padding:4px 8px;border-radius:999px;font-size:10px;font-weight:900;text-transform:uppercase}.sun-dev-error-level.error{background:#fde9e9;color:#9e2828}.sun-dev-error-level.warn{background:#fff3dc;color:#855800}
|
||
.sun-dev-modal-v1768{position:fixed;inset:0;z-index:100100;background:rgba(20,22,26,.54);display:grid;place-items:center;padding:18px}.sun-dev-modal-v1768-card{width:min(620px,100%);max-height:90vh;overflow:auto;background:#fff;border-radius:18px;padding:20px;box-shadow:0 24px 80px rgba(0,0,0,.28)}.sun-dev-modal-v1768-head{display:flex;align-items:center;justify-content:space-between;gap:10px}.sun-dev-modal-v1768-head h2{margin:0}.sun-dev-modal-v1768-close{border:0;border-radius:10px;width:36px;height:36px;background:#f1f2f4;font-size:22px}.sun-dev-modal-v1768-card label{display:block;margin-top:10px}.sun-dev-modal-v1768-card input,.sun-dev-modal-v1768-card select{width:100%;box-sizing:border-box;padding:9px 10px;border:1px solid #d6d9df;border-radius:9px}.sun-dev-modal-v1768-actions{display:flex;gap:8px;justify-content:flex-end;flex-wrap:wrap;margin-top:16px}
|
||
@media(max-width:900px){.sun-dev-plan-top,.sun-dev-journal-kpis{grid-template-columns:1fr}.sun-dev-plan-mini p{min-height:0}}
|
||
`;document.head.appendChild(st);
|
||
}
|
||
|
||
function modal(title,html){
|
||
$('sunDevModalV1768')?.remove();
|
||
const host=document.createElement('div');host.id='sunDevModalV1768';host.className='sun-dev-modal-v1768';host.innerHTML=`<div class="sun-dev-modal-v1768-card"><div class="sun-dev-modal-v1768-head"><h2>${esc(title)}</h2><button class="sun-dev-modal-v1768-close" type="button" data-close>×</button></div><div style="margin-top:12px">${html}</div></div>`;document.body.appendChild(host);
|
||
const close=()=>host.remove();host.addEventListener('click',e=>{if(e.target===host||e.target.closest('[data-close]'))close()});return {host,close,body:host.querySelector('.sun-dev-modal-v1768-card')};
|
||
}
|
||
function setTabChrome(tab){localStorage.setItem(TAB_KEY,tab);qa('#sun-developer-console-v22 [data-dev-tab]').forEach(b=>b.classList.toggle('on',b.dataset.devTab===tab))}
|
||
function body(){return $('sunDevBody')}
|
||
|
||
async function getCompanies(){
|
||
let rows;
|
||
try{rows=await rpc('sun_dev_list_companies_v1768')}catch(_){rows=await rpc('sun_dev_list_companies')}
|
||
rows=Array.isArray(rows)?rows:[];
|
||
const sorted=[...rows].sort((a,b)=>new Date(a.created_at||0)-new Date(b.created_at||0)||String(a.workspace_id).localeCompare(String(b.workspace_id)));
|
||
sorted.forEach((r,i)=>{if(!Number(r.company_number))r.company_number=i+1});
|
||
return sorted.sort((a,b)=>Number(a.company_number||0)-Number(b.company_number||0));
|
||
}
|
||
async function getUsers(){const rows=await rpc('sun_dev_list_users');return Array.isArray(rows)?rows:[]}
|
||
|
||
async function renderCompanies(root){
|
||
const rows=await getCompanies();if(!root?.isConnected||activeTab()!=='companies')return;
|
||
root.innerHTML=`<div class="sun-dev-toolbar"><label style="flex:1">Поиск<input id="sunDevCompanySearchV1768" placeholder="№, название или email владельца"></label><button class="primary" id="sunDevCreateCompanyV1768" type="button">Создать компанию</button></div><div class="sun-dev-table-wrap"><table class="sun-dev-table"><thead><tr><th>№</th><th>Компания</th><th>Владелец</th><th>Тариф</th><th>Доступ</th><th>Сотрудники</th><th>Создана</th><th>Действия</th></tr></thead><tbody id="sunDevCompaniesTbodyV1768"></tbody></table></div>`;
|
||
const draw=()=>{
|
||
const tbody=$('sunDevCompaniesTbodyV1768');if(!tbody)return;
|
||
const q=String($('sunDevCompanySearchV1768')?.value||'').trim().toLowerCase();
|
||
const filtered=rows.filter(r=>`${r.company_number||''} ${r.workspace_name||''} ${r.owner_email||''}`.toLowerCase().includes(q));
|
||
tbody.innerHTML=filtered.map(r=>`<tr data-ws="${esc(r.workspace_id)}"><td><span class="sun-dev-number">№${Number(r.company_number||0)}</span></td><td><div class="sun-dev-company-title"><b>${esc(r.workspace_name||'Компания')}</b></div><div class="sun-dev-muted">${esc(r.workspace_id)}</div></td><td>${esc(r.owner_email||r.pending_owner_email||'—')}${r.pending_owner_email?'<div class="sun-dev-pill warn">ожидает регистрации</div>':''}</td><td>${esc(PLANS[r.plan_id]||r.plan_name||r.plan_id||'—')}<div class="sun-dev-muted">до ${fmtDay(r.trial_ends_at||r.current_period_end)}</div></td><td><span class="sun-dev-pill ${r.access_mode==='full'?'ok':r.access_mode==='blocked'?'bad':'warn'}">${esc(r.access_mode||r.status||'—')}</span></td><td>${Number(r.member_count||0)} / ${r.max_members==null?'∞':Number(r.max_members)}</td><td>${fmtDay(r.created_at)}</td><td><div class="sun-dev-actions"><button class="outline" data-support>Поддержка</button><button class="outline" data-diag>Диагностика</button><button class="outline" data-plan>Тариф</button><button class="outline sun-dev-danger" data-block>${r.access_mode==='blocked'?'Разблокировать':'Заблокировать'}</button><button class="outline sun-dev-danger" data-delete>Удалить</button></div></td></tr>`).join('')||'<tr><td colspan="8" class="sun-dev-empty">Компаний не найдено.</td></tr>';
|
||
qa('tr[data-ws]',tbody).forEach(tr=>{const row=rows.find(x=>String(x.workspace_id)===tr.dataset.ws);tr.querySelector('[data-support]').onclick=()=>enterSupport(row);tr.querySelector('[data-diag]').onclick=()=>showDiagnostics(row);tr.querySelector('[data-plan]').onclick=()=>showCompanyPlan(row);tr.querySelector('[data-block]').onclick=()=>toggleCompanyBlock(row);tr.querySelector('[data-delete]').onclick=()=>deleteCompany(row)});
|
||
};
|
||
draw();$('sunDevCompanySearchV1768').oninput=draw;$('sunDevCreateCompanyV1768').onclick=createCompany;
|
||
}
|
||
|
||
function createCompany(){
|
||
const ui=modal('Создать компанию',`<label>Название<input id="sunDevNewCompanyNameV1768" value="Новая компания"></label><label>Email владельца<input id="sunDevNewOwnerEmailV1768" type="email" placeholder="owner@example.com"></label><div style="display:grid;grid-template-columns:1fr 1fr;gap:10px"><label>Тариф<select id="sunDevNewPlanV1768"><option value="basic">Базовый</option><option value="professional">Профессиональный</option><option value="full" selected>Полный</option></select></label><label>Срок, дней<input id="sunDevNewDaysV1768" type="number" min="1" max="3650" value="30"></label></div><p class="sun-dev-muted">Номер присвоится автоматически и больше не изменится, даже если другие компании будут удалены.</p><div class="sun-dev-modal-v1768-actions"><button class="outline" data-close>Отмена</button><button class="primary" id="sunDevNewCreateV1768">Создать</button></div><div id="sunDevNewErrorV1768" class="sun-dev-muted"></div>`);
|
||
$('sunDevNewCreateV1768').onclick=async()=>{const name=String($('sunDevNewCompanyNameV1768')?.value||'').trim(),email=String($('sunDevNewOwnerEmailV1768')?.value||'').trim(),plan=$('sunDevNewPlanV1768')?.value||'full',days=Math.max(1,Number($('sunDevNewDaysV1768')?.value||30));if(!name){$('sunDevNewErrorV1768').textContent='Введите название.';return}const btn=$('sunDevNewCreateV1768');btn.disabled=true;$('sunDevNewErrorV1768').textContent='Создаю…';try{await rpc('sun_dev_create_company',{p_name:name,p_owner_email:email||null,p_plan:plan,p_days:days,p_boxes:[],p_catalog_version:String(window.SUN_OFFICIAL_CATALOG_VERSION||'')});ui.close();toast('Компания создана и получила следующий номер.','success');renderEnhancedTab('companies')}catch(e){btn.disabled=false;$('sunDevNewErrorV1768').textContent=e?.message||String(e)}};
|
||
}
|
||
async function deleteCompany(row){
|
||
if(!row)return;const name=String(row.workspace_name||'');
|
||
if(!confirm(`Удалить компанию №${Number(row.company_number||0)} «${name}»?\n\nБудут удалены её заказы, клиенты, каталог, настройки, чаты и резервные копии. Сами аккаунты пользователей из Auth удаляться не будут.`))return;
|
||
const typed=prompt(`Для подтверждения введите точное название компании:\n${name}`,'');if(typed===null)return;if(String(typed).trim()!==name){toast('Удаление отменено: название не совпало.','warn');return}
|
||
try{await rpc('sun_dev_delete_company_v1768',{p_workspace:row.workspace_id,p_confirm_name:name});toast(`Компания №${Number(row.company_number||0)} удалена. Её номер больше не будет использоваться.`,`success`,6500);renderEnhancedTab('companies')}catch(e){toast(e?.message||String(e),'error',6500)}
|
||
}
|
||
async function toggleCompanyBlock(row){const blocked=row?.access_mode==='blocked';if(!blocked&&!confirm(`Заблокировать компанию «${row.workspace_name}»? Данные не удалятся.`))return;try{await rpc('sun_dev_set_subscription',{p_workspace:row.workspace_id,p_plan:row.plan_id||'full',p_days:blocked?30:0,p_status:blocked?'active':'expired'});await audit(blocked?'company.unblock':'company.block',row.workspace_id,null,{company:row.workspace_name});toast(blocked?'Компания разблокирована.':'Компания заблокирована.','success');renderEnhancedTab('companies')}catch(e){toast(e?.message||String(e),'error')}}
|
||
function showCompanyPlan(row){const ui=modal(`Тариф · №${Number(row.company_number||0)} ${row.workspace_name}`,`<label>Тариф<select id="sunDevPlanSelectV1768"><option value="basic" ${row.plan_id==='basic'?'selected':''}>Базовый</option><option value="professional" ${row.plan_id==='professional'?'selected':''}>Профессиональный</option><option value="full" ${row.plan_id==='full'?'selected':''}>Полный</option></select></label><label>Продлить на, дней<select id="sunDevPlanDaysV1768"><option value="30">30</option><option value="90">90</option><option value="365">365</option></select></label><div class="sun-dev-modal-v1768-actions"><button class="outline" data-close>Отмена</button><button class="primary" id="sunDevPlanSaveV1768">Применить</button></div>`);$('sunDevPlanSaveV1768').onclick=async()=>{try{const plan=$('sunDevPlanSelectV1768').value,days=Number($('sunDevPlanDaysV1768').value);await rpc('sun_dev_set_subscription',{p_workspace:row.workspace_id,p_plan:plan,p_days:days,p_status:'active'});await audit('company.subscription.set',row.workspace_id,null,{plan,days});ui.close();toast('Тариф обновлён.','success');renderEnhancedTab('companies')}catch(e){toast(e?.message||String(e),'error')}}}
|
||
async function showDiagnostics(row){try{const d=await rpc('sun_dev_workspace_diagnostics',{p_workspace:row.workspace_id});modal(`Диагностика · №${Number(row.company_number||0)} ${row.workspace_name}`,`<div class="sun-dev-grid" style="grid-template-columns:repeat(2,minmax(0,1fr))"><div class="sun-dev-kpi"><small>Заказы</small><b>${Number(d.orders||0)}</b></div><div class="sun-dev-kpi"><small>Клиенты</small><b>${Number(d.clients||0)}</b></div><div class="sun-dev-kpi"><small>Каталог</small><b>${Number(d.catalog_items||0)}</b></div><div class="sun-dev-kpi"><small>Ошибки 24 ч.</small><b>${Number(d.errors_24h||0)}</b></div></div><p><b>Revision:</b> ${Number(d.revision||0)}</p><p><b>Обновлено:</b> ${fmtDate(d.state_updated_at)}</p><p><b>Последняя копия:</b> ${fmtDate(d.latest_backup)}</p><p><b>Режим доступа:</b> ${esc(d.access_mode||'—')}</p>`)}catch(e){toast(e?.message||String(e),'error')}}
|
||
async function enterSupport(row){if(!row)return;try{await cloud()?.enterSupportWorkspace?.(row.workspace_id,row.workspace_name);await audit('support.ui_enter',row.workspace_id,null,{company:row.workspace_name});for(const fn of ['render','renderOrders','renderStats']){try{window[fn]?.()}catch(_){}}const orders=qa('header nav button').find(b=>String(b.dataset.navLabel||b.textContent||'').trim()==='Заказы');orders?.click();toast('Режим поддержки открыт. Изменения не будут сохранены.','info',6500)}catch(e){toast(e?.message||String(e),'error')}}
|
||
|
||
let selectedAccountsWorkspace=null;
|
||
async function renderAccounts(root){
|
||
const [companies,rows]=await Promise.all([getCompanies(),getUsers()]);if(!root?.isConnected||activeTab()!=='accounts')return;
|
||
const userMap=new Map();for(const r of rows){const id=String(r.user_id);if(!userMap.has(id))userMap.set(id,{id,email:r.email,created_at:r.created_at,last_sign_in_at:r.last_sign_in_at,is_platform_admin:r.is_platform_admin===true,workspaces:[]});if(r.workspace_id)userMap.get(id).workspaces.push({id:r.workspace_id,name:r.workspace_name,role:r.role,display_name:r.display_name,is_active:r.is_active!==false})}
|
||
const users=[...userMap.values()];
|
||
const groups=companies.map(c=>({...c,users:users.filter(u=>u.workspaces.some(w=>String(w.id)===String(c.workspace_id)))}));
|
||
const orphan=users.filter(u=>!(u.workspaces||[]).length);
|
||
const selected=selectedAccountsWorkspace==='__orphan'?{workspace_id:'__orphan',company_number:null,workspace_name:'Без компании',users:orphan}:groups.find(g=>String(g.workspace_id)===String(selectedAccountsWorkspace));
|
||
if(selected){
|
||
root.innerHTML=`<div class="sun-dev-account-head"><div class="sun-dev-account-head-left"><button class="outline" id="sunDevAccountsBackV1768" type="button">← Компании</button>${selected.company_number?`<span class="sun-dev-number">№${Number(selected.company_number)}</span>`:''}<div><h2 style="margin:0">${esc(selected.workspace_name)}</h2><div class="sun-dev-muted">${selected.users.length} ${selected.users.length===1?'аккаунт':'аккаунтов'}</div></div></div></div><div class="sun-dev-table-wrap"><table class="sun-dev-table"><thead><tr><th>Аккаунт</th><th>Роль в компании</th><th>Регистрация</th><th>Последний вход</th><th>Статус</th></tr></thead><tbody>${selected.users.map(u=>{const memberships=u.workspaces.filter(w=>selected.workspace_id==='__orphan'||String(w.id)===String(selected.workspace_id));return `<tr><td><b>${esc(u.email||'—')}</b>${u.is_platform_admin?'<div><span class="sun-dev-pill ok">Разработчик</span></div>':''}<div class="sun-dev-muted">${esc(u.id)}</div></td><td>${memberships.length?memberships.map(w=>`<span class="sun-dev-account-role ${w.role==='admin'?'admin':''}">${esc(w.role||'—')}</span>${w.display_name?`<div class="sun-dev-muted">${esc(w.display_name)}</div>`:''}`).join(''):'—'}</td><td>${fmtDate(u.created_at)}</td><td>${fmtDate(u.last_sign_in_at)}</td><td>${memberships.some(w=>w.is_active===false)?'<span class="sun-dev-pill warn">неактивен</span>':'<span class="sun-dev-pill ok">активен</span>'}</td></tr>`}).join('')||'<tr><td colspan="5" class="sun-dev-empty">В этой компании аккаунтов пока нет.</td></tr>'}</tbody></table></div>`;
|
||
$('sunDevAccountsBackV1768').onclick=()=>{selectedAccountsWorkspace=null;renderEnhancedTab('accounts')};return;
|
||
}
|
||
root.innerHTML=`<div class="sun-dev-toolbar"><label style="flex:1">Поиск компании<input id="sunDevAccountsCompanySearchV1768" placeholder="№ или название компании"></label><span class="sun-dev-pill">${users.length} аккаунтов всего</span></div><div id="sunDevAccountsCompanyGridV1768" class="sun-dev-company-grid"></div>`;
|
||
const draw=()=>{const grid=$('sunDevAccountsCompanyGridV1768');if(!grid)return;const q=String($('sunDevAccountsCompanySearchV1768')?.value||'').trim().toLowerCase();const list=groups.filter(g=>`${g.company_number||''} ${g.workspace_name||''}`.toLowerCase().includes(q));grid.innerHTML=list.map(g=>`<button class="sun-dev-company-pick" type="button" data-account-company="${esc(g.workspace_id)}"><h3><span class="sun-dev-number">№${Number(g.company_number||0)}</span>${esc(g.workspace_name)}</h3><p>${esc(g.owner_email||'Владелец не указан')}</p><div class="count">${g.users.length}</div><p>${g.users.length===1?'созданный аккаунт':'созданных аккаунтов'}</p></button>`).join('')+(orphan.length?`<button class="sun-dev-company-pick" type="button" data-account-company="__orphan"><h3>Без компании</h3><p>Системные или ещё не привязанные аккаунты</p><div class="count">${orphan.length}</div><p>аккаунтов</p></button>`:'');qa('[data-account-company]',grid).forEach(b=>b.onclick=()=>{selectedAccountsWorkspace=b.dataset.accountCompany;renderEnhancedTab('accounts')})};draw();$('sunDevAccountsCompanySearchV1768').oninput=draw;
|
||
}
|
||
|
||
async function renderPlans(root){
|
||
const c=client();if(!c)throw new Error('Supabase не подключён.');const [plansR,featuresR]=await Promise.all([c.from('sun_plans').select('id,name,description,max_members,is_active,sort_order').order('sort_order'),c.from('sun_plan_features').select('plan_id,feature_key,enabled')]);if(plansR.error)throw plansR.error;if(featuresR.error)throw featuresR.error;if(!root?.isConnected||activeTab()!=='plans')return;
|
||
const plans=plansR.data||[],rows=featuresR.data||[],featureKeys=[...new Set([...Object.keys(FEATURES),...rows.map(r=>r.feature_key)])];
|
||
root.innerHTML=`<div class="sun-dev-plan-top">${plans.map(p=>`<div class="sun-dev-plan-mini" data-plan-mini="${esc(p.id)}"><h3>${esc(p.name)}</h3><p>${esc(p.description||'')}</p><div class="sun-dev-plan-limit"><label style="margin:0">Сотрудники<input data-plan-max type="number" min="1" placeholder="∞" value="${p.max_members??''}"></label><button class="outline" data-plan-max-save type="button">Сохранить</button></div></div>`).join('')}</div><div class="sun-dev-table-wrap"><table class="sun-dev-table sun-dev-plan-matrix"><thead><tr><th>Функция</th>${plans.map(p=>`<th>${esc(p.name)}</th>`).join('')}</tr></thead><tbody>${featureKeys.map(f=>`<tr><td class="sun-dev-plan-feature">${esc(FEATURES[f]||f)}</td>${plans.map(p=>{const row=rows.find(x=>x.plan_id===p.id&&x.feature_key===f);return `<td><input type="checkbox" data-plan-feature="${esc(p.id)}" data-feature="${esc(f)}" ${row?.enabled?'checked':''} aria-label="${esc(p.name)} — ${esc(FEATURES[f]||f)}"></td>`}).join('')}</tr>`).join('')}</tbody></table></div><p class="sun-dev-muted" style="margin-top:10px">Матрица компактная: строка — функция, столбец — тариф. Изменения чекбоксов сохраняются сразу.</p>`;
|
||
qa('[data-plan-mini]',root).forEach(card=>{card.querySelector('[data-plan-max-save]').onclick=async()=>{const raw=card.querySelector('[data-plan-max]').value;try{await rpc('sun_dev_set_plan_max_members',{p_plan:card.dataset.planMini,p_max_members:raw===''?null:Number(raw)});toast('Лимит сотрудников сохранён.','success')}catch(e){toast(e?.message||String(e),'error')}}});
|
||
qa('[data-plan-feature]',root).forEach(ch=>ch.onchange=async()=>{ch.disabled=true;try{await rpc('sun_dev_set_plan_feature',{p_plan:ch.dataset.planFeature,p_feature:ch.dataset.feature,p_enabled:ch.checked});toast('Функция тарифа обновлена.','success',2500)}catch(e){ch.checked=!ch.checked;toast(e?.message||String(e),'error')}finally{ch.disabled=false}});
|
||
}
|
||
|
||
async function errorGroups(){
|
||
try{return await rpc('sun_dev_error_groups_v1768',{p_hours:168,p_limit:100})||[]}catch(_){const raw=await rpc('sun_dev_list_errors',{p_workspace:null,p_limit:100})||[],map=new Map();for(const e of raw){const key=`${e.workspace_name||''}|${e.app_version||''}|${e.level||''}|${e.message||''}`;if(!map.has(key))map.set(key,{workspace_name:e.workspace_name,app_version:e.app_version,level:e.level,message:e.message,occurrences:0,first_seen:e.created_at,last_seen:e.created_at});const g=map.get(key);g.occurrences++;if(new Date(e.created_at)<new Date(g.first_seen))g.first_seen=e.created_at;if(new Date(e.created_at)>new Date(g.last_seen))g.last_seen=e.created_at}return [...map.values()]}
|
||
}
|
||
async function renderJournal(root){
|
||
const [activity,groups]=await Promise.all([rpc('sun_dev_list_activity',{p_limit:120}),errorGroups()]);if(!root?.isConnected||activeTab()!=='journal')return;
|
||
const prod=(groups||[]).filter(g=>String(g.source_url||'').startsWith('file:')===false),errors=prod.filter(g=>String(g.level||'error').toLowerCase()==='error'),warns=prod.filter(g=>String(g.level||'').toLowerCase()!=='error'),total=prod.reduce((s,g)=>s+Number(g.occurrences||0),0);
|
||
root.innerHTML=`<div class="sun-dev-journal-kpis"><div class="sun-dev-kpi"><small>Актуальные типы ошибок · 7 дней</small><b>${errors.length}</b></div><div class="sun-dev-kpi"><small>Предупреждения · 7 дней</small><b>${warns.length}</b></div><div class="sun-dev-kpi"><small>События этих типов</small><b>${total}</b></div></div>${prod.length?`<div class="sun-dev-card" style="padding:0;overflow:hidden"><div style="padding:14px 16px 5px"><h2>Актуальные ошибки приложения</h2><p class="sun-dev-muted">Повторы сгруппированы. Старые локальные file:// копии больше не попадают в этот журнал.</p></div><div class="sun-dev-table-wrap" style="border:0;border-radius:0"><table class="sun-dev-table"><thead><tr><th>Последний раз</th><th>Компания</th><th>Уровень</th><th>Кол-во</th><th>Версия</th><th>Сообщение</th></tr></thead><tbody>${prod.map(g=>`<tr><td>${fmtDate(g.last_seen)}</td><td>${esc(g.workspace_name||'—')}</td><td><span class="sun-dev-error-level ${String(g.level||'error').toLowerCase()==='error'?'error':'warn'}">${esc(g.level||'error')}</span></td><td><span class="sun-dev-error-count">${Number(g.occurrences||0)}</span></td><td>${esc(g.app_version||'—')}</td><td class="sun-dev-error-message"><b>${esc(g.message||'')}</b>${g.sample_stack?`<details><summary class="sun-dev-muted">Стек</summary><pre class="sun-dev-pre">${esc(g.sample_stack)}</pre></details>`:''}</td></tr>`).join('')}</tbody></table></div></div>`:`<div class="sun-dev-journal-ok"><h3>Актуальных ошибок нет</h3><div>За последние 7 дней production-журнал чист. Старые ошибки от локальных копий отфильтрованы на сервере.</div></div>`}<details class="sun-dev-card" style="margin-top:12px"><summary style="cursor:pointer;font-weight:850">Действия разработчика · ${(activity||[]).length}</summary><div class="sun-dev-table-wrap" style="margin-top:12px"><table class="sun-dev-table"><thead><tr><th>Дата</th><th>Действие</th><th>Компания / аккаунт</th><th>Детали</th></tr></thead><tbody>${(activity||[]).map(a=>`<tr><td>${fmtDate(a.created_at)}</td><td><b>${esc(a.action)}</b><div class="sun-dev-muted">${esc(a.actor_email||'')}</div></td><td>${esc(a.workspace_name||a.target_email||'—')}</td><td class="sun-dev-muted">${esc(JSON.stringify(a.details||{}))}</td></tr>`).join('')||'<tr><td colspan="4">Записей пока нет.</td></tr>'}</tbody></table></div></details>`;
|
||
}
|
||
|
||
async function renderEnhancedTab(tab){
|
||
if(!ENHANCED_TABS.has(tab))return false;const root=body();if(!root)return false;setTabChrome(tab);root.innerHTML='<div class="sun-dev-empty">Загрузка…</div>';
|
||
try{if(tab==='companies')await renderCompanies(root);else if(tab==='accounts')await renderAccounts(root);else if(tab==='plans')await renderPlans(root);else if(tab==='journal')await renderJournal(root)}catch(e){if(root.isConnected&&activeTab()===tab)root.innerHTML=`<div class="sun-dev-card"><h3>Не удалось загрузить раздел</h3><p>${esc(e?.message||e)}</p></div>`;toast(e?.message||String(e),'error')};return true;
|
||
}
|
||
|
||
function sentinel(id){let el=$(id);if(el)return el;el=document.createElement('div');el.id=id;el.hidden=true;el.dataset.sunV1768Sentinel='1';document.body.appendChild(el);return el}
|
||
function removeSentinel(id){const el=$(id);if(el?.dataset?.sunV1768Sentinel==='1')el.remove()}
|
||
function prepareAsyncRoots(nextTab){const current=activeTab();for(const [tab,id] of Object.entries(ASYNC_ROOTS)){if(nextTab===tab)removeSentinel(id);else if(current===tab)queueMicrotask(()=>{if(activeTab()!==tab)sentinel(id)})}}
|
||
|
||
let installed=false,originalOpen=null,originalActivate=null;
|
||
function patchDeveloper(){
|
||
const d=dev();if(!d||installed)return false;installed=true;originalOpen=typeof d.open==='function'?d.open.bind(d):null;originalActivate=typeof d.activateTab==='function'?d.activateTab.bind(d):null;
|
||
if(originalOpen){d.open=async source=>{const wanted=localStorage.getItem(TAB_KEY)||'overview',enhanced=ENHANCED_TABS.has(wanted),safeSource=source instanceof HTMLElement?source:null;if(enhanced)localStorage.setItem(TAB_KEY,'overview');let result;try{result=await originalOpen(safeSource)}catch(e){if(!KNOWN_DOM_RACE.test(String(e?.message||e)))throw e;console.warn('[Caterium v17.6.8] Developer DOM race recovered',e);localStorage.setItem(TAB_KEY,'overview');try{result=await originalOpen(null)}catch(second){throw second}}finally{if(enhanced)localStorage.setItem(TAB_KEY,wanted)}if(enhanced)await renderEnhancedTab(wanted);return result};Object.defineProperty(d,'__sunV1768SafeOpen',{value:true,configurable:true})}
|
||
if(originalActivate){d.activateTab=async(tab,force=false)=>ENHANCED_TABS.has(tab)?renderEnhancedTab(tab):originalActivate(tab,force)}
|
||
return true;
|
||
}
|
||
|
||
function patchToast(){const e=window.SunEnterprise;if(!e||e.__sunV1768Toast||typeof e.toast!=='function')return;const original=e.toast.bind(e);e.toast=(text,...args)=>{const msg=String(text||'');if(developerVisible()&&KNOWN_DOM_RACE.test(msg)){console.warn('[Caterium v17.6.8] suppressed recovered Developer DOM race:',msg);return null}return original(text,...args)};Object.defineProperty(e,'__sunV1768Toast',{value:true,configurable:true})}
|
||
function recoverError(event,error){const msg=String(error?.message||error||'');if(!developerVisible()||!KNOWN_DOM_RACE.test(msg))return false;console.warn('[Caterium v17.6.8] recovered Developer Console DOM race:',error);event?.preventDefault?.();event?.stopImmediatePropagation?.();const tab=activeTab();setTimeout(()=>{if(ENHANCED_TABS.has(tab))renderEnhancedTab(tab);else dev()?.activateTab?.(tab,true)?.catch?.(()=>{})},60);return true}
|
||
|
||
function onClick(e){
|
||
const tabBtn=e.target?.closest?.('#sun-developer-console-v22 [data-dev-tab]');
|
||
if(tabBtn){const next=tabBtn.dataset.devTab;prepareAsyncRoots(next);if(ENHANCED_TABS.has(next)){e.preventDefault();e.stopImmediatePropagation();renderEnhancedTab(next);return}}
|
||
const refresh=e.target?.closest?.('#sunDevRefresh');if(refresh&&ENHANCED_TABS.has(activeTab())){e.preventDefault();e.stopImmediatePropagation();renderEnhancedTab(activeTab())}
|
||
}
|
||
document.addEventListener('click',onClick,true);
|
||
window.addEventListener('unhandledrejection',e=>recoverError(e,e.reason));
|
||
window.addEventListener('error',e=>recoverError(e,e.error||e.message));
|
||
|
||
function boot(){installStyles();patchToast();if(!patchDeveloper()){let tries=0;const timer=setInterval(()=>{tries++;patchToast();if(patchDeveloper()||tries>40)clearInterval(timer)},400)}setInterval(()=>{if(!document.hidden){patchToast();patchDeveloper()}},10000)}
|
||
window.SunDeveloperUXV1768={VERSION,RELEASE,renderEnhancedTab,getCompanies,getUsers,patchDeveloper,checks:()=>({version:VERSION,installed,activeTab:activeTab(),visible:developerVisible()})};
|
||
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',boot,{once:true});else boot();
|
||
})();
|