File manager - Edit - /home/webtrixia/ino.webtrixia.com/app.js
Back
const API = (window.API_BASE || '') + '/api'; const $ = (s) => document.querySelector(s); const $$ = (s) => Array.from(document.querySelectorAll(s)); function token() { return localStorage.getItem('jwt') || ''; } function setToken(t){ if(t){ localStorage.setItem('jwt', t);} else { localStorage.removeItem('jwt'); }} async function api(path, opts={}){ const headers = Object.assign({'Content-Type':'application/json'}, opts.headers||{}); if(token()) headers['Authorization'] = 'Bearer ' + token(); const res = await fetch(API + path, Object.assign({}, opts, { headers })); if(!res.ok){ let msg = 'HTTP '+res.status; try { const j = await res.json(); msg = j.error || msg; } catch{} throw new Error(msg); } return res.json(); } function showTab(name){ $$('.tab').forEach(el => el.classList.add('hidden')); $('#tab-'+name).classList.remove('hidden'); } // Функция за форматиране на парични суми function fmtMoney(n, c='BGN'){ return (Number(n)||0).toLocaleString('bg-BG', { style:'currency', currency:c }); } // Функции за форматиране на число (за полетата за въвеждане на число) function parseLocaleNumber(str) { if (!str) return 0; return Number(str.replace(',', '.')); } function formatLocaleNumber(num, decimals = 2) { return num.toLocaleString('bg-BG', { minimumFractionDigits: decimals, maximumFractionDigits: decimals }); } function bindLocaleNumberField(input, decimals = 2) { if (!input) return; // Първоначално форматираме към bg-BG -> 0,00 const initVal = parseLocaleNumber(input.value); input.value = formatLocaleNumber(initVal, decimals); // При напускане – форматираме към bg-BG -> 0,00 input.addEventListener('blur', () => { const val = parseLocaleNumber(input.value); input.value = formatLocaleNumber(val, decimals); }); /* // При фокус – показваме „скрития“ формат с точка (лесно за редакция) input.addEventListener('focus', () => { input.value = String(input.value).replace(/\./g, '').replace(',', '.'); }); */ // При въвеждане – обнови редовата сума и тотала input.addEventListener('input', () => { const row = input.closest('.grid'); if (row) updateRowSum(row); if (typeof recalcInvoiceDraft === 'function') recalcInvoiceDraft(); }); } // Date utils (работят без TZ изместване) function toISODateLocal(date) { const y = date.getFullYear(); const m = String(date.getMonth() + 1).padStart(2, '0'); const d = String(date.getDate()).padStart(2, '0'); return `${y}-${m}-${d}`; } function fromISODateLocal(iso) { const [y, m, d] = (iso || '').split('-').map(Number); if (!y || !m || !d) return null; return new Date(y, m - 1, d); // локален 00:00 } function formatDMY(iso) { const [y, m, d] = (iso || '').split('-'); // ["YYYY","MM","DD"] if (!y || !m || !d) return iso || ''; // ако не е валидна дата – връща оригинала return `${d}-${m}-${y}`; // ["DD","MM","YYYY"] } function readDateInput(id) { const el = document.getElementById(id); return el ? (el.value || '') : ''; } function setDateInput(id, iso) { const el = document.getElementById(id); if (el) el.value = iso || ''; } function todayISO() { return toISODateLocal(new Date()); } function plusDaysISO(iso, days) { const dt = fromISODateLocal(iso); if (!dt) return ''; dt.setDate(dt.getDate() + days); return toISODateLocal(dt); } // Допълнителни форматери за дата (за полетата за въвеждане на дата) function isoToDMY(iso) { if (!iso) return ''; const [y,m,d] = String(iso).split('-'); return `${d.padStart(2,'0')}-${m.padStart(2,'0')}-${y}`; } function daysInMonth(y, m) { return new Date(y, m, 0).getDate(); // m = 1..12 } function parseDMYToISO(input) { if (!input) return null; // маха разделители (всичко различно от цифри) const digits = String(input).replace(/[^\d]/g,''); // приемаме само формат DDMMYYYY (осем цифри) if (digits.length !== 8) return null; const dd = +digits.slice(0,2); const mm = +digits.slice(2,4); const yyyy = +digits.slice(4,8); if (!(yyyy>=1900 && yyyy<=9999)) return null; if (!(mm>=1 && mm<=12)) return null; if (!(dd>=1 && dd<=daysInMonth(yyyy, mm))) return null; const d = String(dd).padStart(2,'0'); const m = String(mm).padStart(2,'0'); const y = String(yyyy); return `${y}-${m}-${d}`; // ISO } function setErr(el, on) { if (!el) return; el.classList.toggle('err', !!on); } // Закачане на огледални полета function attachDatePair(dateId, textId, btnSel, onChange) { const dateEl = document.getElementById(dateId); // скрит <input type="date"> const textEl = document.getElementById(textId); // видим text ДД-ММ-ГГГГ const btnEl = document.querySelector(btnSel); // бутон (календар) if (!dateEl || !textEl || !btnEl) return; // нативно -> текст const syncText = () => { textEl.value = isoToDMY(dateEl.value); setErr(textEl, false); onChange && onChange(); // хук за доп. валидации }; dateEl.addEventListener('change', syncText); dateEl.addEventListener('input', syncText); syncText(); // текст -> нативно textEl.addEventListener('blur', () => { const iso = parseDMYToISO(textEl.value); if (iso) { dateEl.value = iso; setErr(textEl, false); } else if (textEl.value.trim() !== '') { setErr(textEl, true); } else { setErr(textEl, false); } onChange && onChange(); }); // бутон -> отваря календара btnEl.addEventListener('click', () => { if (typeof dateEl.showPicker === 'function') dateEl.showPicker(); else { dateEl.focus(); dateEl.click(); } }); } // Валидация: (падеж >= дата) function validateDueNotBeforeDate() { const dateEl = document.getElementById('inv-date'); const dueEl = document.getElementById('inv-due'); const dueText = document.getElementById('inv-due-text'); const d = dateEl?.value; const u = dueEl?.value; if (!d || !u) { setErr(dueText, false); return true; } const ok = (new Date(u) >= new Date(d)); setErr(dueText, !ok); return ok; } // Функция за показване на съобщение function showMessage(el, text, isError=false, duration=3600){ el.textContent = text; el.classList.remove('err'); if(isError) el.classList.add('err'); el.classList.add('show'); setTimeout(() => { el.classList.remove('show'); setTimeout(() => { el.textContent = ''; el.classList.remove('err'); }, 400); // изчаква края на анимацията }, duration); } // Функция за изчистване на формата (черновата) за създаване на фактура function resetInvoiceDraft() { const me = window.currentUser || {}; const cur = me.default_currency || document.getElementById('s-cur')?.value || 'BGN'; const taxNum = (typeof me.default_tax_rate === 'number' ? me.default_tax_rate : parseLocaleNumber(document.getElementById('s-tax')?.value || '0')); // Клиент - избери първия в списъка, ако има const clientSel = document.getElementById('inv-client'); if (clientSel && clientSel.options.length > 0) clientSel.selectedIndex = 0; // Дати: днешна дата и падеж (след 14 дни) const today = todayISO(); setDateInput('inv-date', today); setDateInput('inv-due', plusDaysISO(today, 14)); // Закачи огледалните текстови полета attachDatePair('inv-date', 'inv-date-text', '.calendar-btn[data-target="inv-date"]', validateDueNotBeforeDate); attachDatePair('inv-due', 'inv-due-text', '.calendar-btn[data-target="inv-due"]', validateDueNotBeforeDate); // Валутa, ДДС, плащане, ДДС основание const elCur = document.getElementById('inv-cur'); if (elCur) elCur.value = cur; const elTax = document.getElementById('inv-tax'); if (elTax) elTax.value = formatLocaleNumber(taxNum, 2); const elPay = document.getElementById('inv-pay'); if (elPay) elPay.value = 'cash'; const elPS = document.getElementById('inv-place'); if (elPS) elPS.value = ''; const elVR = document.getElementById('inv-vatreason'); if (elVR) elVR.value = ''; // Редове с артикули – зануляване и един празен ред const itemsWrap = document.getElementById('items'); if (itemsWrap) { itemsWrap.innerHTML = ''; addItemRow('', 1, 0); } // Преизчисление на живо if (typeof recalcInvoiceDraft === 'function') recalcInvoiceDraft(); } // Функция за смяна на иконата на бутона за показване/скриване на паролата в раздел настройки function attachPasswordToggle(inputId, buttonId) { const inp = document.getElementById(inputId); const btn = document.getElementById(buttonId); if (!inp || !btn) return; const setVisible = (visible) => { inp.type = visible ? 'text' : 'password'; btn.classList.toggle('is-visible', visible); btn.setAttribute('aria-pressed', String(visible)); btn.setAttribute('aria-label', visible ? 'Скрий паролата' : 'Покажи паролата'); }; // Клик: toggle btn.addEventListener('click', () => { setVisible(inp.type === 'password'); }); // Достъпен вариант с клавиатура (Space/Enter) btn.addEventListener('keydown', (e) => { if (e.code === 'Space' || e.code === 'Enter') { e.preventDefault(); setVisible(inp.type === 'password'); } }); } // Прикачи я към четирите полета attachPasswordToggle('s-old', 'toggle-old'); attachPasswordToggle('s-old2', 'toggle-old2'); attachPasswordToggle('s-new', 'toggle-new'); attachPasswordToggle('s-new2', 'toggle-new2'); // Auth UI const authSec = $('#auth'); const appUI = $('#appUI'); const userBox = $('#userBox'); $('#btnLogin').onclick = async() => { const email = $('#loginEmail').value.trim(); const password = $('#loginPass').value; const msg = $('#loginMsg'); // Валидации преди заявката if (!email) { showMessage(msg, 'Имейл е задължителен', true); return; } if (!password) { showMessage(msg, 'Парола е задължителна', true); return; } try{ const r = await api('/auth/login',{ method:'POST', body: JSON.stringify({email, password}) }); setToken(r.token); showMessage(msg, 'Входът е успешен', false, 2000); await onLoggedIn(); }catch(e){ showMessage(msg, 'Невалидни данни за вход', true, 6000); } }; $('#btnRegister').onclick = async() => { const email = $('#regEmail').value.trim(); const password = $('#regPass').value; const company_name = $('#regCompany').value.trim(); const msg = $('#registerMsg'); // Валидации преди заявката if (!email) { showMessage(msg, 'Имейл е задължителен', true); return; } if (!password) { showMessage(msg, 'Парола е задължителна', true); return; } if (password.length < 6) { showMessage(msg, 'Парола трябва да е поне 6 символа', true); return; } try{ const r = await api('/auth/register',{ method:'POST', body: JSON.stringify({email, password, company_name}) }); setToken(r.token); showMessage(msg, 'Акаунтът е регистриран успешно', false, 4000); await onLoggedIn(); }catch(e){ showMessage(msg, 'Невалидни данни за регистрация', true, 6000); } }; $('#btnLogout').onclick = () => { setToken(''); location.reload(); }; // показване/скриване на мини формата при забравена парола $('#forgotPassLink').onclick = (e) => { e.preventDefault(); $('#forgotWrap').classList.toggle('hidden'); }; // изпращане на заявка за линк за възстановяване $('#btnForgotSend').onclick = async () => { const email = $('#forgotEmail').value.trim(); const msg = $('#forgotMsg'); if (!email) { showMessage(msg, 'Моля, въведете имейл', true); return; } try { await api('/auth/forgot', { method:'POST', body: JSON.stringify({ email }) }); showMessage(msg, 'Ако имейлът съществува, изпратихме инструкции за възстановяване.', false, 5000); } catch (e) { // по съображения за сигурност връщаме същото showMessage(msg, 'Ако имейлът съществува, изпратихме инструкции за възстановяване.', false, 5000); } }; // Tabs $$('nav button[data-tab]').forEach(btn => btn.onclick = () => showTab(btn.dataset.tab)); async function onLoggedIn(){ authSec.classList.add('hidden'); appUI.classList.remove('hidden'); const me = await api('/me'); window.currentUser = me; userBox.textContent = me.company_name || me.email; // settings $('#s-company').value = me.company_name || ''; $('#s-person').value = me.person || ''; $('#s-eik').value = me.eik || ''; $('#s-vat').value = me.vat || ''; $('#s-email').value = me.email_pub || ''; $('#s-phone').value = me.phone || ''; $('#s-address').value = me.address || ''; $('#s-bank').value = me.bank || ''; $('#s-bic').value = me.bic || ''; $('#s-iban').value = me.iban || ''; $('#s-tax').value = formatLocaleNumber(me.default_tax_rate ?? 0, 2); $('#s-cur').value = me.default_currency || 'BGN'; // defaults for invoice form $('#inv-tax').value = formatLocaleNumber(me.default_tax_rate ?? 0, 2); $('#inv-cur').value = me.default_currency || 'BGN'; // Закачи поведение за локален формат към двете полета за ДДС bindLocaleNumberField(document.getElementById('s-tax'), 2); bindLocaleNumberField(document.getElementById('inv-tax'), 2); // Попълване на нативните полета с ISO const today = todayISO(); setDateInput('inv-date', today); setDateInput('inv-due', plusDaysISO(today, 14)); // Закачи огледалните текстови полета attachDatePair('inv-date', 'inv-date-text', '.calendar-btn[data-target="inv-date"]', validateDueNotBeforeDate); attachDatePair('inv-due', 'inv-due-text', '.calendar-btn[data-target="inv-due"]', validateDueNotBeforeDate); await loadClients(); await loadInvoices(); recalcInvoiceDraft(); } $('#btnSaveSettings').onclick = async() => { const msg = $('#settingsMsg'); try { await api('/me', { method:'PUT', body: JSON.stringify({ company_name: $('#s-company').value, person: $('#s-person').value, eik: $('#s-eik').value, vat: $('#s-vat').value, email_pub: $('#s-email').value, phone: $('#s-phone').value, address: $('#s-address').value, bank: $('#s-bank').value, bic: $('#s-bic').value, iban: $('#s-iban').value, default_tax_rate: parseLocaleNumber($('#s-tax').value || '0'), default_currency: $('#s-cur').value, }) }); showMessage(msg, 'Записано'); } catch(e) { showMessage(msg, 'Грешка при запазване на настройките', true); } }; // Смяна на парола (в Настройки) $('#btnChangePass').onclick = async () => { const msg = $('#pwdMsg'); const old1 = $('#s-old').value; const old2 = $('#s-old2').value; const new1 = $('#s-new').value; const new2 = $('#s-new2').value; // локални проверки if (!old1 || !old2 || !new1 || !new2) { showMessage(msg, 'Моля, попълнете всички полета', true); return; } if (old1 !== old2) { showMessage(msg, 'Старата парола и потвърждението не съвпадат', true); return; } if (new1 !== new2) { showMessage(msg, 'Новата парола и потвърждението не съвпадат', true); return; } if (new1.length < 6) { showMessage(msg, 'Новата парола трябва да е поне 6 символа', true); return; } try { await api('/me/password', { method: 'PUT', body: JSON.stringify({ old_password: old1, new_password: new1 }) }); showMessage(msg, 'Паролата е сменена. Излизане...', false, 3000); // автоматично излизане след 3 секунди setTimeout(() => { setToken(''); location.reload(); }, 3000); } catch (e) { showMessage(msg, 'Грешка при смяна на парола', true); } }; async function loadClients(){ const list = await api('/clients'); const tbody = $('#clientsTbl tbody'); tbody.innerHTML = ''; if (!Array.isArray(list) || list.length === 0) { const tr = document.createElement('tr'); tr.innerHTML = `<td colspan="4" style="text-align:center;color:#777;">Няма контрагенти</td>`; tbody.appendChild(tr); // изчисти dropdown за фактура $('#inv-client').innerHTML = ''; return; } list.forEach(c => { const tr = document.createElement('tr'); tr.innerHTML = ` <td>${c.name}</td> <td>${c.email || ''}</td> <td>${c.phone || ''}</td> <td> <button data-id="${c.id}" class="editc">Редакция</button> <button data-id="${c.id}" class="delc">Изтрий</button> </td>`; tbody.appendChild(tr); }); // запълни dropdown за фактуриране const sel = $('#inv-client'); sel.innerHTML = ''; list.forEach(c => { const opt = document.createElement('option'); opt.value = c.id; opt.textContent = c.name; sel.appendChild(opt); }); } $('#btnAddClient').onclick = async() => { const name = $('#c-name').value.trim(); const person = $('#c-person').value.trim(); const eik = $('#c-eik').value.trim(); const vat = $('#c-vat').value.trim(); const email = $('#c-email').value.trim(); const phone = $('#c-phone').value.trim(); const address = $('#c-address').value.trim(); const bank = $('#c-bank').value.trim(); const bic = $('#c-bic').value.trim(); const iban = $('#c-iban').value.trim(); const notes = $('#c-notes').value.trim(); const msg = $('#clientMsg'); if(!name) { showMessage(msg, 'Име е задължително', true); return; } try { await api('/clients',{ method:'POST', body: JSON.stringify({ name, person, eik, vat, email, phone, address, bank, bic, iban, notes }) }); $('#c-name').value = $('#c-person').value = $('#c-eik').value = $('#c-vat').value = $('#c-email').value = $('#c-phone').value = $('#c-address').value = $('#c-bank').value = $('#c-bic').value = $('#c-iban').value = $('#c-notes').value = ''; showMessage(msg, 'Контрагентът е добавен успешно'); await loadClients(); } catch(e) { showMessage(msg, 'Грешка при добавяне на контрагент', true); } }; // Делегиране на действията в таблицата с контрагенти const clientsTbl = $('#clientsTbl'); if (clientsTbl) { clientsTbl.addEventListener('click', async (e) => { const btn = e.target.closest('button'); if (!btn) return; const id = Number(btn.dataset.id); if (!id) return; // Изтриване if (btn.classList.contains('delc')) { if (!confirm('Изтриване на контрагента?')) return; await api('/clients/' + id, { method: 'DELETE' }); await loadClients(); return; } // Редакция if (btn.classList.contains('editc')) { try { // вземи свежи данни за клиента (или извикай /clients и намери по id) const list = await api('/clients'); const c = list.find(x => Number(x.id) === id); if (!c) { alert('Клиентът не е намерен'); return; } // попълни формата $('#ce-id').value = c.id; $('#ce-name').value = c.name || ''; $('#ce-person').value = c.person || ''; $('#ce-eik').value = c.eik || ''; $('#ce-vat').value = c.vat || ''; $('#ce-email').value = c.email || ''; $('#ce-phone').value = c.phone || ''; $('#ce-address').value = c.address || ''; $('#ce-bank').value = c.bank || ''; $('#ce-bic').value = c.bic || ''; $('#ce-iban').value = c.iban || ''; $('#ce-notes').value = c.notes || ''; // покажи панела $('#clientEdit').classList.remove('hidden'); $('#clientEditMsg').textContent = ''; } catch (err) { alert('Грешка при зареждане на клиента: ' + (err.message || err)); } } }); } // Запази редакцията $('#btnSaveClient').onclick = async () => { const id = Number($('#ce-id').value); const body = { name: $('#ce-name').value.trim(), person: $('#ce-person').value.trim(), eik: $('#ce-eik').value.trim(), vat: $('#ce-vat').value.trim(), email: $('#ce-email').value.trim(), phone: $('#ce-phone').value.trim(), address: $('#ce-address').value.trim(), bank: $('#ce-bank').value.trim(), bic: $('#ce-bic').value.trim(), iban: $('#ce-iban').value.trim(), notes: $('#ce-notes').value.trim(), }; const msg = $('#clientEditMsg'); try { await api('/clients/' + id, { method: 'PUT', body: JSON.stringify(body) }); showMessage(msg, 'Записано', false); // презареди списъка и на фактурите, заради името на контрагента await loadInvoices(); // презареди списъка на контрагентите и скрий формата след малко await loadClients(); setTimeout(() => { $('#clientEdit').classList.add('hidden'); }, 400); } catch (e) { showMessage(msg, 'Грешка при запис', true, 4000); } }; // Отказ $('#btnCancelClient').onclick = () => { $('#clientEdit').classList.add('hidden'); const msg = $('#clientEditMsg'); msg.textContent = ''; msg.classList.remove('err', 'show', 'hidden'); }; function addItemRow(desc='', qty=1, price=0){ const wrap = document.createElement('div'); wrap.className = 'grid four'; wrap.innerHTML = ` <div> <input class="it-desc" placeholder="Описание" value="${desc}"> </div> <div> <input class="it-qty" type="text" inputmode="decimal" value="${qty}"> </div> <div> <input class="it-price" type="text" inputmode="decimal" value="${price}"> </div> <div> <label class="it-sum" style="display:flex;align-items:center;">0,00</label> </div> `; document.querySelector('#items').appendChild(wrap); // Закачи поведение за локален формат към двете полета bindLocaleNumberField(wrap.querySelector('.it-qty'), 2); bindLocaleNumberField(wrap.querySelector('.it-price'), 2); // първоначална калкулация за реда и общо updateRowSum(wrap); if (typeof recalcInvoiceDraft === 'function') recalcInvoiceDraft(); } $('#btnAddItem').onclick = () => addItemRow(); addItemRow('',1,0); // помощник: пресмята сумата на конкретен ред function updateRowSum(row){ const q = parseLocaleNumber(row.querySelector('.it-qty')?.value || '0'); const p = parseLocaleNumber(row.querySelector('.it-price')?.value || '0'); const sum = (q * p); const sumEl = row.querySelector('.it-sum'); const cur = $('#inv-cur').value || 'BGN'; if (sumEl) sumEl.textContent = fmtMoney(sum, cur); } // Делегирани слушатели върху контейнера с редовете const itemsWrap = document.querySelector('#items'); // на всяка промяна в количество/цена -> редова сума + обща калкулация itemsWrap.addEventListener('input', (e) => { if (e.target.matches('.it-qty, .it-price')) { const row = e.target.closest('.grid'); if (row) updateRowSum(row); recalcInvoiceDraft(); // обнови междинна + ДДС + крайна сума } }); // Жива калкулация за фактура function recalcInvoiceDraft(){ const items = Array.from($('#items').children).map(row => { const q = parseLocaleNumber(row.querySelector('.it-qty')?.value || '0'); const p = parseLocaleNumber(row.querySelector('.it-price')?.value || '0'); return q*p; }); const subtotal = items.reduce((a,b)=>a+b,0); const taxRate = parseLocaleNumber($('#inv-tax')?.value || '0'); const total = subtotal * (1 + taxRate/100); const cur = $('#inv-cur').value || 'BGN'; // показване на сумата в някакво поле/елемент if($('#inv-total')){ $('#inv-total').textContent = fmtMoney(total, cur); } } // Вързваме калкулацията и към промяна на ДДС и валута $('#inv-tax')?.addEventListener('input', recalcInvoiceDraft); $('#inv-cur')?.addEventListener('input', recalcInvoiceDraft); async function loadInvoices(){ const list = await api('/invoices'); const tbody = $('#invTbl tbody'); tbody.innerHTML=''; if (!Array.isArray(list) || list.length === 0) { const tr = document.createElement('tr'); tr.innerHTML = `<td colspan="6" style="text-align:center;color:#777;">Няма фактури</td>`; tbody.appendChild(tr); return; } list.forEach(inv => { const subtotal = (inv.items||[]).reduce((s,it)=> s + Number(it.qty||0)*Number(it.price||0), 0); const taxRate = Number(inv.tax_rate || 0); const total = subtotal*(1 + taxRate/100); const clientLabel = inv.client_name ?? inv.client?.name ?? inv.client_id ?? ''; const cur = inv.currency || 'BGN'; const tr = document.createElement('tr'); tr.innerHTML = ` <td>${inv.number}</td> <td>${formatDMY(inv.date)}</td> <td>${formatDMY(inv.due_date)}</td> <td>${clientLabel}</td> <td>${fmtMoney(total, cur)}</td> <td> <button class="printO" data-id="${inv.id}">Печат (Ориг.)</button> <button class="printC" data-id="${inv.id}">Печат (Копие)</button> </td> `; tbody.appendChild(tr); }); } $('#btnCreateInv').onclick = async() => { if (!validateDueNotBeforeDate()) { alert('Срокът за плащане не може да е преди датата на фактурата.'); return; } const client_id = Number($('#inv-client').value); const date = readDateInput('inv-date'); // 'YYYY-MM-DD' const due_date = readDateInput('inv-due'); // 'YYYY-MM-DD' const currency = $('#inv-cur').value || 'BGN'; const tax_rate = parseLocaleNumber($('#inv-tax').value || '0'); const payment_method = $('#inv-pay').value; const place_of_supply = $('#inv-place').value.trim(); const vat_reason = $('#inv-vatreason').value.trim(); const items = Array.from($('#items').children).map(row => ({ description: row.querySelector('.it-desc').value, qty: parseLocaleNumber(row.querySelector('.it-qty')?.value || '0'), price: parseLocaleNumber(row.querySelector('.it-price')?.value || '0'), })); const msg = $('#invMsg'); try { const r = await api('/invoices',{ method:'POST', body: JSON.stringify({ client_id, date, due_date, currency, tax_rate, payment_method, place_of_supply, vat_reason, items }) }); showMessage(msg, 'Създадена: ' + r.number); await loadInvoices(); // изчакай презареждането } catch(e) { showMessage(msg, 'Грешка при създаване на фактура', true); } }; // Изчисти формата за създаване на фактури $('#btnClearInv').onclick = async() => { if (!confirm('Сигурни ли сте, че искате да изчистите формата за създаване на фактури?')) return; await resetInvoiceDraft(); const msg = $('#invMsg'); showMessage(msg, 'Формата за създаване на фактури е изчистена'); // Скрол до заглавието на сайта const header = document.getElementById('mainHeader'); if (header) { header.scrollIntoView({ behavior: 'smooth', block: 'start' }); } }; // Един слушател за кликове в таблицата с фактури const invTbl = $('#invTbl'); if (invTbl) { invTbl.addEventListener('click', async (e) => { const btn = e.target.closest('button.printO, button.printC'); if (!btn) return; // кликът не е върху бутон за печат const id = Number(btn.dataset.id); if (!id) return; // Определи типа според класа на бутона const type = btn.classList.contains('printC') ? 'copy' : 'original'; // Деактивирай бутона временно (UX) const prev = btn.textContent; btn.disabled = true; btn.textContent = 'Генерирам...'; try { const r = await api(`/invoices/${id}/print_url`, { method: 'POST', body: JSON.stringify({ type }) }); if (r?.url) { window.open(r.url, '_blank'); } else { alert('Неуспешно генериране на линк за печат.'); } } catch (err) { alert('Грешка при генериране на печат: ' + (err?.message || err)); } finally { btn.disabled = false; btn.textContent = prev; } }); } // Initialize if(token()){ onLoggedIn(); } else { authSec.classList.remove('hidden'); }
| ver. 1.4 |
Github
|
.
| PHP 8.2.31 | Generation time: 0 |
proxy
|
phpinfo
|
Settings