Initial Codes
This commit is contained in:
@@ -0,0 +1,508 @@
|
||||
{% 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 %}
|
||||
Reference in New Issue
Block a user