Files
QB-Import-from-Excel/templates/connection.html
T
2026-02-13 10:38:45 -05:00

508 lines
21 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 your QuickBooks Online account to start importing data.</p>
</div>
<div class="connection-layout">
<!-- 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>
<!-- Connection Status Section -->
<section class="card collapsible">
<div class="card-header" onclick="toggleSection(this)">
<h3>Connection Status</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">
<div class="connection-status-display {% if is_connected %}connected{% else %}disconnected{% endif %}">
<div class="status-icon">
{% if is_connected %}
<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 %}Connected{% else %}Not Connected{% endif %}</h4>
{% if is_connected and company_name %}
<p class="company-name">{{ company_name }}</p>
{% endif %}
</div>
</div>
<div class="connection-actions">
{% if is_connected %}
<button type="button" id="test-connection" 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" 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
</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>
<!-- Help Section -->
<section class="card help-card collapsible collapsed">
<div class="card-header" onclick="toggleSection(this)">
<h3>Setup Instructions</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">
<div class="help-section">
<h4>Option 1: OAuth Flow (Recommended)</h4>
<ol>
<li>Go to <a href="https://developer.intuit.com" target="_blank">developer.intuit.com</a></li>
<li>Create or select your app → "Keys & OAuth" section</li>
<li>Add redirect URI: <code>http://localhost:5000/callback</code></li>
<li>Copy your Client ID and Client Secret</li>
<li>Save credentials above, then click "Connect to QuickBooks"</li>
</ol>
</div>
<div class="help-section">
<h4>Option 2: Manual Token Entry</h4>
<ol>
<li>In the developer portal, click "OAuth 2.0 Playground"</li>
<li>Authorize your sandbox/production company</li>
<li>Copy the Access Token, Refresh Token, and Realm ID</li>
<li>Paste them in the "Manual Token Entry" section above</li>
</ol>
</div>
<div class="help-note">
<strong>Note:</strong> Access tokens expire after 1 hour. The app will automatically refresh using the refresh token.
</div>
</div>
</section>
</div>
</div>
<style>
/* Collapsible Sections */
.card.collapsible .card-header {
cursor: pointer;
user-select: none;
display: flex;
justify-content: space-between;
align-items: center;
}
.card.collapsible .card-header:hover {
background: var(--bg);
}
.header-right {
display: flex;
align-items: center;
gap: 12px;
}
.collapse-icon {
transition: transform 0.3s ease;
display: flex;
align-items: center;
color: var(--text-muted);
}
.card.collapsible.collapsed .collapse-icon {
transform: rotate(-90deg);
}
.collapsible-content {
max-height: 2000px;
overflow: hidden;
transition: max-height 0.3s ease, padding 0.3s ease, opacity 0.3s ease;
opacity: 1;
}
.card.collapsible.collapsed .collapsible-content {
max-height: 0;
padding-top: 0;
padding-bottom: 0;
opacity: 0;
}
/* Ensure card-body padding transitions smoothly */
.card.collapsible .card-body {
transition: padding 0.3s ease;
}
.card.collapsible.collapsed .card-body {
padding: 0 20px;
}
</style>
{% endblock %}
{% block extra_js %}
<script>
// Toggle collapsible section
function toggleSection(header) {
const card = header.closest('.card.collapsible');
card.classList.toggle('collapsed');
// Save state to localStorage
const sectionId = header.querySelector('h3').textContent.trim();
const isCollapsed = card.classList.contains('collapsed');
localStorage.setItem('section_' + sectionId, isCollapsed ? 'collapsed' : 'expanded');
}
// Global variable to track OAuth polling
let oauthPollTimer = null;
document.addEventListener('DOMContentLoaded', function() {
// Check if we need to handle OAuth completion on page load
const oauthComplete = localStorage.getItem('oauth_complete');
if (oauthComplete) {
localStorage.removeItem('oauth_complete');
// Page was just reloaded after OAuth - show success message
showToast('Successfully connected to QuickBooks!', 'success');
}
// Restore section states from localStorage
document.querySelectorAll('.card.collapsible').forEach(card => {
const sectionId = card.querySelector('.card-header h3').textContent.trim();
const savedState = localStorage.getItem('section_' + sectionId);
if (savedState === 'collapsed') {
card.classList.add('collapsed');
} else if (savedState === 'expanded') {
card.classList.remove('collapsed');
}
});
// 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
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) {
// Clear any previous OAuth flag
localStorage.removeItem('oauth_complete');
// Open OAuth in new window
const oauthPopup = window.open(result.auth_url, 'QBO_OAuth', 'width=600,height=700');
// Start polling for OAuth completion
startOAuthPolling(oauthPopup);
} else {
showToast(result.error || 'Failed to start OAuth flow', 'error');
}
} catch (error) {
showToast('Error starting OAuth: ' + error.message, 'error');
}
});
}
// Test connection
const testBtn = document.getElementById('test-connection');
if (testBtn) {
testBtn.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
const disconnectBtn = document.getElementById('disconnect');
if (disconnectBtn) {
disconnectBtn.addEventListener('click', async function() {
if (!confirm('Are you sure you want to disconnect from QuickBooks?')) {
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');
}
});
}
// 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) {
// Clear any existing timer
if (oauthPollTimer) {
clearInterval(oauthPollTimer);
}
let checkCount = 0;
const maxChecks = 300; // 5 minutes max (300 * 1000ms)
oauthPollTimer = setInterval(async function() {
checkCount++;
// Check localStorage for OAuth completion signal
const oauthComplete = localStorage.getItem('oauth_complete');
if (oauthComplete) {
clearInterval(oauthPollTimer);
localStorage.removeItem('oauth_complete');
showToast('Successfully connected to QuickBooks!', 'success');
location.reload();
return;
}
// Check if popup is closed
if (popup && popup.closed) {
clearInterval(oauthPollTimer);
// Wait a moment then check connection status
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;
}
// Stop polling after max time
if (checkCount >= maxChecks) {
clearInterval(oauthPollTimer);
showToast('OAuth timeout. Please try again.', 'warning');
}
}, 1000);
}
</script>
{% endblock %}