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;