modul umbau
This commit is contained in:
parent
9ebfd9b8c3
commit
9a9cabdec6
174
public/app.js
174
public/app.js
|
|
@ -1,5 +1,5 @@
|
||||||
// Global state
|
// Global state
|
||||||
let customers = []; // shared, updated by customer-view.js
|
let customers = window.customers || [];
|
||||||
let quotes = [];
|
let quotes = [];
|
||||||
let invoices = [];
|
let invoices = [];
|
||||||
let currentQuoteId = null;
|
let currentQuoteId = null;
|
||||||
|
|
@ -20,14 +20,14 @@ function customerSearch(type) {
|
||||||
get filteredCustomers() {
|
get filteredCustomers() {
|
||||||
// Wenn Suche leer ist: ALLE Kunden zurückgeben (kein .slice mehr!)
|
// Wenn Suche leer ist: ALLE Kunden zurückgeben (kein .slice mehr!)
|
||||||
if (!this.search) {
|
if (!this.search) {
|
||||||
return customers;
|
return (window.getCustomers ? window.getCustomers() : customers);
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchLower = this.search.toLowerCase();
|
const searchLower = this.search.toLowerCase();
|
||||||
|
|
||||||
// Filtern: Sucht im Namen, Line1, Stadt oder Account Nummer
|
// Filtern: Sucht im Namen, Line1, Stadt oder Account Nummer
|
||||||
// Auch hier: Kein .slice mehr, damit alle Ergebnisse (z.B. alle mit 'C') angezeigt werden
|
// Auch hier: Kein .slice mehr, damit alle Ergebnisse (z.B. alle mit 'C') angezeigt werden
|
||||||
return customers.filter(c =>
|
return (window.getCustomers ? window.getCustomers() : customers).filter(c =>
|
||||||
(c.name || '').toLowerCase().includes(searchLower) ||
|
(c.name || '').toLowerCase().includes(searchLower) ||
|
||||||
(c.line1 || '').toLowerCase().includes(searchLower) ||
|
(c.line1 || '').toLowerCase().includes(searchLower) ||
|
||||||
(c.city || '').toLowerCase().includes(searchLower) ||
|
(c.city || '').toLowerCase().includes(searchLower) ||
|
||||||
|
|
@ -76,9 +76,7 @@ window.customerSearch = customerSearch;
|
||||||
|
|
||||||
// Initialize app
|
// Initialize app
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
loadCustomers();
|
|
||||||
loadQuotes();
|
loadQuotes();
|
||||||
//loadInvoices();
|
|
||||||
setDefaultDate();
|
setDefaultDate();
|
||||||
checkCurrentLogo();
|
checkCurrentLogo();
|
||||||
loadLaborRate();
|
loadLaborRate();
|
||||||
|
|
@ -95,7 +93,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup form handlers
|
// Setup form handlers
|
||||||
document.getElementById('customer-form').addEventListener('submit', handleCustomerSubmit);
|
|
||||||
document.getElementById('quote-form').addEventListener('submit', handleQuoteSubmit);
|
document.getElementById('quote-form').addEventListener('submit', handleQuoteSubmit);
|
||||||
document.getElementById('invoice-form').addEventListener('submit', handleInvoiceSubmit);
|
document.getElementById('invoice-form').addEventListener('submit', handleInvoiceSubmit);
|
||||||
document.getElementById('quote-tax-exempt').addEventListener('change', updateQuoteTotals);
|
document.getElementById('quote-tax-exempt').addEventListener('change', updateQuoteTotals);
|
||||||
|
|
@ -128,7 +125,9 @@ function showTab(tabName) {
|
||||||
} else if (tabName === 'invoices') {
|
} else if (tabName === 'invoices') {
|
||||||
loadInvoices();
|
loadInvoices();
|
||||||
} else if (tabName === 'customers') {
|
} else if (tabName === 'customers') {
|
||||||
loadCustomers();
|
if (window.customerView) {
|
||||||
|
window.customerView.loadCustomers();
|
||||||
|
}
|
||||||
} else if (tabName === 'settings') {
|
} else if (tabName === 'settings') {
|
||||||
checkCurrentLogo();
|
checkCurrentLogo();
|
||||||
}
|
}
|
||||||
|
|
@ -204,167 +203,6 @@ async function uploadLogo() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// --- 2. Credits async laden ---
|
|
||||||
async function loadCustomerCredits() {
|
|
||||||
const qboCustomers = customers.filter(c => c.qbo_id);
|
|
||||||
for (const cust of qboCustomers) {
|
|
||||||
const span = document.getElementById(`customer-credit-${cust.id}`);
|
|
||||||
if (!span) continue;
|
|
||||||
try {
|
|
||||||
const res = await fetch(`/api/qbo/customer-credit/${cust.qbo_id}`);
|
|
||||||
const data = await res.json();
|
|
||||||
if (data.credit > 0) {
|
|
||||||
span.innerHTML = `<span class="inline-block px-2 py-0.5 text-xs font-semibold rounded-full bg-blue-100 text-blue-800">Credit: $${data.credit.toFixed(2)}</span>`;
|
|
||||||
} else {
|
|
||||||
span.textContent = '';
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
span.textContent = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// --- 3. Downpayment Dialog ---
|
|
||||||
async function openDownpaymentModal(customerId, customerQboId, customerName) {
|
|
||||||
// Load QBO data if needed
|
|
||||||
let bankAccounts = [];
|
|
||||||
let paymentMethods = [];
|
|
||||||
try {
|
|
||||||
const [accRes, pmRes] = await Promise.all([
|
|
||||||
fetch('/api/qbo/accounts'),
|
|
||||||
fetch('/api/qbo/payment-methods')
|
|
||||||
]);
|
|
||||||
if (accRes.ok) bankAccounts = await accRes.json();
|
|
||||||
if (pmRes.ok) paymentMethods = await pmRes.json();
|
|
||||||
} catch (e) { console.error('Error loading QBO data:', e); }
|
|
||||||
|
|
||||||
const accountOptions = bankAccounts.map(a => `<option value="${a.id}">${a.name}</option>`).join('');
|
|
||||||
const filtered = paymentMethods.filter(p => /check|ach/i.test(p.name));
|
|
||||||
const methods = filtered.length > 0 ? filtered : paymentMethods;
|
|
||||||
const methodOptions = methods.map(p => `<option value="${p.id}">${p.name}</option>`).join('');
|
|
||||||
const today = new Date().toISOString().split('T')[0];
|
|
||||||
|
|
||||||
let modal = document.getElementById('downpayment-modal');
|
|
||||||
if (!modal) {
|
|
||||||
modal = document.createElement('div');
|
|
||||||
modal.id = 'downpayment-modal';
|
|
||||||
modal.className = 'modal fixed inset-0 bg-black bg-opacity-50 z-50 justify-center items-start pt-10 overflow-y-auto';
|
|
||||||
document.body.appendChild(modal);
|
|
||||||
}
|
|
||||||
|
|
||||||
modal.innerHTML = `
|
|
||||||
<div class="bg-white rounded-lg shadow-2xl w-full max-w-lg mx-auto p-8">
|
|
||||||
<div class="flex justify-between items-center mb-4">
|
|
||||||
<h2 class="text-2xl font-bold text-gray-800">💰 Record Downpayment</h2>
|
|
||||||
<button onclick="closeDownpaymentModal()" class="text-gray-400 hover:text-gray-600">
|
|
||||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-3 mb-4">
|
|
||||||
<p class="text-sm text-blue-800">
|
|
||||||
<strong>Customer:</strong> ${customerName}<br>
|
|
||||||
This will record an unapplied payment (credit) on the customer's QBO account.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-4 mb-4">
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">Amount</label>
|
|
||||||
<input type="number" id="dp-amount" step="0.01" min="0.01" placeholder="0.00"
|
|
||||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500 text-lg font-semibold">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">Payment Date</label>
|
|
||||||
<input type="date" id="dp-date" value="${today}"
|
|
||||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">Reference #</label>
|
|
||||||
<input type="text" id="dp-reference" placeholder="Check # or ACH ref"
|
|
||||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">Payment Method</label>
|
|
||||||
<select id="dp-method" class="w-full px-3 py-2 border border-gray-300 rounded-md bg-white">${methodOptions}</select>
|
|
||||||
</div>
|
|
||||||
<div class="col-span-2">
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">Deposit To</label>
|
|
||||||
<select id="dp-deposit" class="w-full px-3 py-2 border border-gray-300 rounded-md bg-white">${accountOptions}</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex justify-end space-x-3">
|
|
||||||
<button onclick="closeDownpaymentModal()" class="px-6 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300">Cancel</button>
|
|
||||||
<button onclick="submitDownpayment(${customerId}, '${customerQboId}')" id="dp-submit-btn"
|
|
||||||
class="px-6 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 font-semibold">
|
|
||||||
💰 Record Downpayment
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>`;
|
|
||||||
|
|
||||||
modal.classList.add('active');
|
|
||||||
document.getElementById('dp-amount').focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeDownpaymentModal() {
|
|
||||||
const modal = document.getElementById('downpayment-modal');
|
|
||||||
if (modal) modal.classList.remove('active');
|
|
||||||
}
|
|
||||||
async function submitDownpayment(customerId, customerQboId) {
|
|
||||||
const amount = parseFloat(document.getElementById('dp-amount').value);
|
|
||||||
const date = document.getElementById('dp-date').value;
|
|
||||||
const ref = document.getElementById('dp-reference').value;
|
|
||||||
const methodSelect = document.getElementById('dp-method');
|
|
||||||
const depositSelect = document.getElementById('dp-deposit');
|
|
||||||
|
|
||||||
if (!amount || amount <= 0) { alert('Please enter an amount.'); return; }
|
|
||||||
if (!date || !methodSelect.value || !depositSelect.value) { alert('Please fill in all fields.'); return; }
|
|
||||||
|
|
||||||
if (!confirm(`Record downpayment of $${amount.toFixed(2)}?`)) return;
|
|
||||||
|
|
||||||
const btn = document.getElementById('dp-submit-btn');
|
|
||||||
btn.innerHTML = '⏳ Processing...';
|
|
||||||
btn.disabled = true;
|
|
||||||
if (typeof showSpinner === 'function') showSpinner('Recording downpayment in QBO...');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/qbo/record-downpayment', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
customer_id: customerId,
|
|
||||||
customer_qbo_id: customerQboId,
|
|
||||||
amount: amount,
|
|
||||||
payment_date: date,
|
|
||||||
reference_number: ref,
|
|
||||||
payment_method_id: methodSelect.value,
|
|
||||||
payment_method_name: methodSelect.options[methodSelect.selectedIndex]?.text || '',
|
|
||||||
deposit_to_account_id: depositSelect.value,
|
|
||||||
deposit_to_account_name: depositSelect.options[depositSelect.selectedIndex]?.text || ''
|
|
||||||
})
|
|
||||||
});
|
|
||||||
const result = await response.json();
|
|
||||||
if (response.ok) {
|
|
||||||
alert(`✅ ${result.message}`);
|
|
||||||
closeDownpaymentModal();
|
|
||||||
renderCustomers(); // Refresh credit display
|
|
||||||
} else {
|
|
||||||
alert(`❌ Error: ${result.error}`);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
alert('Network error.');
|
|
||||||
} finally {
|
|
||||||
btn.innerHTML = '💰 Record Downpayment';
|
|
||||||
btn.disabled = false;
|
|
||||||
if (typeof hideSpinner === 'function') hideSpinner();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Quote Management
|
// Quote Management
|
||||||
async function loadQuotes() {
|
async function loadQuotes() {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue