File manager - Edit - /home/webtrixia/.trash/invoices.html.5
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);--brd2:rgba(0,0,0,.14); --tx:#1a1a2e;--tx2:#4a4a6a;--tx3:#8888aa;--acc:#6c63ff;--acc2:#a78bfa;--grn:#22c55e;--gdim:rgba(34,197,94,.12); --amb:#f59e0b;--adim:rgba(245,158,11,.12);--red:#ef4444;--rdim:rgba(239,68,68,.12); --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:1280px;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);cursor:pointer;font-size:12px;text-decoration:none} .tabs{display:flex;gap:4px;margin-bottom:18px;background:var(--sur);border:1px solid var(--brd);padding:5px;border-radius:12px;flex-wrap:wrap} .tab{padding:9px 14px;border-radius:8px;font-size:12.5px;font-weight:500;cursor:pointer;color:var(--tx2);white-space:nowrap} .tab.active{background:var(--acc);color:#fff} .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:14px;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:hover{opacity:.88} table{width:100%;border-collapse:collapse;font-size:12.5px} th{text-align:left;padding:9px 10px;color:var(--tx3);font-weight:500;border-bottom:1px solid var(--brd);white-space:nowrap} td{padding:10px;border-bottom:1px solid var(--brd);vertical-align:middle} tr:hover td{background:var(--sur2)} .badge{padding:3px 10px;border-radius:20px;font-size:11px;font-weight:600;white-space:nowrap} .badge.draft{background:var(--sur3);color:var(--tx2)} .badge.sent{background:rgba(56,189,248,.15);color:#0284c7} .badge.paid{background:var(--gdim);color:#15803d} .badge.overdue{background:var(--rdim);color:#b91c1c} .badge.cancelled{background:var(--sur3);color:var(--tx3);text-decoration:line-through} .actions{display:flex;gap:6px} .iconbtn{background:var(--sur2);border:1px solid var(--brd);border-radius:7px;padding:5px 9px;cursor:pointer;font-size:12px} label{display:block;font-size:11px;color:var(--tx3);text-transform:uppercase;letter-spacing:.06em;margin:10px 0 5px} input,select,textarea{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,textarea:focus{border-color:var(--acc)} .row2{display:grid;grid-template-columns:1fr 1fr;gap:10px} .row3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px} .grid2{display:grid;grid-template-columns:1fr 1fr;gap:16px} @media(max-width:860px){.grid2{grid-template-columns:1fr}} /* modal */ .mo{position:fixed;inset:0;background:rgba(20,20,35,.5);display:none;align-items:flex-start;justify-content:center;z-index:999;padding:30px 14px;overflow:auto} .mo.show{display:flex} .mb{background:var(--sur);border-radius:16px;padding:22px;width:100%;max-width:720px} .mb h3{font-size:15px;margin-bottom:14px} .items{border:1px solid var(--brd);border-radius:10px;overflow:hidden;margin-top:6px} .itemrow{display:grid;grid-template-columns:1fr 70px 70px 90px 90px 30px;gap:6px;padding:8px;border-bottom:1px solid var(--brd);align-items:center} .itemrow input{padding:7px 8px;font-size:12px} .itemhead{font-size:10px;color:var(--tx3);text-transform:uppercase} .totalsbox{margin-top:10px;text-align:right;font-size:13px;line-height:1.9} .totalsbox b{font-size:16px;color:var(--acc)} .eiklook{display:flex;gap:6px} .eiklook input{flex:1} .close{position:absolute;top:16px;right:20px;cursor:pointer;color:var(--tx3);font-size:20px} .mbrel{position:relative} .small{font-size:11px;color:var(--tx3)} .stat{display:flex;flex-direction:column;gap:2px} .stat b{font-size:20px} .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} .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} .scanbox{display:grid;grid-template-columns:1fr 340px;gap:16px} .scanresult{background:var(--sur2);border:1px solid var(--brd);border-radius:12px;padding:14px} .scanline{display:flex;justify-content:space-between;gap:12px;border-bottom:1px solid var(--brd);padding:8px 0;font-size:12.5px} .scanline:last-child{border-bottom:0}.scanline span:first-child{color:var(--tx3)}.scanline b{text-align:right} @media(max-width:980px){.scanbox{grid-template-columns:1fr}} </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="tabs" id="tabs"></div> <div id="content"></div> </div> <div class="mo" id="docModal"><div class="mb mbrel"> <span class="close" onclick="closeModal('docModal')">✕</span> <h3 id="docModalTitle">Документ</h3> <div class="row2"> <div> <label>Клиент</label> <select id="d_client" onchange="fillClientSnapshot()"></select> </div> <div> <label>ЕИК за търсене (VIES)</label> <div class="eiklook"> <input id="d_eiklookup" placeholder="напр. 123456789"> <button class="btn s" onclick="lookupEik()">🔎 Търси</button> </div> </div> </div> <div class="row3"> <div><label>Дата на издаване</label><input id="d_issue" type="date"></div> <div><label>Данъчна дата</label><input id="d_tax" type="date"></div> <div><label>Падеж (по избор)</label><input id="d_due" type="date"></div> </div> <label>Артикули</label> <div class="items" id="itemsBox"></div> <button class="btn s" style="margin-top:8px" onclick="addItemRow()">+ Артикул</button> <div class="row3" style="margin-top:10px"> <div><label>ДДС %</label><input id="d_vat" type="number" value="20" oninput="recalcTotals()"></div> <div><label>Валута</label><select id="d_currency"><option>EUR</option><option>BGN</option></select></div> <div><label>Статус</label><select id="d_status"><option value="draft">Чернова</option><option value="sent">Изпратена</option><option value="paid">Платена</option><option value="overdue">Просрочена</option><option value="cancelled">Анулирана</option></select></div> </div> <label>Бележки</label> <textarea id="d_notes" rows="2"></textarea> <div class="totalsbox" id="totalsBox"></div> <div style="display:flex;gap:8px;margin-top:14px"> <button class="btn p" onclick="saveDocument()">💾 Запази</button> <button class="btn s" onclick="closeModal('docModal')">Отказ</button> </div> </div></div> <div class="mo" id="clientModal"><div class="mb mbrel"> <span class="close" onclick="closeModal('clientModal')">✕</span> <h3>Клиент</h3> <div class="row2"> <div><label>Име</label><input id="c_name"></div> <div><label>Фирма</label><input id="c_company"></div> </div> <div class="row2"> <div><label>ЕИК</label><input id="c_eik"></div> <div><label>ДДС номер</label><input id="c_vat"></div> </div> <div class="row2"> <div><label>Имейл</label><input id="c_email"></div> <div><label>Телефон</label><input id="c_phone"></div> </div> <label>Адрес</label><input id="c_address"> <div class="row2"> <div><label>Град</label><input id="c_city"></div> <div><label>МОЛ</label><input id="c_mol"></div> </div> <button class="btn p" style="margin-top:14px" onclick="saveClient()">💾 Запази клиент</button> </div></div> <div class="toast" id="toast"></div> <script> 'use strict'; var API=(function(){ var base=window.location.origin+window.location.pathname.replace(/invoices\.html$/,'').replace(/\/$/,''); var own=base+'/api.php'; var saved=localStorage.getItem('wt_api'); if(saved){ try{ var u=new URL(saved,window.location.href); if(u.origin===window.location.origin)return saved; localStorage.removeItem('wt_api'); }catch(e){localStorage.removeItem('wt_api');} } return own; })(); 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?j.data:null; }catch(e){console.error(action,e);return null;} } function toast(msg){var t=document.getElementById('toast');t.textContent=msg;t.style.display='block';setTimeout(()=>t.style.display='none',2600);} function openModal(id){document.getElementById(id).classList.add('show');} function closeModal(id){document.getElementById(id).classList.remove('show');} function money(n,cur){cur=cur||'EUR';return (parseFloat(n||0)).toFixed(2)+' '+cur;} var TABS=[ {id:'dashboard',label:'📊 Табло'}, {id:'ai_scan',label:'AI сканиране'}, {id:'proforma',label:'Проформи'}, {id:'invoice',label:'Фактури'}, {id:'credit_note',label:'Кредитни изв.'}, {id:'debit_note',label:'Дебитни изв.'}, {id:'offer',label:'Оферти'}, {id:'clients',label:'👥 Клиенти'}, {id:'recurring',label:'🔁 Периодични'}, {id:'reports',label:'📈 Справки'}, {id:'settings',label:'⚙️ Настройки'} ]; var CUR_TAB='dashboard'; var CLIENTS=[]; var AI_SCAN=null; function renderTabs(){ document.getElementById('tabs').innerHTML=TABS.map(t=> '<div class="tab'+(t.id===CUR_TAB?' active':'')+'" onclick="goTab(\''+t.id+'\')">'+t.label+'</div>').join(''); } function goTab(id){CUR_TAB=id;renderTabs();renderContent();} var TYPE_LABEL={proforma:'Проформа фактура',invoice:'Фактура',credit_note:'Кредитно известие',debit_note:'Дебитно известие',offer:'Оферта'}; async function renderContent(){ var c=document.getElementById('content'); if(CUR_TAB==='dashboard'){ await renderDashboard(c); return; } if(CUR_TAB==='ai_scan'){ await renderAiScan(c); return; } if(CUR_TAB==='clients'){ await renderClients(c); return; } if(CUR_TAB==='recurring'){ await renderRecurring(c); return; } if(CUR_TAB==='reports'){ await renderReports(c); return; } if(CUR_TAB==='settings'){ await renderSettings(c); return; } await renderDocList(c,CUR_TAB); } // ── DASHBOARD ── async function renderDashboard(c){ c.innerHTML='<div class="statgrid" id="statgrid"><div class="statcard">Зарежда…</div></div>'; var stats=await af('invoice_stats'); if(!stats){c.innerHTML='<div class="panel">Няма връзка с API. Стартирай веднъж <code>api.php?action=install_invoicing</code>.</div>';return;} var byType={}; (stats.by_type||[]).forEach(t=>byType[t.type]=t); var unpaid=stats.unpaid||{cnt:0,total:0}; var html='<div class="statgrid">'; html+='<div class="statcard"><div class="stat"><span class="small">Фактури</span><b>'+((byType.invoice&&byType.invoice.cnt)||0)+'</b><span class="small">'+money((byType.invoice&&byType.invoice.total)||0)+'</span></div></div>'; html+='<div class="statcard"><div class="stat"><span class="small">Проформи</span><b>'+((byType.proforma&&byType.proforma.cnt)||0)+'</b><span class="small">'+money((byType.proforma&&byType.proforma.total)||0)+'</span></div></div>'; html+='<div class="statcard"><div class="stat"><span class="small">Оферти</span><b>'+((byType.offer&&byType.offer.cnt)||0)+'</b></div></div>'; html+='<div class="statcard"><div class="stat"><span class="small">Неплатени фактури</span><b style="color:var(--red)">'+unpaid.cnt+'</b><span class="small">'+money(unpaid.total)+'</span></div></div>'; html+='</div>'; html+='<div class="panel"><div class="ph"><div class="pt">Приходи по месеци (фактури)</div></div><table><thead><tr><th>Месец</th><th>Сума</th></tr></thead><tbody>'; (stats.by_month||[]).forEach(m=>{html+='<tr><td>'+m.ym+'</td><td>'+money(m.total)+'</td></tr>';}); html+='</tbody></table></div>'; c.innerHTML=html; } // ── DOCUMENT LIST (proforma/invoice/credit_note/debit_note/offer) ── async function renderDocList(c,type){ c.innerHTML='<div class="panel">Зарежда…</div>'; var rows=await af('documents','GET','', '&type='+type); var label=TYPE_LABEL[type]||type; var html='<div class="panel"><div class="ph"><div class="pt">'+label+'</div><div class="actions">'+ '<button class="btn s" onclick="exportCsv(\''+type+'\')">⬇ Експорт CSV</button>'+ '<button class="btn p" onclick="newDocument(\''+type+'\')">+ Нов</button></div></div>'; html+='<table><thead><tr><th>№</th><th>Дата</th><th>Клиент</th><th>Сума</th><th>Статус</th><th></th></tr></thead><tbody>'; (rows||[]).forEach(r=>{ var snap=r.client_snapshot||{}; html+='<tr><td>'+(r.full_number||r.number)+'</td><td>'+(r.issue_date||'')+'</td><td>'+(snap.name||'—')+'</td>'+ '<td>'+money(r.total,r.currency)+'</td><td><span class="badge '+r.status+'">'+r.status+'</span></td>'+ '<td class="actions">'+ '<span class="iconbtn" title="Преглед / Печат" onclick="window.open(\'invoice-doc.html?id='+r.id+'\',\'_blank\')">👁</span>'+ '<span class="iconbtn" title="Редакция" onclick="editDocument(\''+r.id+'\',\''+type+'\')">✎</span>'+ '<span class="iconbtn" title="Имейл" onclick="emailDocument(\''+r.id+'\')">✉</span>'+ '<span class="iconbtn" title="Изтрий" onclick="deleteDocument(\''+r.id+'\',\''+type+'\')">🗑</span>'+ '</td></tr>'; }); if(!rows||!rows.length) html+='<tr><td colspan="6" class="small">Няма документи</td></tr>'; html+='</tbody></table></div>'; c.innerHTML=html; } function exportCsv(type){ window.open(API+'?action=documents_export&type='+type,'_blank'); } // ── AI SCAN ── async function renderAiScan(c){ c.innerHTML='<div class="panel"><div class="ph"><div><div class="pt">AI сканиране на фактури</div><div class="small">Постави OCR текст от PDF/снимка или качи текстов файл. Системата ще предложи полета за чернова.</div></div><button class="btn p" onclick="runAiScan()">Сканирай</button></div>'+ '<div class="scanbox"><div><label>Файл (TXT/CSV/JSON или текст от OCR)</label><input type="file" id="scan_file" onchange="readScanFile(this)"><label>Текст от фактура</label><textarea id="scan_text" rows="18" placeholder="Постави тук текста от фактурата, проформата или касовия документ..."></textarea></div>'+ '<div class="scanresult" id="scan_result"><div class="small">Резултатът ще се появи тук.</div></div></div></div>'; } function readScanFile(inp){ var f=inp.files&&inp.files[0]; if(!f)return; var r=new FileReader(); r.onload=function(){ document.getElementById('scan_text').value=String(r.result||''); runAiScan(); }; r.readAsText(f); } function normalizeAmount(v){ v=String(v||'').replace(/\s/g,'').replace(/[^\d,.-]/g,''); if(v.indexOf(',')>-1 && v.indexOf('.')>-1) v=v.replace(/\./g,'').replace(',','.'); else v=v.replace(',','.'); return parseFloat(v)||0; } function firstMatch(text,patterns){ for(var i=0;i<patterns.length;i++){var m=text.match(patterns[i]); if(m)return (m[1]||m[0]||'').trim();} return ''; } function parseAiInvoice(text){ var raw=String(text||''); var lines=raw.split(/\r?\n/).map(x=>x.trim()).filter(Boolean); var joined=lines.join('\n'); var eik=firstMatch(joined,[/(?:ЕИК|EIK|Булстат|BULSTAT)[:\s№-]*([0-9]{9,13})/i,/\b([0-9]{9})\b/]); var vatNo=firstMatch(joined,[/(BG[0-9]{9,12})/i,/(?:ДДС|VAT)[:\s№-]*(BG?[0-9]{9,12})/i]).toUpperCase(); var number=firstMatch(joined,[/(?:Фактура|Invoice|Документ|№|No\.?)[:\s№-]*([A-ZА-Я0-9\/.-]{3,})/i]); var date=firstMatch(joined,[/(?:Дата|Date)[:\s-]*(\d{1,2}[.\/-]\d{1,2}[.\/-]\d{2,4})/i,/(\d{1,2}[.\/-]\d{1,2}[.\/-]\d{4})/]); var due=firstMatch(joined,[/(?:Падеж|Due)[:\s-]*(\d{1,2}[.\/-]\d{1,2}[.\/-]\d{2,4})/i]); var total=0, vat=0, currency=firstMatch(joined,[/\b(EUR|BGN|USD)\b/i,/\b(лв\.?|лева)\b/i]).toUpperCase(); if(currency==='ЛВ.'||currency==='ЛВ'||currency==='ЛЕВА')currency='BGN'; lines.forEach(function(l){ if(/(общо|total|сума за плащане|amount due)/i.test(l)){ var nums=l.match(/[0-9][0-9\s.,]*[0-9]/g); if(nums&&nums.length) total=Math.max(total,normalizeAmount(nums[nums.length-1])); } if(/(ддс|vat)/i.test(l)){ var vn=l.match(/[0-9][0-9\s.,]*[0-9]/g); if(vn&&vn.length) vat=Math.max(vat,normalizeAmount(vn[vn.length-1])); } }); if(!total){ var all=joined.match(/[0-9][0-9\s.,]*[0-9]/g)||[]; all.forEach(n=>{var x=normalizeAmount(n); if(x>total)total=x;}); } var company=''; for(var i=0;i<lines.length;i++){ if(/(доставчик|supplier|издател|продавач|получател|client)/i.test(lines[i]) && lines[i+1]){company=lines[i+1];break;} if(eik && lines[i].indexOf(eik)>-1 && i>0){company=lines[i-1];break;} } var vatPercent=vat&&total?Math.round((vat/(total-vat))*100):20; if(!isFinite(vatPercent)||vatPercent<0||vatPercent>30)vatPercent=20; var subtotal=total&&vatPercent>=0?total/(1+vatPercent/100):0; return {number:number,date:dateToIso(date),due:dateToIso(due),company:company,eik:eik,vat_no:vatNo,total:total,subtotal:subtotal,vat_percent:vatPercent,currency:currency||'EUR',raw:raw}; } function dateToIso(s){ if(!s)return ''; var m=String(s).match(/(\d{1,2})[.\/-](\d{1,2})[.\/-](\d{2,4})/); if(!m)return ''; var y=m[3].length===2?'20'+m[3]:m[3]; return y+'-'+String(m[2]).padStart(2,'0')+'-'+String(m[1]).padStart(2,'0'); } function runAiScan(){ var txt=document.getElementById('scan_text').value; if(!txt.trim()){toast('Постави текст за сканиране');return;} AI_SCAN=parseAiInvoice(txt); var r=AI_SCAN; document.getElementById('scan_result').innerHTML= scanLine('Номер',r.number||'—')+scanLine('Дата',r.date||'—')+scanLine('Падеж',r.due||'—')+ scanLine('Фирма',r.company||'—')+scanLine('ЕИК',r.eik||'—')+scanLine('ДДС №',r.vat_no||'—')+ scanLine('Общо',money(r.total,r.currency))+scanLine('ДДС %',r.vat_percent+'%')+ '<button class="btn p" style="width:100%;margin-top:12px" onclick="applyAiScan()">Създай фактура чернова</button>'+ '<button class="btn s" style="width:100%;margin-top:8px" onclick="copyScanJson()">Копирай JSON</button>'; } function scanLine(k,v){return '<div class="scanline"><span>'+k+'</span><b>'+String(v||'').replace(/</g,'<')+'</b></div>';} function copyScanJson(){ if(AI_SCAN&&navigator.clipboard)navigator.clipboard.writeText(JSON.stringify(AI_SCAN,null,2)); toast('Копирано'); } async function applyAiScan(){ if(!AI_SCAN){toast('Първо сканирай документ');return;} await ensureClients(); var found=CLIENTS.find(c=>(AI_SCAN.eik&&c.eik===AI_SCAN.eik)||(AI_SCAN.company&&((c.company||c.name||'').toLowerCase().indexOf(AI_SCAN.company.toLowerCase())>-1))); await newDocument('invoice'); if(found) fillClientSelect(found.id); if(AI_SCAN.date){document.getElementById('d_issue').value=AI_SCAN.date;document.getElementById('d_tax').value=AI_SCAN.date;} if(AI_SCAN.due)document.getElementById('d_due').value=AI_SCAN.due; document.getElementById('d_vat').value=AI_SCAN.vat_percent||20; document.getElementById('d_currency').value=AI_SCAN.currency||'EUR'; document.getElementById('d_notes').value='AI сканиране: '+(AI_SCAN.number||'')+(AI_SCAN.company?' · '+AI_SCAN.company:'')+(AI_SCAN.eik?' · ЕИК '+AI_SCAN.eik:''); document.getElementById('itemsBox').innerHTML=''; addItemRow({name:'Сканиран документ '+(AI_SCAN.number||''),qty:1,unit:'бр.',price:(AI_SCAN.subtotal||AI_SCAN.total||0).toFixed(2)}); recalcTotals(); } async function deleteDocument(id,type){ if(!confirm('Изтриване на документа?'))return; await af('documents','DELETE','', '&id='+id); renderDocList(document.getElementById('content'),type); } // ── DOCUMENT EDITOR MODAL ── var EDIT_ID=null, EDIT_TYPE=null; async function ensureClients(){ if(!CLIENTS.length) CLIENTS=await af('clients')||[]; } async function newDocument(type){ await ensureClients(); EDIT_ID=null; EDIT_TYPE=type; document.getElementById('docModalTitle').textContent='Нов документ — '+(TYPE_LABEL[type]||type); fillClientSelect(); document.getElementById('d_issue').value=new Date().toISOString().slice(0,10); document.getElementById('d_tax').value=new Date().toISOString().slice(0,10); document.getElementById('d_due').value=''; document.getElementById('d_vat').value=20; document.getElementById('d_currency').value='EUR'; document.getElementById('d_status').value='draft'; document.getElementById('d_notes').value=''; document.getElementById('itemsBox').innerHTML=''; addItemRow(); recalcTotals(); openModal('docModal'); } async function editDocument(id,type){ await ensureClients(); var full=await af('document_full','GET','', '&id='+id); if(!full){toast('Грешка при зареждане');return;} var d=full.document; EDIT_ID=id; EDIT_TYPE=type; document.getElementById('docModalTitle').textContent=(TYPE_LABEL[type]||type)+' № '+d.full_number; fillClientSelect(d.client_id); document.getElementById('d_issue').value=d.issue_date||''; document.getElementById('d_tax').value=d.tax_date||''; document.getElementById('d_due').value=d.due_date||''; document.getElementById('d_vat').value=d.vat_percent||20; document.getElementById('d_currency').value=d.currency||'EUR'; document.getElementById('d_status').value=d.status||'draft'; document.getElementById('d_notes').value=d.notes||''; document.getElementById('itemsBox').innerHTML=''; (d.items||[]).forEach(it=>addItemRow(it)); if(!d.items||!d.items.length) addItemRow(); recalcTotals(); openModal('docModal'); } function fillClientSelect(selectedId){ var sel=document.getElementById('d_client'); sel.innerHTML='<option value="">— без клиент —</option>'+CLIENTS.map(c=> '<option value="'+c.id+'"'+(c.id===selectedId?' selected':'')+'>'+(c.name||c.company)+'</option>').join(''); } function fillClientSnapshot(){ /* client chosen; snapshot handled server-side on save */ } function addItemRow(it){ it=it||{name:'',qty:1,unit:'бр.',price:0}; var box=document.getElementById('itemsBox'); if(!box.querySelector('.itemhead-row')){ var head=document.createElement('div'); head.className='itemrow itemhead-row'; head.innerHTML='<div class="itemhead">Артикул</div><div class="itemhead">Кол.</div><div class="itemhead">Мярка</div><div class="itemhead">Цена</div><div class="itemhead">Общо</div><div></div>'; box.appendChild(head); } var row=document.createElement('div'); row.className='itemrow'; row.innerHTML='<input class="it_name" value="'+(it.name||'').replace(/"/g,'"')+'" placeholder="Услуга/продукт">'+ '<input class="it_qty" type="number" step="0.01" value="'+(it.qty||1)+'">'+ '<input class="it_unit" value="'+(it.unit||'бр.')+'">'+ '<input class="it_price" type="number" step="0.01" value="'+(it.price||0)+'">'+ '<input class="it_total" disabled value="0.00">'+ '<span class="iconbtn" onclick="this.parentElement.remove();recalcTotals()">✕</span>'; box.appendChild(row); row.querySelectorAll('input').forEach(inp=>inp.addEventListener('input',recalcTotals)); recalcTotals(); } function recalcTotals(){ var rows=document.querySelectorAll('#itemsBox .itemrow:not(.itemhead-row)'); var subtotal=0; rows.forEach(r=>{ var q=parseFloat(r.querySelector('.it_qty').value)||0; var p=parseFloat(r.querySelector('.it_price').value)||0; var t=q*p; r.querySelector('.it_total').value=t.toFixed(2); subtotal+=t; }); var vatP=parseFloat(document.getElementById('d_vat').value)||0; var vat=subtotal*vatP/100; var total=subtotal+vat; var cur=document.getElementById('d_currency').value; document.getElementById('totalsBox').innerHTML= 'Данъчна основа: '+money(subtotal,cur)+'<br>ДДС ('+vatP+'%): '+money(vat,cur)+'<br><b>Общо: '+money(total,cur)+'</b>'; } async function saveDocument(){ var items=[]; document.querySelectorAll('#itemsBox .itemrow:not(.itemhead-row)').forEach(r=>{ items.push({ name:r.querySelector('.it_name').value, qty:parseFloat(r.querySelector('.it_qty').value)||0, unit:r.querySelector('.it_unit').value, price:parseFloat(r.querySelector('.it_price').value)||0 }); }); var payload={ id:EDIT_ID||undefined, type:EDIT_TYPE, client_id:document.getElementById('d_client').value||null, issue_date:document.getElementById('d_issue').value, tax_date:document.getElementById('d_tax').value, due_date:document.getElementById('d_due').value||null, items:items, vat_percent:parseFloat(document.getElementById('d_vat').value)||0, currency:document.getElementById('d_currency').value, status:document.getElementById('d_status').value, notes:document.getElementById('d_notes').value }; var r=await af('documents','POST',payload); if(r){ toast('Записано: № '+r.full_number); closeModal('docModal'); renderDocList(document.getElementById('content'),EDIT_TYPE); } else toast('Грешка при запис'); } async function lookupEik(){ var eik=document.getElementById('d_eiklookup').value.trim(); if(!eik){toast('Въведи ЕИК');return;} var r=await af('eik_lookup','GET','', '&eik='+encodeURIComponent(eik)); if(r&&r.found){ toast('Намерено: '+r.name); // offer to create/select client quickly if(confirm('Намерена фирма: '+r.name+'\n'+r.address+'\n\nДобави като нов клиент?')){ var nc=await af('clients','POST',{name:r.name,company:r.name,eik:r.eik,vat_number:r.vat_number,address:r.address}); if(nc){ CLIENTS=await af('clients')||[]; fillClientSelect(nc.id); } } } else { toast((r&&r.message)||'Не е намерено — въведи данните ръчно в раздел Клиенти'); } } async function emailDocument(id){ var to=prompt('Имейл на получателя:'); if(!to)return; var link=window.location.origin+window.location.pathname.replace(/invoices\.html$/,'')+'invoice-doc.html?id='+id; var r=await af('send_document_email','POST',{document_id:id,to:to,link:link}); if(r) toast('Изпратено на '+to); else toast('Грешка — провери SMTP настройките'); } // ── CLIENTS ── async function renderClients(c){ CLIENTS=await af('clients')||[]; var html='<div class="panel"><div class="ph"><div class="pt">Клиенти ('+CLIENTS.length+')</div>'+ '<div class="actions"><button class="btn s" onclick="showImport()">⬆ Импорт CSV</button><button class="btn p" onclick="newClient()">+ Нов клиент</button></div></div>'; html+='<div id="importBox" style="display:none;margin-bottom:12px">'+ '<label>CSV (колони: name,company,email,phone,eik,vat_number,address,city,mol)</label>'+ '<textarea id="csvText" rows="5" placeholder="name,company,email,phone,eik\nИван Иванов,Тест ЕООД,ivan@test.bg,088..,123456789"></textarea>'+ '<button class="btn p" style="margin-top:8px" onclick="importCsv()">Импортирай</button></div>'; html+='<table><thead><tr><th>Име/Фирма</th><th>ЕИК</th><th>Имейл</th><th>Град</th><th></th></tr></thead><tbody>'; CLIENTS.forEach(cl=>{ html+='<tr><td>'+(cl.company||cl.name)+'</td><td>'+(cl.eik||'—')+'</td><td>'+(cl.email||'—')+'</td><td>'+(cl.city||'—')+'</td>'+ '<td class="actions"><span class="iconbtn" onclick="editClient(\''+cl.id+'\')">✎</span>'+ '<span class="iconbtn" onclick="deleteClient(\''+cl.id+'\')">🗑</span></td></tr>'; }); if(!CLIENTS.length) html+='<tr><td colspan="5" class="small">Няма клиенти</td></tr>'; html+='</tbody></table></div>'; c.innerHTML=html; } function showImport(){ document.getElementById('importBox').style.display='block'; } async function importCsv(){ var csv=document.getElementById('csvText').value.trim(); if(!csv){toast('Постави CSV');return;} var r=await af('clients_import','POST',{csv:csv}); if(r){ toast('Импортирани: '+r.imported); renderClients(document.getElementById('content')); } } var EDIT_CLIENT_ID=null; function newClient(){ EDIT_CLIENT_ID=null; ['c_name','c_company','c_eik','c_vat','c_email','c_phone','c_address','c_city','c_mol'].forEach(id=>document.getElementById(id).value=''); openModal('clientModal'); } async function editClient(id){ var cl=CLIENTS.find(x=>x.id===id); if(!cl)return; EDIT_CLIENT_ID=id; document.getElementById('c_name').value=cl.name||''; document.getElementById('c_company').value=cl.company||''; document.getElementById('c_eik').value=cl.eik||''; document.getElementById('c_vat').value=cl.vat_number||''; document.getElementById('c_email').value=cl.email||''; document.getElementById('c_phone').value=cl.phone||''; document.getElementById('c_address').value=cl.address||''; document.getElementById('c_city').value=cl.city||''; document.getElementById('c_mol').value=cl.mol||''; openModal('clientModal'); } async function saveClient(){ var payload={ id:EDIT_CLIENT_ID||undefined, name:document.getElementById('c_name').value, company:document.getElementById('c_company').value, eik:document.getElementById('c_eik').value, vat_number:document.getElementById('c_vat').value, email:document.getElementById('c_email').value, phone:document.getElementById('c_phone').value, address:document.getElementById('c_address').value, city:document.getElementById('c_city').value, mol:document.getElementById('c_mol').value }; var r=await af('clients','POST',payload); if(r){ toast('Клиентът е запазен'); closeModal('clientModal'); renderClients(document.getElementById('content')); } } async function deleteClient(id){ if(!confirm('Изтриване на клиента?'))return; await af('clients','DELETE','', '&id='+id); renderClients(document.getElementById('content')); } // ── RECURRING ── async function renderRecurring(c){ var rows=await af('recurring')||[]; await ensureClients(); var cronUrl=API+'?action=run_recurring&key=webtrixia_cron_2026'; var html='<div class="panel"><div class="ph"><div class="pt">Периодични фактури</div><button class="btn p" onclick="newRecurring()">+ Ново правило</button></div>'; html+='<p class="small">За автоматично генериране добави cPanel Cron Job (напр. на 1-во число всеки месец), който отваря: <code>'+cronUrl+'</code></p>'; html+='<table><thead><tr><th>Клиент</th><th>Тип</th><th>Интервал</th><th>Следващо генериране</th><th>Активно</th><th></th></tr></thead><tbody>'; rows.forEach(r=>{ var cl=CLIENTS.find(x=>x.id===r.client_id); html+='<tr><td>'+(cl?(cl.company||cl.name):'—')+'</td><td>'+(TYPE_LABEL[r.doc_type]||r.doc_type)+'</td><td>'+r.interval_type+'</td><td>'+r.next_run_date+'</td><td>'+(r.active==1?'Да':'Не')+'</td>'+ '<td class="actions"><span class="iconbtn" onclick="deleteRecurring(\''+r.id+'\')">🗑</span></td></tr>'; }); if(!rows.length) html+='<tr><td colspan="6" class="small">Няма правила</td></tr>'; html+='</tbody></table></div>'; c.innerHTML=html; } async function deleteRecurring(id){ if(!confirm('Изтрий правилото?'))return; await af('recurring','DELETE','', '&id='+id); renderRecurring(document.getElementById('content')); } async function newRecurring(){ var cl=prompt('ID на клиент (виж в раздел Клиенти) — или остави празно:'); var interval=prompt('Интервал: monthly / quarterly / yearly','monthly'); var next=prompt('Дата на следващо генериране (YYYY-MM-DD):', new Date().toISOString().slice(0,10)); var itemName=prompt('Наименование на услугата:','Абонаментна поддръжка'); var price=prompt('Цена (без ДДС):','100'); var r=await af('recurring','POST',{client_id:cl,doc_type:'invoice',interval_type:interval,next_run_date:next, items:[{name:itemName,qty:1,unit:'бр.',price:parseFloat(price)||0}],vat_percent:20,active:1}); if(r){ toast('Правилото е създадено'); renderRecurring(document.getElementById('content')); } } // ── REPORTS ── async function renderReports(c){ var stats=await af('invoice_stats'); if(!stats){c.innerHTML='<div class="panel">Няма данни</div>';return;} var html='<div class="grid2">'; html+='<div class="panel"><div class="ph"><div class="pt">По тип документ</div></div><table><thead><tr><th>Тип</th><th>Брой</th><th>Сума</th></tr></thead><tbody>'; (stats.by_type||[]).forEach(t=>html+='<tr><td>'+(TYPE_LABEL[t.type]||t.type)+'</td><td>'+t.cnt+'</td><td>'+money(t.total)+'</td></tr>'); html+='</tbody></table></div>'; html+='<div class="panel"><div class="ph"><div class="pt">Топ клиенти</div></div><table><thead><tr><th>Клиент</th><th>Фактури</th><th>Сума</th></tr></thead><tbody>'; (stats.by_client||[]).forEach(cc=>html+='<tr><td>'+cc.name+'</td><td>'+cc.cnt+'</td><td>'+money(cc.total)+'</td></tr>'); html+='</tbody></table></div>'; html+='</div>'; c.innerHTML=html; } // ── SETTINGS ── async function renderSettings(c){ var s=await af('company_settings')||{}; var html='<div class="grid2">'; html+='<div class="panel"><div class="ph"><div class="pt">Профил на фирмата</div></div>'+ fld('s_name','Име на фирмата',s.name)+ row2(fld('s_eik','ЕИК/Булстат',s.eik),fld('s_vat','ДДС №',s.vat_number))+ fld('s_address','Адрес',s.address)+ row2(fld('s_city','Град',s.city),fld('s_mol','МОЛ',s.mol))+ row2(fld('s_iban','IBAN',s.iban),fld('s_bic','BIC/SWIFT',s.bic))+ fld('s_bank','Банка',s.bank_name)+ row2(fld('s_defvat','ДДС % по подразбиране',s.default_vat_percent||20,'number'),selCur('s_currency',s.currency))+ '</div>'; html+='<div class="panel"><div class="ph"><div class="pt">Номерация (представка към № за всеки тип)</div></div>'+ row2(fld('s_pref_proforma','Проформа',s.prefix_proforma),fld('s_pref_invoice','Фактура',s.prefix_invoice))+ row2(fld('s_pref_credit','Кредитно изв.',s.prefix_credit_note),fld('s_pref_debit','Дебитно изв.',s.prefix_debit_note))+ fld('s_pref_offer','Оферта',s.prefix_offer)+ '<p class="small" style="margin-top:8px">Номерът е 10-цифрен, автоматично нарастващ поотделно за всеки тип и година (напр. 0000000114), както в примерната ти проформа.</p>'+ '</div>'; html+='<div class="panel"><div class="ph"><div class="pt">Имейл сървър (SMTP — за изпращане на документи)</div></div>'+ row2(fld('s_smtp_host','SMTP хост',s.smtp_host),fld('s_smtp_port','Порт',s.smtp_port||587,'number'))+ row2(fld('s_smtp_user','Потребител',s.smtp_user),fld('s_smtp_pass','Парола',s.smtp_pass,'password'))+ row2(selSecure('s_smtp_secure',s.smtp_secure),fld('s_smtp_from_email','From имейл',s.smtp_from_email))+ fld('s_smtp_from_name','From име',s.smtp_from_name)+ '</div>'; html+='</div><button class="btn p" onclick="saveSettings()">💾 Запази настройките</button>'; c.innerHTML=html; } function fld(id,label,val,type){ return '<label>'+label+'</label><input id="'+id+'" type="'+(type||'text')+'" value="'+(val==null?'':String(val).replace(/"/g,'"'))+'">'; } function row2(a,b){ return '<div class="row2"><div>'+a+'</div><div>'+b+'</div></div>'; } function selCur(id,val){ return '<label>Валута</label><select id="'+id+'"><option'+(val==='EUR'?' selected':'')+'>EUR</option><option'+(val==='BGN'?' selected':'')+'>BGN</option></select>'; } function selSecure(id,val){ return '<label>Криптиране</label><select id="'+id+'"><option value="tls"'+(val==='tls'?' selected':'')+'>TLS (587)</option><option value="ssl"'+(val==='ssl'?' selected':'')+'>SSL (465)</option></select>'; } async function saveSettings(){ var payload={ name:document.getElementById('s_name').value, eik:document.getElementById('s_eik').value, vat_number:document.getElementById('s_vat').value, address:document.getElementById('s_address').value, city:document.getElementById('s_city').value, mol:document.getElementById('s_mol').value, iban:document.getElementById('s_iban').value, bic:document.getElementById('s_bic').value, bank_name:document.getElementById('s_bank').value, default_vat_percent:document.getElementById('s_defvat').value, currency:document.getElementById('s_currency').value, prefix_proforma:document.getElementById('s_pref_proforma').value, prefix_invoice:document.getElementById('s_pref_invoice').value, prefix_credit_note:document.getElementById('s_pref_credit').value, prefix_debit_note:document.getElementById('s_pref_debit').value, prefix_offer:document.getElementById('s_pref_offer').value, prefix_quote:document.getElementById('s_pref_offer').value, smtp_host:document.getElementById('s_smtp_host').value, smtp_port:document.getElementById('s_smtp_port').value, smtp_user:document.getElementById('s_smtp_user').value, smtp_pass:document.getElementById('s_smtp_pass').value, smtp_secure:document.getElementById('s_smtp_secure').value, smtp_from_email:document.getElementById('s_smtp_from_email').value, smtp_from_name:document.getElementById('s_smtp_from_name').value }; var r=await af('company_settings','POST',payload); if(r) toast('Настройките са запазени'); else toast('Грешка при запис'); } async function moduleGuard(key,label){ try{ await syncLicenseFromMaster(); var r=await af('branding_settings'); if(r && (r.license_state==='locked'||r.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">🔒</div><h2>Хъбът е заключен</h2>'+ '<p style="margin-top:8px;font-size:13px">'+(r.license_message||'Лицензът не е активен.')+'</p></div>'; return false; } if(r && r.modules_enabled && r.modules_enabled.hasOwnProperty(key) && !r.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">🔒</div><h2>'+label+' е изключен</h2>'+ '<p style="margin-top:8px;font-size:13px">Този модул е изключен за тази инсталация. Свържи се с администратора.</p>'+ '<a href="index.html" style="display:inline-block;margin-top:16px;color:#6c63ff">← Обратно към Hub</a></div>'; return false; } }catch(e){} return true; } async function syncLicenseFromMaster(){ try{ var c=await af('license_client_config'); if(!c||!c.verify_url)return; var rr=await fetch(c.verify_url,{cache:'no-store',mode:'cors'}); var payload=await rr.json(); if(payload&&payload.state) await af('license_sync','POST',{payload:payload}); }catch(e){} } renderTabs(); moduleGuard('invoicing','Фактурирането').then(ok=>{ if(ok) renderContent(); }); </script> </body> </html>
| ver. 1.4 |
Github
|
.
| PHP 8.2.31 | Generation time: 0 |
proxy
|
phpinfo
|
Settings