update
This commit is contained in:
parent
272f325d98
commit
52dcdce8bb
|
|
@ -82,6 +82,18 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
setDefaultDate();
|
setDefaultDate();
|
||||||
checkCurrentLogo();
|
checkCurrentLogo();
|
||||||
|
|
||||||
|
// *** FIX 3: Gespeicherten Tab wiederherstellen (oder 'quotes' als Default) ***
|
||||||
|
const savedTab = localStorage.getItem('activeTab') || 'quotes';
|
||||||
|
showTab(savedTab);
|
||||||
|
|
||||||
|
// Hash-basierte Tab-Navigation (z.B. nach OAuth Redirect /#settings)
|
||||||
|
if (window.location.hash) {
|
||||||
|
const hashTab = window.location.hash.replace('#', '');
|
||||||
|
if (['quotes', 'invoices', 'customers', 'settings'].includes(hashTab)) {
|
||||||
|
showTab(hashTab);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Setup form handlers
|
// Setup form handlers
|
||||||
document.getElementById('customer-form').addEventListener('submit', handleCustomerSubmit);
|
document.getElementById('customer-form').addEventListener('submit', handleCustomerSubmit);
|
||||||
document.getElementById('quote-form').addEventListener('submit', handleQuoteSubmit);
|
document.getElementById('quote-form').addEventListener('submit', handleQuoteSubmit);
|
||||||
|
|
@ -108,6 +120,9 @@ function showTab(tabName) {
|
||||||
document.getElementById(`${tabName}-tab`).classList.remove('hidden');
|
document.getElementById(`${tabName}-tab`).classList.remove('hidden');
|
||||||
document.getElementById(`tab-${tabName}`).classList.add('bg-blue-800');
|
document.getElementById(`tab-${tabName}`).classList.add('bg-blue-800');
|
||||||
|
|
||||||
|
// *** FIX 3: Tab-Auswahl persistieren ***
|
||||||
|
localStorage.setItem('activeTab', tabName);
|
||||||
|
|
||||||
if (tabName === 'quotes') {
|
if (tabName === 'quotes') {
|
||||||
loadQuotes();
|
loadQuotes();
|
||||||
} else if (tabName === 'invoices') {
|
} else if (tabName === 'invoices') {
|
||||||
|
|
@ -1113,6 +1128,7 @@ function viewInvoicePDF(id) {
|
||||||
window.open(`/api/invoices/${id}/pdf`, '_blank');
|
window.open(`/api/invoices/${id}/pdf`, '_blank');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// *** FIX 2: Verbesserte Erfolgsmeldung mit QBO DocNumber ***
|
||||||
async function exportToQBO(id) {
|
async function exportToQBO(id) {
|
||||||
if (!confirm('Rechnung wirklich an QuickBooks Online senden?')) return;
|
if (!confirm('Rechnung wirklich an QuickBooks Online senden?')) return;
|
||||||
|
|
||||||
|
|
@ -1127,8 +1143,8 @@ async function exportToQBO(id) {
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
alert(`✅ Erfolg! QBO ID: ${result.qbo_id}`);
|
alert(`✅ Erfolg! QBO ID: ${result.qbo_id}, Rechnungsnr: ${result.qbo_doc_number}`);
|
||||||
// Optional: Liste neu laden um Status zu aktualisieren
|
// Liste neu laden um aktualisierte invoice_number anzuzeigen
|
||||||
loadInvoices();
|
loadInvoices();
|
||||||
} else {
|
} else {
|
||||||
alert(`❌ Fehler: ${result.error}`);
|
alert(`❌ Fehler: ${result.error}`);
|
||||||
|
|
|
||||||
|
|
@ -149,6 +149,37 @@
|
||||||
|
|
||||||
<hr class="my-8 border-gray-200">
|
<hr class="my-8 border-gray-200">
|
||||||
|
|
||||||
|
<h3 class="text-xl font-semibold mb-4 text-gray-800">QuickBooks Online Authorization</h3>
|
||||||
|
<p class="text-gray-600 mb-4">
|
||||||
|
Wenn der Token abgelaufen ist oder die Verbindung fehlschlägt,
|
||||||
|
hier neu autorisieren. Du wirst zu Intuit weitergeleitet.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="flex items-center space-x-4 mb-4">
|
||||||
|
<a href="/auth/qbo"
|
||||||
|
class="bg-green-600 hover:bg-green-700 text-white px-6 py-2 rounded-lg font-semibold shadow-md inline-flex items-center">
|
||||||
|
🔑 Authorize QBO
|
||||||
|
</a>
|
||||||
|
<span id="qbo-status" class="text-sm text-gray-500">Checking...</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// QBO Status beim Laden prüfen
|
||||||
|
fetch('/api/qbo/status')
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
const el = document.getElementById('qbo-status');
|
||||||
|
if (data.connected) {
|
||||||
|
el.innerHTML = '<span class="text-green-600">✅ Connected (Realm: ' + data.realmId + ')</span>';
|
||||||
|
} else {
|
||||||
|
el.innerHTML = '<span class="text-red-600">❌ Not connected — please authorize</span>';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
document.getElementById('qbo-status').innerHTML = '<span class="text-gray-400">Status unknown</span>';
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<h3 class="text-xl font-semibold mb-4 text-gray-800">QuickBooks Online Connection Test</h3>
|
<h3 class="text-xl font-semibold mb-4 text-gray-800">QuickBooks Online Connection Test</h3>
|
||||||
<p class="text-gray-600 mb-4">Test the connection and token refresh logic by fetching a report of overdue invoices (> 30 days) directly from QBO.</p>
|
<p class="text-gray-600 mb-4">Test the connection and token refresh logic by fetching a report of overdue invoices (> 30 days) directly from QBO.</p>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
// qbo_helper.js - FIX FÜR ERROR 3200
|
// qbo_helper.js - MIT OAUTH FLOW & VERBESSERTEM TOKEN HANDLING
|
||||||
require('dotenv').config();
|
require('dotenv').config();
|
||||||
const OAuthClient = require('intuit-oauth');
|
const OAuthClient = require('intuit-oauth');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
|
@ -31,7 +31,7 @@ const getOAuthClient = () => {
|
||||||
console.error("❌ Fehler beim Laden des gespeicherten Tokens:", e.message);
|
console.error("❌ Fehler beim Laden des gespeicherten Tokens:", e.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (savedToken) {
|
if (savedToken && savedToken.refresh_token) {
|
||||||
oauthClient.setToken(savedToken);
|
oauthClient.setToken(savedToken);
|
||||||
console.log("✅ Gespeicherter Token aus qbo_token.json geladen.");
|
console.log("✅ Gespeicherter Token aus qbo_token.json geladen.");
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -40,13 +40,25 @@ const getOAuthClient = () => {
|
||||||
refresh_token: process.env.QBO_REFRESH_TOKEN || '',
|
refresh_token: process.env.QBO_REFRESH_TOKEN || '',
|
||||||
realmId: process.env.QBO_REALM_ID
|
realmId: process.env.QBO_REALM_ID
|
||||||
};
|
};
|
||||||
|
if (envToken.refresh_token) {
|
||||||
oauthClient.setToken(envToken);
|
oauthClient.setToken(envToken);
|
||||||
console.log("ℹ️ Token aus .env geladen (Fallback).");
|
console.log("ℹ️ Token aus .env geladen (Fallback).");
|
||||||
|
} else {
|
||||||
|
console.warn("⚠️ Kein gültiger Token vorhanden. Bitte unter Settings → QBO autorisieren.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return oauthClient;
|
return oauthClient;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setzt den oauthClient zurück, damit beim nächsten getOAuthClient()
|
||||||
|
* der Token frisch aus der Datei geladen wird.
|
||||||
|
*/
|
||||||
|
function resetOAuthClient() {
|
||||||
|
oauthClient = null;
|
||||||
|
}
|
||||||
|
|
||||||
function saveTokens() {
|
function saveTokens() {
|
||||||
try {
|
try {
|
||||||
const token = getOAuthClient().getToken();
|
const token = getOAuthClient().getToken();
|
||||||
|
|
@ -60,6 +72,12 @@ function saveTokens() {
|
||||||
async function makeQboApiCall(requestOptions) {
|
async function makeQboApiCall(requestOptions) {
|
||||||
const client = getOAuthClient();
|
const client = getOAuthClient();
|
||||||
|
|
||||||
|
// Prüfen ob überhaupt ein Refresh Token vorhanden ist
|
||||||
|
const currentToken = client.getToken();
|
||||||
|
if (!currentToken || !currentToken.refresh_token) {
|
||||||
|
throw new Error("Kein gültiger QBO Token vorhanden. Bitte unter Settings → 'Authorize QBO' klicken.");
|
||||||
|
}
|
||||||
|
|
||||||
const doRefresh = async () => {
|
const doRefresh = async () => {
|
||||||
console.log("🔄 QBO Token Refresh wird ausgeführt...");
|
console.log("🔄 QBO Token Refresh wird ausgeführt...");
|
||||||
try {
|
try {
|
||||||
|
|
@ -68,7 +86,15 @@ async function makeQboApiCall(requestOptions) {
|
||||||
saveTokens();
|
saveTokens();
|
||||||
return authResponse;
|
return authResponse;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("❌ Refresh fehlgeschlagen:", e.originalMessage || e);
|
const errMsg = e.originalMessage || e.message || String(e);
|
||||||
|
console.error("❌ Refresh fehlgeschlagen:", errMsg);
|
||||||
|
|
||||||
|
// Wenn der Refresh Token komplett ungültig ist → klare Meldung
|
||||||
|
if (errMsg.includes('invalid') || errMsg.includes('Authorize again')) {
|
||||||
|
throw new Error(
|
||||||
|
"Der Refresh Token ist abgelaufen. Bitte unter Settings → 'Authorize QBO' neu autorisieren."
|
||||||
|
);
|
||||||
|
}
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -82,7 +108,7 @@ async function makeQboApiCall(requestOptions) {
|
||||||
if (data.fault && data.fault.error) {
|
if (data.fault && data.fault.error) {
|
||||||
const errorCode = data.fault.error[0].code;
|
const errorCode = data.fault.error[0].code;
|
||||||
|
|
||||||
// --- FIX: 3200 (Auth Failed) HINZUGEFÜGT ---
|
// 3200 (Auth Failed), 3202, 3100 → Refresh versuchen
|
||||||
if (errorCode === '3200' || errorCode === '3202' || errorCode === '3100') {
|
if (errorCode === '3200' || errorCode === '3202' || errorCode === '3100') {
|
||||||
console.log(`⚠️ QBO meldet Token-Fehler (${errorCode}). Versuche Refresh und Retry...`);
|
console.log(`⚠️ QBO meldet Token-Fehler (${errorCode}). Versuche Refresh und Retry...`);
|
||||||
await doRefresh();
|
await doRefresh();
|
||||||
|
|
@ -114,5 +140,7 @@ async function makeQboApiCall(requestOptions) {
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getOAuthClient,
|
getOAuthClient,
|
||||||
makeQboApiCall
|
makeQboApiCall,
|
||||||
|
saveTokens,
|
||||||
|
resetOAuthClient
|
||||||
};
|
};
|
||||||
53
server.js
53
server.js
|
|
@ -4,7 +4,8 @@ const path = require('path');
|
||||||
const puppeteer = require('puppeteer');
|
const puppeteer = require('puppeteer');
|
||||||
const fs = require('fs').promises;
|
const fs = require('fs').promises;
|
||||||
const multer = require('multer');
|
const multer = require('multer');
|
||||||
const { makeQboApiCall, getOAuthClient } = require('./qbo_helper');
|
const OAuthClient = require('intuit-oauth');
|
||||||
|
const { makeQboApiCall, getOAuthClient, saveTokens, resetOAuthClient } = require('./qbo_helper');
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = process.env.PORT || 3000;
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|
@ -1253,7 +1254,7 @@ app.post('/api/invoices/:id/export', async (req, res) => {
|
||||||
|
|
||||||
// 6. DB Update: Wir speichern AUCH die QBO-Nummer, damit wir wissen, wie sie drüben heißt
|
// 6. DB Update: Wir speichern AUCH die QBO-Nummer, damit wir wissen, wie sie drüben heißt
|
||||||
await client.query(
|
await client.query(
|
||||||
`UPDATE invoices SET qbo_id = $1, qbo_sync_token = $2, qbo_doc_number = $3 WHERE id = $4`,
|
`UPDATE invoices SET qbo_id = $1, qbo_sync_token = $2, qbo_doc_number = $3, invoice_number = $3 WHERE id = $4`,
|
||||||
[qboInvoice.Id, qboInvoice.SyncToken, qboInvoice.DocNumber, id]
|
[qboInvoice.Id, qboInvoice.SyncToken, qboInvoice.DocNumber, id]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -1307,6 +1308,54 @@ app.get('/api/qbo/overdue', async (req, res) => {
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
// Schritt 1: User klickt "Authorize" → Redirect zu Intuit
|
||||||
|
app.get('/auth/qbo', (req, res) => {
|
||||||
|
const client = getOAuthClient();
|
||||||
|
const authUri = client.authorizeUri({
|
||||||
|
scope: [OAuthClient.scopes.Accounting],
|
||||||
|
state: 'intuit-qbo-auth'
|
||||||
|
});
|
||||||
|
console.log('🔗 Redirecting to QBO Authorization:', authUri);
|
||||||
|
res.redirect(authUri);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Schritt 2: Intuit redirected zurück mit Code → Token holen
|
||||||
|
app.get('/auth/qbo/callback', async (req, res) => {
|
||||||
|
const client = getOAuthClient();
|
||||||
|
try {
|
||||||
|
const authResponse = await client.createToken(req.url);
|
||||||
|
console.log('✅ QBO Authorization erfolgreich!');
|
||||||
|
saveTokens();
|
||||||
|
|
||||||
|
// Redirect zurück zur App (Settings Tab)
|
||||||
|
res.redirect('/#settings');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('❌ QBO Authorization fehlgeschlagen:', e);
|
||||||
|
res.status(500).send(`
|
||||||
|
<h2>QBO Authorization Failed</h2>
|
||||||
|
<p>${e.message || e}</p>
|
||||||
|
<a href="/">Zurück zur App</a>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Status-Check Endpoint (für die UI)
|
||||||
|
app.get('/api/qbo/status', (req, res) => {
|
||||||
|
try {
|
||||||
|
const client = getOAuthClient();
|
||||||
|
const token = client.getToken();
|
||||||
|
const hasToken = !!(token && token.refresh_token);
|
||||||
|
res.json({
|
||||||
|
connected: hasToken,
|
||||||
|
realmId: token?.realmId || null
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
res.json({ connected: false });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Start server and browser
|
// Start server and browser
|
||||||
async function startServer() {
|
async function startServer() {
|
||||||
await initBrowser();
|
await initBrowser();
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
// =====================================================
|
||||||
|
// set_qbo_token.js
|
||||||
|
//
|
||||||
|
// Einmalig ausführen um qbo_token.json korrekt zu setzen.
|
||||||
|
// Die intuit-oauth Library braucht ein vollständiges Token-Objekt,
|
||||||
|
// nicht nur access_token + refresh_token.
|
||||||
|
//
|
||||||
|
// Verwendung:
|
||||||
|
// node set_qbo_token.js <ACCESS_TOKEN> <REFRESH_TOKEN> <REALM_ID>
|
||||||
|
//
|
||||||
|
// Beispiel:
|
||||||
|
// node set_qbo_token.js "eyJlbmMi..." "AB11..." "9341..."
|
||||||
|
// =====================================================
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const accessToken = process.argv[2];
|
||||||
|
const refreshToken = process.argv[3];
|
||||||
|
const realmId = process.argv[4];
|
||||||
|
|
||||||
|
if (!accessToken || !refreshToken || !realmId) {
|
||||||
|
console.log('');
|
||||||
|
console.log('Verwendung:');
|
||||||
|
console.log(' node set_qbo_token.js <ACCESS_TOKEN> <REFRESH_TOKEN> <REALM_ID>');
|
||||||
|
console.log('');
|
||||||
|
console.log('Die Werte bekommst du aus dem Intuit OAuth Playground:');
|
||||||
|
console.log(' https://developer.intuit.com/app/developer/playground');
|
||||||
|
console.log('');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Das ist das Format, das die intuit-oauth Library erwartet
|
||||||
|
const tokenObject = {
|
||||||
|
token_type: "bearer",
|
||||||
|
access_token: accessToken,
|
||||||
|
refresh_token: refreshToken,
|
||||||
|
expires_in: 3600,
|
||||||
|
x_refresh_token_expires_in: 8726400,
|
||||||
|
realmId: realmId,
|
||||||
|
// createdAt wird von der Library geprüft um zu sehen ob der Token abgelaufen ist
|
||||||
|
createdAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
const tokenFile = path.join(__dirname, 'qbo_token.json');
|
||||||
|
fs.writeFileSync(tokenFile, JSON.stringify(tokenObject, null, 2));
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
console.log('✅ qbo_token.json erfolgreich erstellt!');
|
||||||
|
console.log(` 📁 ${tokenFile}`);
|
||||||
|
console.log(` 🔑 Access Token: ${accessToken.substring(0, 20)}...`);
|
||||||
|
console.log(` 🔄 Refresh Token: ${refreshToken.substring(0, 15)}...`);
|
||||||
|
console.log(` 🏢 Realm ID: ${realmId}`);
|
||||||
|
console.log('');
|
||||||
|
console.log('Nächste Schritte:');
|
||||||
|
console.log(' 1. Docker Container neu starten: docker compose restart quote_app');
|
||||||
|
console.log(' 2. In Settings → "Test Connection" klicken');
|
||||||
|
console.log('');
|
||||||
Loading…
Reference in New Issue