Files
QB-Import-from-Excel/templates/connection.html
T

963 lines
39 KiB
HTML

{% extends "base.html" %}
{% block title %}Connection - QBO Excel Sync{% endblock %}
{% block content %}
<div class="connection-page">
<div class="page-header">
<h2>QuickBooks Connection</h2>
<p>Connect to QuickBooks Online or QuickBooks Desktop to start importing data.</p>
</div>
<div class="connection-layout">
<!-- Connection Type Selector -->
<section class="card">
<div class="card-header">
<h3>Connection Type</h3>
</div>
<div class="card-body">
<div class="connection-type-selector">
<label class="connection-type-option {% if connection_type != 'qbd' %}active{% endif %}">
<input type="radio" name="connection_type" value="qbo" {% if connection_type != 'qbd' %}checked{% endif %}>
<div class="option-content">
<div class="option-icon qbo-icon">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/>
<path d="M8 12l2 2 4-4"/>
</svg>
</div>
<div class="option-text">
<strong>QuickBooks Online</strong>
<span>Cloud-based, OAuth authentication</span>
</div>
</div>
</label>
<label class="connection-type-option {% if connection_type == 'qbd' %}active{% endif %} {% if not qbd_available %}disabled{% endif %}">
<input type="radio" name="connection_type" value="qbd" {% if connection_type == 'qbd' %}checked{% endif %} {% if not qbd_available %}disabled{% endif %}>
<div class="option-content">
<div class="option-icon qbd-icon">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/>
<line x1="8" y1="21" x2="16" y2="21"/>
<line x1="12" y1="17" x2="12" y2="21"/>
</svg>
</div>
<div class="option-text">
<strong>QuickBooks Desktop</strong>
<span>{% if qbd_available %}Local installation, direct COM connection{% else %}Windows only, requires QuickBooks Desktop{% endif %}</span>
</div>
</div>
</label>
</div>
</div>
</section>
<!-- QBO Section -->
<div id="qbo-section" class="{% if connection_type == 'qbd' %}hidden{% endif %}">
<!-- Credentials Section -->
<section class="card collapsible">
<div class="card-header" onclick="toggleSection(this)">
<h3>API Credentials</h3>
<span class="collapse-icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="6 9 12 15 18 9"/>
</svg>
</span>
</div>
<div class="card-body collapsible-content">
<form id="credentials-form" class="form">
<div class="form-group">
<label for="client_id">Client ID</label>
<input type="text" id="client_id" name="client_id"
value="{{ credentials.client_id }}"
placeholder="Enter your Intuit Developer Client ID">
</div>
<div class="form-group">
<label for="client_secret">Client Secret</label>
<input type="password" id="client_secret" name="client_secret"
value="{{ credentials.client_secret }}"
placeholder="Enter your Client Secret">
</div>
<div class="form-group">
<label for="redirect_uri">Redirect URI</label>
<input type="text" id="redirect_uri" name="redirect_uri"
value="{{ credentials.redirect_uri or 'http://localhost:5000/callback' }}"
placeholder="http://localhost:5000/callback">
<small class="form-help">This must match exactly in your Intuit Developer app settings.</small>
</div>
<div class="form-group">
<label for="environment">Environment</label>
<select id="environment" name="environment">
<option value="sandbox" {% if environment == 'sandbox' %}selected{% endif %}>Sandbox (Testing)</option>
<option value="production" {% if environment == 'production' %}selected{% endif %}>Production (Live)</option>
</select>
</div>
<button type="submit" class="btn btn-secondary">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/>
<polyline points="17 21 17 13 7 13 7 21"/>
<polyline points="7 3 7 8 15 8"/>
</svg>
Save Credentials
</button>
</form>
</div>
</section>
<!-- QBO Connection Status Section -->
<section class="card">
<div class="card-header">
<h3>QuickBooks Online Status</h3>
</div>
<div class="card-body">
<div class="connection-status-display {% if is_connected and connection_type != 'qbd' %}connected{% else %}disconnected{% endif %}">
<div class="status-icon">
{% if is_connected and connection_type != 'qbd' %}
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
<polyline points="22 4 12 14.01 9 11.01"/>
</svg>
{% else %}
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/>
<line x1="15" y1="9" x2="9" y2="15"/>
<line x1="9" y1="9" x2="15" y2="15"/>
</svg>
{% endif %}
</div>
<div class="status-details">
<h4>{% if is_connected and connection_type != 'qbd' %}Connected{% else %}Not Connected{% endif %}</h4>
{% if is_connected and company_name and connection_type != 'qbd' %}
<p class="company-name">{{ company_name }}</p>
{% endif %}
</div>
</div>
<div class="connection-actions">
{% if is_connected and connection_type != 'qbd' %}
<button type="button" id="test-connection-qbo" class="btn btn-secondary">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="23 4 23 10 17 10"/>
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
</svg>
Test Connection
</button>
<button type="button" id="disconnect-qbo" class="btn btn-danger">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M18.36 6.64a9 9 0 1 1-12.73 0"/>
<line x1="12" y1="2" x2="12" y2="12"/>
</svg>
Disconnect
</button>
{% else %}
<button type="button" id="connect-oauth" class="btn btn-primary">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
</svg>
Connect to QuickBooks Online
</button>
{% endif %}
</div>
</div>
</section>
<!-- Manual Token Entry -->
<section class="card collapsible collapsed">
<div class="card-header" onclick="toggleSection(this)">
<h3>Alternative: Manual Token Entry</h3>
<div class="header-right">
<span class="card-badge">Advanced</span>
<span class="collapse-icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="6 9 12 15 18 9"/>
</svg>
</span>
</div>
</div>
<div class="card-body collapsible-content">
<p class="card-description">
If OAuth redirect isn't working, you can manually enter tokens from Intuit's OAuth 2.0 Playground.
</p>
<form id="manual-token-form" class="form">
<div class="form-group">
<label for="access_token">Access Token</label>
<textarea id="access_token" name="access_token" rows="3"
placeholder="Paste access token from OAuth Playground"></textarea>
</div>
<div class="form-group">
<label for="refresh_token">Refresh Token</label>
<textarea id="refresh_token" name="refresh_token" rows="2"
placeholder="Paste refresh token (optional but recommended)"></textarea>
</div>
<div class="form-group">
<label for="realm_id">Realm ID (Company ID)</label>
<input type="text" id="realm_id" name="realm_id"
placeholder="Enter the Realm ID / Company ID">
</div>
<button type="submit" class="btn btn-secondary">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/>
<polyline points="17 21 17 13 7 13 7 21"/>
<polyline points="7 3 7 8 15 8"/>
</svg>
Save Tokens & Connect
</button>
</form>
</div>
</section>
</div>
<!-- QBD Section -->
<div id="qbd-section" class="{% if connection_type != 'qbd' %}hidden{% endif %}">
<section class="card">
<div class="card-header">
<h3>QuickBooks Desktop Connection</h3>
</div>
<div class="card-body">
{% if not qbd_available %}
<div class="alert alert-warning">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
<line x1="12" y1="9" x2="12" y2="13"/>
<line x1="12" y1="17" x2="12.01" y2="17"/>
</svg>
<div>
<strong>QuickBooks Desktop not available</strong>
<p>QuickBooks Desktop integration requires:</p>
<ul>
<li>Windows operating system</li>
<li>QuickBooks Desktop installed and running</li>
<li>pywin32 Python package (<code>pip install pywin32</code>)</li>
</ul>
</div>
</div>
{% else %}
<div class="qbd-info">
<div class="info-item">
<span class="info-label">Status:</span>
<span class="info-value" id="qbd-status">
{% if qbd_running %}
<span class="status-badge success">QuickBooks is running</span>
{% else %}
<span class="status-badge warning">QuickBooks not detected</span>
{% endif %}
</span>
</div>
</div>
<div class="connection-status-display {% if is_connected and connection_type == 'qbd' %}connected{% else %}disconnected{% endif %}" id="qbd-connection-status">
<div class="status-icon">
{% if is_connected and connection_type == 'qbd' %}
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
<polyline points="22 4 12 14.01 9 11.01"/>
</svg>
{% else %}
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/>
<line x1="15" y1="9" x2="9" y2="15"/>
<line x1="9" y1="9" x2="15" y2="15"/>
</svg>
{% endif %}
</div>
<div class="status-details">
<h4 id="qbd-status-text">{% if is_connected and connection_type == 'qbd' %}Connected{% else %}Not Connected{% endif %}</h4>
{% if is_connected and company_name and connection_type == 'qbd' %}
<p class="company-name" id="qbd-company-name">{{ company_name }}</p>
{% else %}
<p class="company-name" id="qbd-company-name"></p>
{% endif %}
</div>
</div>
<div class="qbd-instructions">
<h4>How to connect:</h4>
<ol>
<li><strong>Open QuickBooks Desktop</strong> on this computer (run as Administrator if needed)</li>
<li><strong>Open a company file</strong> - a company must be open, not just QuickBooks</li>
<li><strong>IMPORTANT: Check Integrated Applications setting</strong>
<ul>
<li>Go to <strong>Edit → Preferences → Integrated Applications</strong></li>
<li>Click <strong>Company Preferences</strong> tab</li>
<li>Make sure <strong>"Don't allow any applications..."</strong> is <strong>UNCHECKED</strong></li>
</ul>
</li>
<li>Click <strong>"Connect to QuickBooks Desktop"</strong> below</li>
<li>When QuickBooks shows an authorization dialog, click <strong>"Yes, always allow access"</strong></li>
</ol>
<div class="qbd-troubleshooting">
<h5>Troubleshooting:</h5>
<ul>
<li><strong>Connection hangs?</strong> Check Edit → Preferences → Integrated Applications → Company Preferences → make sure "Don't allow any applications to access this company file" is UNCHECKED</li>
<li>Try switching to <strong>Single-user mode</strong> (File → Switch to Single-user Mode)</li>
<li>Check for hidden QuickBooks dialogs (Alt+Tab or check taskbar)</li>
<li>Run QuickBooks as Administrator</li>
<li>Make sure you're logged into a company file (not the "No Company Open" screen)</li>
</ul>
</div>
</div>
<div class="qbd-error-display" id="qbd-error-display" style="display: none;">
<div class="alert alert-danger">
<strong>Connection Error:</strong>
<p id="qbd-error-message"></p>
<small id="qbd-error-details"></small>
</div>
</div>
<div class="connection-actions">
{% if is_connected and connection_type == 'qbd' %}
<button type="button" id="test-connection-qbd" class="btn btn-secondary">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="23 4 23 10 17 10"/>
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
</svg>
Test Connection
</button>
<button type="button" id="disconnect-qbd" class="btn btn-danger">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M18.36 6.64a9 9 0 1 1-12.73 0"/>
<line x1="12" y1="2" x2="12" y2="12"/>
</svg>
Disconnect
</button>
{% else %}
<button type="button" id="connect-qbd" class="btn btn-primary">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/>
<line x1="8" y1="21" x2="16" y2="21"/>
<line x1="12" y1="17" x2="12" y2="21"/>
</svg>
Connect to QuickBooks Desktop
</button>
<button type="button" id="refresh-qbd-status" class="btn btn-secondary">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="23 4 23 10 17 10"/>
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
</svg>
Refresh Status
</button>
{% endif %}
</div>
{% endif %}
</div>
</section>
</div>
</div>
</div>
<style>
/* Connection Type Selector */
.connection-type-selector {
display: flex;
gap: 16px;
margin-bottom: 8px;
}
.connection-type-option {
flex: 1;
cursor: pointer;
border: 2px solid #e2e8f0;
border-radius: 12px;
padding: 20px;
transition: all 0.2s ease;
background: #fff;
}
.connection-type-option:hover:not(.disabled) {
border-color: #94a3b8;
}
.connection-type-option.active {
border-color: #2563eb;
background: #eff6ff;
}
.connection-type-option.disabled {
opacity: 0.6;
cursor: not-allowed;
background: #f8fafc;
}
.connection-type-option input[type="radio"] {
display: none;
}
.option-content {
display: flex;
align-items: center;
gap: 16px;
}
.option-icon {
width: 56px;
height: 56px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.option-icon.qbo-icon {
background: linear-gradient(135deg, #2CA01C 0%, #1a8a0e 100%);
color: white;
}
.option-icon.qbd-icon {
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
color: white;
}
.option-text {
display: flex;
flex-direction: column;
gap: 4px;
}
.option-text strong {
font-size: 16px;
color: #1e293b;
}
.option-text span {
font-size: 13px;
color: #64748b;
}
/* QBD specific styles */
.qbd-info {
background: #f8fafc;
padding: 16px;
border-radius: 8px;
margin-bottom: 20px;
}
.info-item {
display: flex;
align-items: center;
gap: 12px;
}
.info-label {
font-weight: 500;
color: #64748b;
}
.status-badge {
display: inline-flex;
align-items: center;
padding: 4px 12px;
border-radius: 20px;
font-size: 13px;
font-weight: 500;
}
.status-badge.success {
background: #dcfce7;
color: #166534;
}
.status-badge.warning {
background: #fef3c7;
color: #92400e;
}
.qbd-instructions {
background: #f0f9ff;
border: 1px solid #bae6fd;
border-radius: 8px;
padding: 16px;
margin: 20px 0;
}
.qbd-instructions h4 {
margin: 0 0 12px 0;
color: #0369a1;
font-size: 14px;
}
.qbd-instructions ol {
margin: 0;
padding-left: 20px;
color: #0c4a6e;
}
.qbd-instructions li {
margin-bottom: 8px;
font-size: 14px;
}
.qbd-instructions li:last-child {
margin-bottom: 0;
}
.alert {
display: flex;
gap: 12px;
padding: 16px;
border-radius: 8px;
margin-bottom: 20px;
}
.alert-warning {
background: #fef3c7;
border: 1px solid #fcd34d;
color: #92400e;
}
.alert svg {
flex-shrink: 0;
margin-top: 2px;
}
.alert strong {
display: block;
margin-bottom: 8px;
}
.alert p {
margin: 0 0 8px 0;
font-size: 14px;
}
.alert ul {
margin: 0;
padding-left: 20px;
}
.alert li {
font-size: 13px;
margin-bottom: 4px;
}
.alert code {
background: rgba(0,0,0,0.1);
padding: 2px 6px;
border-radius: 4px;
font-size: 12px;
}
.alert-danger {
background: #fef2f2;
border: 1px solid #fecaca;
color: #991b1b;
}
.alert-danger strong {
color: #7f1d1d;
}
.alert-danger p {
margin: 8px 0;
}
.alert-danger small {
display: block;
margin-top: 8px;
color: #b91c1c;
font-family: monospace;
font-size: 11px;
word-break: break-all;
}
.qbd-troubleshooting {
margin-top: 16px;
padding-top: 16px;
border-top: 1px dashed #bae6fd;
}
.qbd-troubleshooting h5 {
margin: 0 0 8px 0;
color: #0369a1;
font-size: 13px;
}
.qbd-troubleshooting ul {
margin: 0;
padding-left: 20px;
color: #0c4a6e;
}
.qbd-troubleshooting li {
font-size: 13px;
margin-bottom: 6px;
}
.qbd-error-display {
margin: 16px 0;
}
.hidden {
display: none !important;
}
@media (max-width: 768px) {
.connection-type-selector {
flex-direction: column;
}
}
</style>
{% endblock %}
{% block extra_js %}
<script>
let oauthPollTimer = null;
// Toggle collapsible sections
function toggleSection(header) {
const card = header.closest('.card');
const content = card.querySelector('.collapsible-content');
card.classList.toggle('collapsed');
}
document.addEventListener('DOMContentLoaded', function() {
// Connection type switching
document.querySelectorAll('input[name="connection_type"]').forEach(radio => {
radio.addEventListener('change', async function() {
const type = this.value;
// Update UI
document.querySelectorAll('.connection-type-option').forEach(opt => {
opt.classList.remove('active');
});
this.closest('.connection-type-option').classList.add('active');
// Show/hide sections
document.getElementById('qbo-section').classList.toggle('hidden', type === 'qbd');
document.getElementById('qbd-section').classList.toggle('hidden', type !== 'qbd');
// Notify server
try {
await fetch('/api/connection/switch', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: type })
});
} catch (error) {
console.error('Error switching connection type:', error);
}
});
});
// Save credentials form
document.getElementById('credentials-form').addEventListener('submit', async function(e) {
e.preventDefault();
const formData = {
client_id: document.getElementById('client_id').value,
client_secret: document.getElementById('client_secret').value,
redirect_uri: document.getElementById('redirect_uri').value,
environment: document.getElementById('environment').value
};
try {
const response = await fetch('/api/credentials', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});
const result = await response.json();
if (result.success) {
showToast('Credentials saved successfully', 'success');
} else {
showToast(result.error || 'Failed to save credentials', 'error');
}
} catch (error) {
showToast('Error saving credentials: ' + error.message, 'error');
}
});
// Connect via OAuth (QBO)
const connectBtn = document.getElementById('connect-oauth');
if (connectBtn) {
connectBtn.addEventListener('click', async function() {
try {
const response = await fetch('/api/oauth/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
});
const result = await response.json();
if (result.success && result.auth_url) {
localStorage.removeItem('oauth_complete');
const oauthPopup = window.open(result.auth_url, 'QBO_OAuth', 'width=600,height=700');
startOAuthPolling(oauthPopup);
} else {
showToast(result.error || 'Failed to start OAuth flow', 'error');
}
} catch (error) {
showToast('Error starting OAuth: ' + error.message, 'error');
}
});
}
// Test connection QBO
const testBtnQbo = document.getElementById('test-connection-qbo');
if (testBtnQbo) {
testBtnQbo.addEventListener('click', async function() {
try {
const response = await fetch('/api/connection/test');
const result = await response.json();
if (result.success) {
showToast(`Connected to ${result.company_name}. Found ${result.customers_count} customers, ${result.vendors_count} vendors, ${result.accounts_count} accounts.`, 'success');
} else {
showToast(result.error || 'Connection test failed', 'error');
}
} catch (error) {
showToast('Error testing connection: ' + error.message, 'error');
}
});
}
// Disconnect QBO
const disconnectBtnQbo = document.getElementById('disconnect-qbo');
if (disconnectBtnQbo) {
disconnectBtnQbo.addEventListener('click', async function() {
if (!confirm('Are you sure you want to disconnect from QuickBooks Online?')) return;
try {
const response = await fetch('/api/disconnect', { method: 'POST' });
const result = await response.json();
if (result.success) {
showToast('Disconnected successfully', 'success');
setTimeout(() => location.reload(), 1000);
} else {
showToast(result.error || 'Failed to disconnect', 'error');
}
} catch (error) {
showToast('Error disconnecting: ' + error.message, 'error');
}
});
}
// Connect to QBD
const connectBtnQbd = document.getElementById('connect-qbd');
if (connectBtnQbd) {
connectBtnQbd.addEventListener('click', async function() {
this.disabled = true;
this.innerHTML = '<span class="spinner"></span> Connecting... (Check QuickBooks for authorization dialog)';
// Show a helpful message
showToast('Connecting... Please check QuickBooks Desktop for an authorization dialog and click "Yes, always allow access"', 'info', 10000);
// Hide previous error
const errorDisplay = document.getElementById('qbd-error-display');
if (errorDisplay) errorDisplay.style.display = 'none';
try {
const response = await fetch('/api/qbd/connect', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({})
});
const result = await response.json();
console.log('QBD Connect result:', result);
if (result.success) {
showToast(`Connected to ${result.company_name}`, 'success');
setTimeout(() => location.reload(), 1000);
} else {
// Show detailed error
const errorMsg = result.error || 'Failed to connect to QuickBooks Desktop';
const errorDetails = result.details || result.code || '';
showToast(errorMsg, 'error');
// Show in error display area
if (errorDisplay) {
document.getElementById('qbd-error-message').textContent = errorMsg;
document.getElementById('qbd-error-details').textContent = errorDetails;
errorDisplay.style.display = 'block';
}
// Log full response for debugging
console.error('QBD connection failed:', result);
if (result.traceback) {
console.error('Traceback:', result.traceback);
}
this.disabled = false;
this.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg> Connect to QuickBooks Desktop';
}
} catch (error) {
console.error('QBD connection error:', error);
showToast('Error connecting: ' + error.message, 'error');
this.disabled = false;
this.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg> Connect to QuickBooks Desktop';
}
});
}
// Test connection QBD
const testBtnQbd = document.getElementById('test-connection-qbd');
if (testBtnQbd) {
testBtnQbd.addEventListener('click', async function() {
try {
const response = await fetch('/api/qbd/test');
const result = await response.json();
if (result.success) {
showToast(`Connected to ${result.company_name}. Found ${result.customers_count} customers, ${result.vendors_count} vendors, ${result.accounts_count} accounts.`, 'success');
} else {
showToast(result.error || 'Connection test failed', 'error');
}
} catch (error) {
showToast('Error testing connection: ' + error.message, 'error');
}
});
}
// Disconnect QBD
const disconnectBtnQbd = document.getElementById('disconnect-qbd');
if (disconnectBtnQbd) {
disconnectBtnQbd.addEventListener('click', async function() {
if (!confirm('Are you sure you want to disconnect from QuickBooks Desktop?')) return;
try {
const response = await fetch('/api/qbd/disconnect', { method: 'POST' });
const result = await response.json();
if (result.success) {
showToast('Disconnected successfully', 'success');
setTimeout(() => location.reload(), 1000);
} else {
showToast(result.error || 'Failed to disconnect', 'error');
}
} catch (error) {
showToast('Error disconnecting: ' + error.message, 'error');
}
});
}
// Refresh QBD status
const refreshBtnQbd = document.getElementById('refresh-qbd-status');
if (refreshBtnQbd) {
refreshBtnQbd.addEventListener('click', async function() {
try {
const response = await fetch('/api/qbd/status');
const result = await response.json();
if (result.success) {
const statusEl = document.querySelector('#qbd-status .status-badge');
const connectBtn = document.getElementById('connect-qbd');
if (result.qb_running) {
statusEl.className = 'status-badge success';
statusEl.textContent = 'QuickBooks is running';
if (connectBtn) connectBtn.disabled = false;
} else {
statusEl.className = 'status-badge warning';
statusEl.textContent = 'QuickBooks not detected';
if (connectBtn) connectBtn.disabled = true;
}
showToast('Status refreshed', 'info');
}
} catch (error) {
showToast('Error refreshing status: ' + error.message, 'error');
}
});
}
// Manual token form
document.getElementById('manual-token-form').addEventListener('submit', async function(e) {
e.preventDefault();
const formData = {
access_token: document.getElementById('access_token').value,
refresh_token: document.getElementById('refresh_token').value,
realm_id: document.getElementById('realm_id').value
};
if (!formData.access_token || !formData.realm_id) {
showToast('Please enter at least the Access Token and Realm ID', 'warning');
return;
}
try {
const response = await fetch('/api/oauth/manual', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});
const result = await response.json();
if (result.success) {
showToast(`Connected to ${result.company_name}`, 'success');
setTimeout(() => location.reload(), 1000);
} else {
showToast(result.error || 'Failed to connect with tokens', 'error');
}
} catch (error) {
showToast('Error connecting: ' + error.message, 'error');
}
});
});
// Function to poll for OAuth completion
function startOAuthPolling(popup) {
if (oauthPollTimer) {
clearInterval(oauthPollTimer);
}
let checkCount = 0;
const maxChecks = 300;
oauthPollTimer = setInterval(async function() {
checkCount++;
const oauthComplete = localStorage.getItem('oauth_complete');
if (oauthComplete) {
clearInterval(oauthPollTimer);
localStorage.removeItem('oauth_complete');
showToast('Successfully connected to QuickBooks!', 'success');
location.reload();
return;
}
if (popup && popup.closed) {
clearInterval(oauthPollTimer);
setTimeout(async function() {
try {
const response = await fetch('/api/connection/test');
const result = await response.json();
if (result.connected === true) {
showToast('Successfully connected to QuickBooks!', 'success');
location.reload();
} else {
showToast('Popup closed. Click "Connect to QuickBooks" to try again.', 'warning');
}
} catch (err) {
showToast('Popup closed. Click "Connect to QuickBooks" to try again.', 'warning');
}
}, 1500);
return;
}
if (checkCount >= maxChecks) {
clearInterval(oauthPollTimer);
showToast('OAuth timeout. Please try again.', 'warning');
}
}, 1000);
}
</script>
{% endblock %}