This commit is contained in:
2026-02-19 14:45:19 -06:00
parent 48fa86916b
commit b24a360fba
5 changed files with 438 additions and 20 deletions

View File

@@ -81,7 +81,7 @@ document.addEventListener('DOMContentLoaded', () => {
//loadInvoices();
setDefaultDate();
checkCurrentLogo();
loadLaborRate();
// *** FIX 3: Gespeicherten Tab wiederherstellen (oder 'quotes' als Default) ***
const savedTab = localStorage.getItem('activeTab') || 'quotes';
showTab(savedTab);
@@ -219,30 +219,28 @@ async function loadCustomers() {
function renderCustomers() {
const tbody = document.getElementById('customers-list');
tbody.innerHTML = customers.map(customer => {
// Logik: Line 1-4 zusammenbauen
// filter(Boolean) entfernt null/undefined/leere Strings
const lines = [customer.line1, customer.line2, customer.line3, customer.line4].filter(Boolean);
// City, State, Zip anhängen
const cityStateZip = [customer.city, customer.state, customer.zip_code].filter(Boolean).join(' ');
// Alles zusammenfügen
let fullAddress = lines.join(', ');
if (cityStateZip) {
fullAddress += (fullAddress ? ', ' : '') + cityStateZip;
}
if (cityStateZip) fullAddress += (fullAddress ? ', ' : '') + cityStateZip;
// QBO Status
const qboStatus = customer.qbo_id
? `<span class="inline-block px-2 py-0.5 text-xs font-semibold rounded-full bg-green-100 text-green-800" title="QBO ID: ${customer.qbo_id}">QBO ✓</span>`
: `<button onclick="exportCustomerToQbo(${customer.id})" class="inline-block px-2 py-0.5 text-xs font-semibold rounded-full bg-orange-100 text-orange-800 hover:bg-orange-200 cursor-pointer" title="Kunde nach QBO exportieren">QBO Export</button>`;
return `
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">${customer.name}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
${customer.name} ${qboStatus}
</td>
<td class="px-6 py-4 text-sm text-gray-500">${fullAddress || '-'}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${customer.account_number || '-'}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium space-x-2">
<button onclick="editCustomer(${customer.id})" class="text-blue-600 hover:text-blue-900">Edit</button>
<button onclick="deleteCustomer(${customer.id})" class="text-red-600 hover:text-red-900">Delete</button>
</td>
</tr>
`;
</tr>`;
}).join('');
}
@@ -351,7 +349,34 @@ async function deleteCustomer(id) {
alert('Error deleting customer');
}
}
async function exportCustomerToQbo(customerId) {
const customer = customers.find(c => c.id === customerId);
if (!customer) return;
if (!confirm(`Kunde "${customer.name}" nach QuickBooks Online exportieren?`)) return;
showSpinner('Exportiere Kunde nach QBO...');
try {
const response = await fetch(`/api/customers/${customerId}/export-qbo`, { method: 'POST' });
const result = await response.json();
if (response.ok) {
alert(`✅ Kunde "${result.name}" erfolgreich in QBO erstellt (ID: ${result.qbo_id}).`);
// Kunden-Liste neu laden
const custResponse = await fetch('/api/customers');
customers = await custResponse.json();
renderCustomers();
} else {
alert(`❌ Fehler: ${result.error}`);
}
} catch (error) {
console.error('Error exporting customer:', error);
alert('Netzwerkfehler beim Export.');
} finally {
hideSpinner();
}
}
// Quote Management
async function loadQuotes() {
try {
@@ -916,7 +941,7 @@ function addInvoiceItem(item = null) {
<div class="col-span-2">
<label class="block text-xs font-medium text-gray-700 mb-1">Type (Internal)</label>
<select data-item="${itemId}" data-field="qbo_item_id" class="w-full px-2 py-2 border border-gray-300 rounded-md text-sm bg-white" onchange="updateInvoiceItemPreview(this.closest('[id^=invoice-item-]'), ${itemId})">
<select data-item="${itemId}" data-field="qbo_item_id" class="w-full px-2 py-2 border border-gray-300 rounded-md text-sm bg-white" onchange="handleTypeChange(this, ${itemId})">
<option value="9" ${item && item.qbo_item_id == '9' ? 'selected' : ''}>Parts</option>
<option value="5" ${item && item.qbo_item_id == '5' ? 'selected' : ''}>Labor</option>
</select>
@@ -999,7 +1024,27 @@ function updateInvoiceItemPreview(itemDiv, itemId) {
descPreview.textContent = preview || 'New item';
}
}
function handleTypeChange(selectEl, itemId) {
const itemDiv = selectEl.closest(`[id^=invoice-item-]`);
// Wenn Labor gewählt und Rate leer → Labor Rate eintragen
if (selectEl.value === '5' && qboLaborRate) {
const rateInput = itemDiv.querySelector('[data-field="rate"]');
if (rateInput && (!rateInput.value || rateInput.value === '0')) {
rateInput.value = qboLaborRate;
// Amount neu berechnen
const qtyInput = itemDiv.querySelector('[data-field="quantity"]');
const amountInput = itemDiv.querySelector('[data-field="amount"]');
if (qtyInput.value) {
const qty = parseFloat(qtyInput.value) || 0;
amountInput.value = (qty * qboLaborRate).toFixed(2);
}
}
}
updateInvoiceItemPreview(itemDiv, itemId);
updateInvoiceTotals();
}
function removeInvoiceItem(itemId) {
document.getElementById(`invoice-item-${itemId}`).remove();
updateInvoiceTotals();
@@ -1261,4 +1306,54 @@ async function importFromQBO() {
btn.disabled = false;
}
}
// =====================================================
// 3. Labor Rate laden und in addInvoiceItem verwenden
// NEUE globale Variable + Lade-Funktion
// =====================================================
let qboLaborRate = null; // Wird beim Start geladen
async function loadLaborRate() {
try {
const response = await fetch('/api/qbo/labor-rate');
const data = await response.json();
if (data.rate) {
qboLaborRate = data.rate;
console.log(`💰 Labor Rate geladen: $${qboLaborRate}`);
}
} catch (e) {
console.log('Labor Rate konnte nicht geladen werden, verwende keinen Default.');
}
}
// =====================================================
// 5. Spinner Funktionen — NEUE Funktionen hinzufügen
// Wird bei QBO-Operationen angezeigt
// =====================================================
function showSpinner(message = 'Bitte warten...') {
let overlay = document.getElementById('qbo-spinner');
if (!overlay) {
overlay = document.createElement('div');
overlay.id = 'qbo-spinner';
overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.4);display:flex;align-items:center;justify-content:center;z-index:9999;';
document.body.appendChild(overlay);
}
overlay.innerHTML = `
<div class="bg-white rounded-xl shadow-2xl px-8 py-6 flex items-center gap-4">
<svg class="animate-spin h-8 w-8 text-blue-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span class="text-lg font-medium text-gray-700" id="qbo-spinner-text">${message}</span>
</div>`;
overlay.style.display = 'flex';
}
function hideSpinner() {
const overlay = document.getElementById('qbo-spinner');
if (overlay) overlay.style.display = 'none';
}
window.openInvoiceModal = openInvoiceModal;

View File

@@ -167,9 +167,16 @@ function renderInvoiceRow(invoice) {
// --- BUTTONS (Edit | QBO | PDF HTML | Payment | Del) ---
const editBtn = `<button onclick="window.invoiceView.edit(${invoice.id})" class="text-blue-600 hover:text-blue-900">Edit</button>`;
const qboBtn = hasQbo
? `<span class="text-gray-400 text-xs cursor-pointer" title="In QBO (ID: ${invoice.qbo_id}) — Click to reset" onclick="window.invoiceView.resetQbo(${invoice.id})">✓ QBO</span>`
: `<button onclick="window.invoiceView.exportToQBO(${invoice.id})" class="text-orange-600 hover:text-orange-900" title="Export to QuickBooks">QBO Export</button>`;
// QBO Button — nur aktiv wenn Kunde eine qbo_id hat
const customerHasQbo = !!invoice.customer_qbo_id;
let qboBtn;
if (hasQbo) {
qboBtn = `<span class="text-gray-400 text-xs cursor-pointer" title="In QBO (ID: ${invoice.qbo_id}) — Click to reset" onclick="window.invoiceView.resetQbo(${invoice.id})">✓ QBO</span>`;
} else if (!customerHasQbo) {
qboBtn = `<span class="text-gray-300 text-xs cursor-not-allowed" title="Kunde muss erst nach QBO exportiert werden">QBO ⚠</span>`;
} else {
qboBtn = `<button onclick="window.invoiceView.exportToQBO(${invoice.id})" class="text-orange-600 hover:text-orange-900" title="Export to QuickBooks">QBO Export</button>`;
}
const pdfBtn = draft
? `<span class="text-gray-300 text-sm cursor-not-allowed" title="PDF erst nach QBO Export">PDF</span>`
@@ -332,12 +339,14 @@ export function viewHTML(id) { window.open(`/api/invoices/${id}/html`, '_blank')
export async function exportToQBO(id) {
if (!confirm('Rechnung an QuickBooks Online senden?')) return;
if (typeof showSpinner === 'function') showSpinner('Exportiere Rechnung nach QBO...');
try {
const r = await fetch(`/api/invoices/${id}/export`, { method: 'POST' });
const d = await r.json();
if (r.ok) { alert(`✅ QBO ID: ${d.qbo_id}, Nr: ${d.qbo_doc_number}`); loadInvoices(); }
else alert(`${d.error}`);
} catch (e) { alert('Netzwerkfehler.'); }
finally { if (typeof hideSpinner === 'function') hideSpinner(); }
}
export async function resetQbo(id) {

View File

@@ -233,6 +233,7 @@ async function submitPayment() {
const submitBtn = document.getElementById('payment-submit-btn');
submitBtn.innerHTML = '⏳ Wird gesendet...';
submitBtn.disabled = true;
if (typeof showSpinner === 'function') showSpinner('Erstelle Payment in QBO...');
try {
const response = await fetch('/api/qbo/record-payment', {
@@ -264,6 +265,7 @@ async function submitPayment() {
} finally {
submitBtn.innerHTML = '💰 Record Payment in QBO';
submitBtn.disabled = false;
if (typeof hideSpinner === 'function') hideSpinner();
}
}