963 lines
39 KiB
HTML
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 %} |