File manager - Edit - /home/webtrixia/.trash/hubs.html.1
Back
<!DOCTYPE html> <html lang="bg"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Партньорски хъбове — Webtrixia Hub</title> <link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@300;400;500;600&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet"> <style> :root{--bg:#f0f0f8;--sur:#fff;--sur2:#f5f5fc;--sur3:#eaeaf5;--brd:rgba(0,0,0,.08);--tx:#1a1a2e;--tx2:#4a4a6a;--tx3:#8888aa; --acc:#6c63ff;--grn:#22c55e;--gdim:rgba(34,197,94,.14);--red:#ef4444;--rdim:rgba(239,68,68,.14);--amb:#f59e0b;--adim:rgba(245,158,11,.14);--fn:'DM Sans',sans-serif;--mo:'DM Mono',monospace} *{margin:0;padding:0;box-sizing:border-box} body{font-family:var(--fn);background:var(--bg);color:var(--tx);min-height:100vh;padding:22px 18px 60px} .wrap{max-width:1200px;margin:0 auto} .top{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;flex-wrap:wrap;gap:10px} h1{font-size:20px;font-weight:600} .sub{font-size:12px;color:var(--tx3);margin-top:2px} .lnk{color:var(--acc);text-decoration:none;font-size:12px} .statgrid{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin-bottom:16px} @media(max-width:860px){.statgrid{grid-template-columns:repeat(2,1fr)}} .statcard{background:var(--sur);border:1px solid var(--brd);border-radius:14px;padding:16px} .statcard .lbl{font-size:11px;color:var(--tx3);text-transform:uppercase;letter-spacing:.06em} .statcard b{font-size:22px;display:block;margin-top:4px} .panel{background:var(--sur);border:1px solid var(--brd);border-radius:16px;padding:18px;margin-bottom:16px} .ph{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;flex-wrap:wrap;gap:8px} .pt{font-size:14px;font-weight:600} .btn{padding:9px 15px;border-radius:9px;border:none;cursor:pointer;font-family:var(--fn);font-size:12.5px;font-weight:600} .btn.p{background:var(--acc);color:#fff} .btn.s{background:var(--sur2);color:var(--tx2);border:1px solid var(--brd)} .btn.d{background:var(--rdim);color:var(--red)} .btn.g{background:var(--gdim);color:#15803d} .btn.a{background:var(--adim);color:#b45309} table{width:100%;border-collapse:collapse;font-size:12.5px} th{text-align:left;padding:8px 9px;color:var(--tx3);font-weight:500;border-bottom:1px solid var(--brd);white-space:nowrap} td{padding:9px;border-bottom:1px solid var(--brd);vertical-align:middle} label{display:block;font-size:11px;color:var(--tx3);text-transform:uppercase;letter-spacing:.06em;margin:10px 0 5px} input,select{width:100%;background:var(--sur2);border:1px solid var(--brd);border-radius:9px;padding:10px 12px;color:var(--tx);font-family:var(--fn);font-size:13px;outline:none} input:focus,select:focus{border-color:var(--acc)} .row3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px} .row4{display:grid;grid-template-columns:1.2fr 1.6fr 1.4fr .8fr;gap:10px} @media(max-width:860px){.row3,.row4{grid-template-columns:1fr}} .dot{width:8px;height:8px;border-radius:50%;display:inline-block;margin-right:6px} .dot.ok{background:var(--grn)}.dot.err{background:var(--red)} .modchip{display:inline-block;margin:3px 4px 0 0;padding:6px 11px;border-radius:8px;font-size:11.5px;font-weight:600;cursor:pointer;border:1px solid var(--brd)} .modchip.on{background:var(--gdim);color:#15803d;border-color:rgba(34,197,94,.3)} .modchip.off{background:var(--sur2);color:var(--tx3)} .badge{padding:3px 9px;border-radius:20px;font-size:11px;font-weight:600;white-space:nowrap} .badge.ok{background:var(--gdim);color:#15803d} .badge.overdue{background:var(--rdim);color:#b91c1c} .badge.stopped{background:var(--sur3);color:var(--tx3)} .small{font-size:11px;color:var(--tx3)} .hubcard{border:1px solid var(--brd);border-radius:14px;padding:16px;margin-bottom:14px;background:var(--sur)} .toast{position:fixed;bottom:20px;right:20px;background:#1a1a2e;color:#fff;padding:12px 18px;border-radius:10px;font-size:13px;display:none;z-index:1200} .iconbtn{background:var(--sur2);border:1px solid var(--brd);border-radius:7px;padding:5px 9px;cursor:pointer;font-size:12px} </style> </head> <body> <div class="wrap"> <div class="top"> <div><h1>🏢 Партньорски хъбове</h1><div class="sub">Клиенти с хъб · дистанционни модули · месечни абонаменти</div></div> <a href="index.html" class="lnk">← Обратно към Hub</a> </div> <div class="statgrid" id="stats"></div> <div class="panel"> <div class="ph"><div class="pt">Добави клиентски хъб</div></div> <div class="row4"> <div><label>Име на клиент</label><input id="h_name" placeholder="напр. Строй Инвест ЕООД"></div> <div><label>URL на хъба</label><input id="h_url" placeholder="https://hub.klient.com"></div> <div><label>License key (от тяхната инсталация)</label><input id="h_key" placeholder="ключът от w_hub_install"></div> <div><label> </label><button class="btn p" style="width:100%" onclick="addHub()">+ Добави</button></div> </div> <p class="small" style="margin-top:8px">Ключът се показва в края на инсталацията им (или в тяхната таблица branding_settings → license_key). С него включваш/изключваш модулите им дистанционно.</p> </div> <div class="panel" id="overduePanel" style="display:none"> <div class="ph"><div class="pt" style="color:#b91c1c">⚠️ Просрочени плащания</div></div> <table id="overdueTbl"></table> </div> <div id="hubList"></div> </div> <div class="toast" id="toast"></div> <script> 'use strict'; var API=(function(){ var saved=localStorage.getItem('wt_api'); if(saved)return saved; var base=window.location.origin+window.location.pathname.replace(/hubs\.html$/,'').replace(/\/$/,''); return base+'/api.php'; })(); async function af(action,method,body,qs){ method=method||'GET';qs=qs||''; try{ var o={method:method,headers:{'Content-Type':'application/json'}}; if(body)o.body=JSON.stringify(body); var r=await fetch(API+'?action='+action+qs,o); var j=await r.json(); return j.ok?{ok:true,data:j.data}:{ok:false,error:j.error}; }catch(e){return {ok:false,error:e.message};} } function toast(m){var t=document.getElementById('toast');t.textContent=m;t.style.display='block';setTimeout(()=>t.style.display='none',2600);} function money(n,c){return (parseFloat(n||0)).toFixed(2)+' '+(c||'EUR');} function esc(s){return (s==null?'':String(s)).replace(/&/g,'&').replace(/</g,'<').replace(/"/g,'"');} var MODULE_LABELS={projects:'Проекти',clients:'Клиенти',finance:'Финанси',tasks:'Задачи',offers:'Оферти', contracts:'Договори',hosting:'Хостинги',passwords:'Достъпи',presence:'Присъствие',targets:'Таргети', invoicing:'Фактуриране',hr_requests:'Молби',waybills:'Пътни листове'}; var HUBS=[], SUBS=[]; async function loadAll(){ var st=await af('hub_billing_stats'); if(!st.ok){ var msg=String(st.error||''); var hint=''; if(msg.indexOf('Unknown action')!==-1) hint='На ТОЗИ хъб върви стара версия на api.php. Качи новите файлове от пакета (api.php, hubs.html…) върху този сайт и презареди.'; else if(msg.indexOf('no_config')!==-1 || msg.indexOf('Базата данни не е конфигурирана')!==-1) hint='Този хъб няма config.php — стартирай w_hub_install.php тук.'; else if(msg.indexOf('exist')!==-1 || msg.toLowerCase().indexOf('table')!==-1) hint='Липсват новите таблици. Отвори веднъж: <a href="'+API+'?action=install_platform" target="_blank">api.php?action=install_platform</a> и презареди.'; else hint='Грешка: '+esc(msg)+'. Провери дали api.php е новата версия и дали install_platform е стартиран.'; document.getElementById('stats').innerHTML=''; document.getElementById('hubList').innerHTML='<div class="panel" style="border-color:rgba(239,68,68,.35)"><div class="pt" style="color:#b91c1c;margin-bottom:8px">Хъбът не е готов за този модул</div><div class="small" style="font-size:12.5px;line-height:1.7">'+hint+'</div></div>'; return; } renderStats(st.data); var hs=await af('client_hubs'); HUBS=hs.ok?(hs.data||[]):[]; var ss=await af('hub_subscriptions'); SUBS=ss.ok?(ss.data||[]):[]; renderHubs(); } function renderStats(s){ document.getElementById('stats').innerHTML= '<div class="statcard"><div class="lbl">Клиентски хъбове</div><b>'+s.hubs+'</b></div>'+ '<div class="statcard"><div class="lbl">Активни абонаменти</div><b>'+s.active_subscriptions+'</b></div>'+ '<div class="statcard"><div class="lbl">MRR (месечен приход)</div><b style="color:#15803d">'+money(s.mrr)+'</b></div>'+ '<div class="statcard"><div class="lbl">Просрочени</div><b style="color:'+(s.overdue.length?'#b91c1c':'inherit')+'">'+s.overdue.length+'</b></div>'; var op=document.getElementById('overduePanel'); if(s.overdue.length){ op.style.display='block'; var html='<thead><tr><th>Клиент</th><th>Модул</th><th>Сума/мес</th><th>Падеж</th><th></th></tr></thead><tbody>'; s.overdue.forEach(o=>{ html+='<tr><td>'+esc(o.hub_name||'—')+'</td><td>'+(MODULE_LABELS[o.module]||o.module)+'</td><td>'+money(o.price_monthly,o.currency)+'</td>'+ '<td style="color:#b91c1c">'+o.next_billing_date+'</td>'+ '<td style="display:flex;gap:6px"><button class="btn g" onclick="markPaid(\''+o.id+'\')">✓ Платено</button>'+ '<button class="btn d" onclick="stopModuleForSub(\''+o.id+'\',\''+o.hub_id+'\',\''+o.module+'\')">⛔ Спри модула</button></td></tr>'; }); document.getElementById('overdueTbl').innerHTML=html+'</tbody>'; } else op.style.display='none'; } async function addHub(){ var name=document.getElementById('h_name').value.trim(), url=document.getElementById('h_url').value.trim().replace(/\/$/,''), key=document.getElementById('h_key').value.trim(); if(!name||!url||!key){toast('Попълни име, URL и ключ');return;} var r=await af('client_hubs','POST',{name:name,url:url,license_key:key}); if(r.ok){ toast('Хъбът е добавен'); ['h_name','h_url','h_key'].forEach(id=>document.getElementById(id).value=''); loadAll(); } else toast('Грешка: '+r.error); } function renderHubs(){ var box=document.getElementById('hubList'); if(!HUBS.length){ box.innerHTML='<div class="panel small">Няма добавени клиентски хъбове още.</div>'; return; } var html=''; HUBS.forEach(h=>{ var hubSubs=SUBS.filter(s=>s.hub_id===h.id); var mrr=hubSubs.filter(s=>s.active==1).reduce((a,s)=>a+parseFloat(s.price_monthly||0),0); html+='<div class="hubcard">'+ '<div class="ph"><div><span class="pt">🏢 '+esc(h.name)+'</span> <span class="small">'+esc(h.url)+'</span></div>'+ '<div style="display:flex;gap:6px;align-items:center"><span class="small">MRR: <b style="color:#15803d">'+money(mrr)+'</b></span>'+ '<button class="btn d" onclick="removeHub(\''+h.id+'\')">Премахни</button></div></div>'+ '<div id="mods_'+h.id+'" class="small">Свързвам се с хъба…</div>'+ '<div style="margin-top:14px"><div class="small" style="text-transform:uppercase;letter-spacing:.06em;margin-bottom:6px">Абонаменти</div>'+ '<table><thead><tr><th>Модул</th><th>Цена/мес</th><th>Следващо плащане</th><th>Статус</th><th></th></tr></thead><tbody>'; hubSubs.forEach(s=>{ var overdue = s.active==1 && s.next_billing_date && s.next_billing_date < new Date().toISOString().slice(0,10); html+='<tr><td>'+(MODULE_LABELS[s.module]||s.module)+'</td><td>'+money(s.price_monthly,s.currency)+'</td>'+ '<td>'+(s.next_billing_date||'—')+'</td>'+ '<td><span class="badge '+(s.active==0?'stopped':(overdue?'overdue':'ok'))+'">'+(s.active==0?'спрян':(overdue?'просрочен':'активен'))+'</span></td>'+ '<td style="display:flex;gap:5px;flex-wrap:wrap">'+ (s.active==1?'<button class="btn g" onclick="markPaid(\''+s.id+'\')">✓ Платено</button>':'')+ (s.active==1?'<button class="btn a" onclick="toggleSub(\''+s.id+'\',0)">⏸ Спри</button>' :'<button class="btn g" onclick="toggleSub(\''+s.id+'\',1)">▶ Активирай</button>')+ '<span class="iconbtn" onclick="delSub(\''+s.id+'\')">🗑</span></td></tr>'; }); if(!hubSubs.length) html+='<tr><td colspan="5" class="small">Няма абонаменти</td></tr>'; html+='</tbody></table>'+ '<div class="row3" style="margin-top:10px;align-items:end">'+ '<div><label>Модул</label><select id="ns_mod_'+h.id+'">'+Object.keys(MODULE_LABELS).map(k=>'<option value="'+k+'">'+MODULE_LABELS[k]+'</option>').join('')+'</select></div>'+ '<div><label>Цена €/месец</label><input id="ns_price_'+h.id+'" type="number" step="0.01" value="5"></div>'+ '<div><button class="btn p" style="width:100%" onclick="addSub(\''+h.id+'\')">+ Абонамент</button></div>'+ '</div></div></div>'; }); box.innerHTML=html; HUBS.forEach(h=>loadRemoteModules(h)); } async function loadRemoteModules(h){ var box=document.getElementById('mods_'+h.id); if(!box)return; if(window.location.protocol==='https:' && h.url.indexOf('http://')===0){ box.innerHTML='<span class="dot err"></span>Браузърът блокира връзката: този хъб е на https, а клиентският е на http (mixed content). Смени URL-а на клиента на https:// (издай му SSL от cPanel → SSL/TLS Status → Run AutoSSL).'; return; } try{ var resp=await fetch(h.url+'/api.php?action=branding_settings'); var j=await resp.json(); if(!j.ok){ var m=String(j.error||''); if(m==='no_config'||m.indexOf('не е конфигурирана')!==-1) box.innerHTML='<span class="dot err"></span>Хъбът на клиента не е инсталиран (няма config.php) — стартирай w_hub_install.php там.'; else if(m.indexOf('Unknown action')!==-1) box.innerHTML='<span class="dot err"></span>Клиентът е със стара версия на api.php — качи новите файлове при него.'; else box.innerHTML='<span class="dot err"></span>Хъбът отговори с грешка: '+esc(m); return; } var mods=j.data.modules_enabled||{}; var html='<span class="dot ok"></span>Свързан — '+esc(j.data.product_name||'')+'<div style="margin-top:8px">'; Object.keys(MODULE_LABELS).forEach(k=>{ var on=!!mods[k]; html+='<span class="modchip '+(on?'on':'off')+'" onclick="remoteToggle(\''+h.id+'\',\''+esc(h.url)+'\',\''+esc(h.license_key)+'\',\''+k+'\',this)">'+MODULE_LABELS[k]+(on?' ✓':'')+'</span>'; }); box.innerHTML=html+'</div>'; af('client_hub_ping','GET','', '&id='+h.id); }catch(e){ box.innerHTML='<span class="dot err"></span>Няма мрежова връзка с '+esc(h.url)+'/api.php — провери: 1) URL-ът е точен и без път след домейна, 2) хъбът е инсталиран там, 3) отвори '+esc(h.url)+'/api.php?action=test директно в браузъра (трябва да върне JSON).'; } } async function remoteToggle(hubId,url,key,mod,el){ var willEnable=el.classList.contains('off'); try{ var resp=await fetch(url+'/api.php?action=module_toggle',{method:'POST',headers:{'Content-Type':'application/json'}, body:JSON.stringify({admin_secret:key,module:mod,enabled:willEnable?1:0})}); var j=await resp.json(); if(j.ok){ el.classList.toggle('on'); el.classList.toggle('off'); el.innerHTML=MODULE_LABELS[mod]+(willEnable?' ✓':''); toast(MODULE_LABELS[mod]+' → '+(willEnable?'включен':'изключен')+' при клиента'); } else toast('Грешка: '+j.error); }catch(e){ toast('Няма връзка с хъба на клиента'); } } async function removeHub(id){ if(!confirm('Премахване на хъба от регистъра? (Неговата инсталация не се пипа.)'))return; await af('client_hubs','DELETE','', '&id='+id); loadAll(); } async function addSub(hubId){ var mod=document.getElementById('ns_mod_'+hubId).value; var price=parseFloat(document.getElementById('ns_price_'+hubId).value)||0; var r=await af('hub_subscriptions','POST',{hub_id:hubId,module:mod,price_monthly:price}); if(r.ok){ toast('Абонаментът е добавен'); loadAll(); } else toast('Грешка: '+r.error); } async function delSub(id){ if(!confirm('Изтрий абонамента?'))return; await af('hub_subscriptions','DELETE','', '&id='+id); loadAll(); } async function markPaid(id){ var r=await af('hub_subscription_paid','POST',{id:id}); if(r.ok){ toast('Отбелязано като платено — следващ падеж: '+r.data.next_billing_date); loadAll(); } else toast('Грешка: '+r.error); } async function toggleSub(id,active){ var sub=SUBS.find(s=>s.id===id); if(!sub)return; sub.active=active; var r=await af('hub_subscriptions','POST',sub); if(r.ok){ toast(active?'Абонаментът е активиран':'Абонаментът е спрян'); loadAll(); } } async function stopModuleForSub(subId,hubId,mod){ var h=HUBS.find(x=>x.id===hubId); if(h){ try{ var resp=await fetch(h.url+'/api.php?action=module_toggle',{method:'POST',headers:{'Content-Type':'application/json'}, body:JSON.stringify({admin_secret:h.license_key,module:mod,enabled:0})}); var j=await resp.json(); toast(j.ok?'Модулът е изключен при клиента':'Грешка при изключване: '+(j.error||'')); }catch(e){ toast('Няма връзка с хъба на клиента'); } } var sub=SUBS.find(s=>s.id===subId); if(sub){ sub.active=0; await af('hub_subscriptions','POST',sub); } loadAll(); } async function moduleGuard(key,label){ try{ var r=await af('branding_settings'); if(r.ok && r.data && (r.data.license_state==='locked'||r.data.license_state==='invalid')){ document.body.innerHTML='<div style="max-width:480px;margin:80px auto;text-align:center;font-family:sans-serif;color:#4a4a6a">'+ '<div style="font-size:40px;margin-bottom:12px">\ud83d\udd12</div><h2>Хъбът е заключен</h2>'+ '<p style="margin-top:8px;font-size:13px">'+esc(r.data.license_message||'Лицензът не е активен.')+'</p></div>'; return false; } if(r.ok && r.data && r.data.modules_enabled && r.data.modules_enabled.hasOwnProperty(key) && !r.data.modules_enabled[key]){ document.body.innerHTML='<div style="max-width:480px;margin:80px auto;text-align:center;font-family:sans-serif;color:#4a4a6a">'+ '<div style="font-size:40px;margin-bottom:12px">\ud83d\udd12</div><h2>'+label+' е изключен</h2>'+ '<p style="margin-top:8px;font-size:13px">Този модул е достъпен само на master хъба. Включи го от w_admin.php ако това е твоят master.</p>'+ '<a href="index.html" style="display:inline-block;margin-top:16px;color:#6c63ff">\u2190 Обратно към Hub</a></div>'; return false; } }catch(e){} return true; } moduleGuard('hubs_management','Управлението на хъбове').then(function(ok){ if(ok) loadAll(); }); </script> </body> </html>
| ver. 1.4 |
Github
|
.
| PHP 8.2.31 | Generation time: 0 |
proxy
|
phpinfo
|
Settings