update
This commit is contained in:
127
public/app.js
127
public/app.js
@@ -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;
|
||||
Reference in New Issue
Block a user