Initial Codes

This commit is contained in:
2026-02-13 10:38:45 -05:00
parent 0dd81218e0
commit 40a151787f
24 changed files with 7627 additions and 1 deletions
+597
View File
@@ -0,0 +1,597 @@
{% extends "base.html" %}
{% block title %}Templates - QBO Excel Sync{% endblock %}
{% block content %}
<div class="templates-page">
<div class="page-header">
<h2>Mapping Templates</h2>
<p>Create and manage field mapping templates for your imports.</p>
</div>
<div class="templates-layout">
<aside class="templates-sidebar">
<div class="sidebar-header">
<h3>Templates</h3>
<button class="btn btn-primary btn-sm" id="new-template-btn">+ New</button>
</div>
<div class="filter-group">
<select id="filter-type">
<option value="">All Types</option>
<option value="check">Check</option>
<option value="invoice">Invoice</option>
<option value="bill">Bill</option>
<option value="customer">Customer</option>
<option value="vendor">Vendor</option>
<option value="account">Account</option>
</select>
</div>
<ul class="template-list" id="template-list">
{% for template in templates %}
<li class="template-item" data-name="{{ template.name }}" data-type="{{ template.data_type }}">
<span class="template-name">{{ template.name }}</span>
<span class="template-type">{{ template.data_type }}</span>
</li>
{% endfor %}
{% if not templates %}
<li class="empty-state">No custom templates yet</li>
{% endif %}
</ul>
</aside>
<main class="template-detail" id="template-detail">
<div class="empty-detail"><p>Select a template or create a new one</p></div>
</main>
</div>
</div>
<!-- Template Modal -->
<div class="modal" id="template-modal" hidden style="display: none;">
<div class="modal-backdrop" id="modal-backdrop"></div>
<div class="modal-content modal-lg">
<div class="modal-header">
<h3 id="modal-title">New Template</h3>
<button type="button" class="btn btn-icon" id="modal-close">&times;</button>
</div>
<div class="modal-body">
<div class="form-row">
<div class="form-group">
<label>Template Name</label>
<input type="text" id="template-name" placeholder="e.g., My Payroll Import" required>
</div>
<div class="form-group">
<label>Data Type</label>
<select id="template-type">
<option value="check">Check</option>
<option value="invoice">Invoice</option>
<option value="bill">Bill</option>
<option value="customer">Customer</option>
<option value="vendor">Vendor</option>
<option value="account">Account</option>
</select>
</div>
</div>
<div class="mapping-section">
<div class="mapping-header">
<label>Field Mappings</label>
<div class="mapping-actions">
<button type="button" class="btn btn-sm btn-secondary" id="load-defaults">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="7 10 12 15 17 10"/>
<line x1="12" y1="15" x2="12" y2="3"/>
</svg>
Load Defaults
</button>
</div>
</div>
<div class="excel-columns-note" id="excel-columns-note" style="display: none;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/>
<line x1="12" y1="16" x2="12" y2="12"/>
<line x1="12" y1="8" x2="12.01" y2="8"/>
</svg>
<span>Excel columns available from your last uploaded file</span>
</div>
<table class="data-table mapping-table">
<thead>
<tr>
<th style="width: 25%;">Excel Column</th>
<th style="width: 35%;">QBO Field</th>
<th style="width: 15%;">Transform</th>
<th style="width: 5%;">Req</th>
<th style="width: 15%;">Default Value</th>
<th style="width: 5%;"></th>
</tr>
</thead>
<tbody id="modal-mappings"></tbody>
</table>
<button type="button" class="btn btn-sm" id="add-mapping-row">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="12" y1="5" x2="12" y2="19"/>
<line x1="5" y1="12" x2="19" y2="12"/>
</svg>
Add Field
</button>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" id="modal-cancel">Cancel</button>
<button type="button" class="btn btn-primary" id="modal-save">Save Template</button>
</div>
</div>
</div>
<style>
.modal-lg {
max-width: 900px;
width: 95%;
}
.mapping-section {
margin-top: 20px;
}
.mapping-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.mapping-header label {
font-weight: 600;
font-size: 14px;
}
.mapping-actions {
display: flex;
gap: 8px;
}
.excel-columns-note {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: #e0f2fe;
color: #0369a1;
border-radius: 6px;
font-size: 13px;
margin-bottom: 12px;
}
.mapping-table {
margin-bottom: 12px;
}
.mapping-table select,
.mapping-table input[type="text"] {
width: 100%;
padding: 6px 8px;
font-size: 13px;
}
.mapping-table input[type="checkbox"] {
width: 18px;
height: 18px;
}
.mapping-table .qbo-field-select {
min-width: 200px;
}
.mapping-table .qbo-field-select option {
padding: 4px 8px;
}
.mapping-table .qbo-field-select optgroup {
font-weight: 600;
color: #374151;
}
.field-required {
color: #dc2626;
font-weight: bold;
}
.remove-row-btn {
padding: 4px 8px;
background: #fee2e2;
color: #dc2626;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.remove-row-btn:hover {
background: #fecaca;
}
</style>
{% endblock %}
{% block extra_js %}
<script>
let currentTemplate = null;
let modalMappings = [];
let qboFields = [];
let excelColumns = [];
document.addEventListener('DOMContentLoaded', function() {
// Template list click handlers
document.querySelectorAll('.template-item').forEach(item => {
item.addEventListener('click', () => loadTemplate(item.dataset.name));
});
// Filter handler
document.getElementById('filter-type').addEventListener('change', filterTemplates);
// New template button
document.getElementById('new-template-btn').addEventListener('click', openNewTemplate);
// Data type change - reload QBO fields
document.getElementById('template-type').addEventListener('change', loadQBOFields);
// Modal close handlers
document.getElementById('modal-close').addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
closeModal();
});
document.getElementById('modal-cancel').addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
closeModal();
});
document.getElementById('modal-backdrop').addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
closeModal();
});
// Modal action handlers
document.getElementById('modal-save').addEventListener('click', saveTemplate);
document.getElementById('load-defaults').addEventListener('click', loadDefaultFields);
document.getElementById('add-mapping-row').addEventListener('click', addMappingRow);
// Close modal on Escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closeModal();
}
});
});
function filterTemplates() {
const type = document.getElementById('filter-type').value;
document.querySelectorAll('.template-item').forEach(item => {
item.style.display = (!type || item.dataset.type === type) ? '' : 'none';
});
}
async function loadTemplate(name) {
try {
const response = await fetch('/api/templates');
const result = await response.json();
if (result.success) {
const template = result.templates.find(t => t.name === name);
if (template) {
currentTemplate = template;
renderTemplateDetail(template);
document.querySelectorAll('.template-item').forEach(i => i.classList.toggle('active', i.dataset.name === name));
}
}
} catch (error) { showToast('Error loading template', 'error'); }
}
function renderTemplateDetail(template) {
document.getElementById('template-detail').innerHTML =
'<div class="detail-header"><h3>' + escapeHtml(template.name) + '</h3><span class="badge">' + template.data_type + '</span></div>' +
'<div class="detail-actions"><button class="btn btn-secondary" onclick="editTemplate()">Edit</button><button class="btn btn-danger" onclick="deleteTemplate()">Delete</button></div>' +
'<table class="data-table"><thead><tr><th>Excel Column</th><th>QBO Field</th><th>Transform</th><th>Required</th></tr></thead><tbody>' +
template.mappings.map(m => '<tr><td>' + escapeHtml(m.excel_column || '-') + '</td><td>' + escapeHtml(m.qbo_field) + '</td><td>' + (m.transform || '-') + '</td><td>' + (m.required ? 'Yes' : 'No') + '</td></tr>').join('') +
'</tbody></table>';
}
async function loadQBOFields() {
const type = document.getElementById('template-type').value;
try {
const response = await fetch('/api/qbo-fields/' + type);
const result = await response.json();
if (result.success) {
qboFields = result.qbo_fields || [];
excelColumns = result.excel_columns || [];
// Show/hide Excel columns note
const note = document.getElementById('excel-columns-note');
if (excelColumns.length > 0) {
note.style.display = 'flex';
} else {
note.style.display = 'none';
}
// Re-render mappings with new fields
renderModalMappings();
}
} catch (error) {
console.error('Error loading QBO fields:', error);
}
}
async function openNewTemplate() {
currentTemplate = null;
document.getElementById('modal-title').textContent = 'New Template';
document.getElementById('template-name').value = '';
document.getElementById('template-type').value = 'check';
modalMappings = [];
// Load QBO fields for default type
await loadQBOFields();
const modal = document.getElementById('template-modal');
modal.hidden = false;
modal.style.display = 'flex';
}
async function editTemplate() {
if (!currentTemplate) return;
document.getElementById('modal-title').textContent = 'Edit Template';
document.getElementById('template-name').value = currentTemplate.name;
document.getElementById('template-type').value = currentTemplate.data_type;
modalMappings = JSON.parse(JSON.stringify(currentTemplate.mappings));
// Load QBO fields for this type
await loadQBOFields();
const modal = document.getElementById('template-modal');
modal.hidden = false;
modal.style.display = 'flex';
}
function closeModal() {
const modal = document.getElementById('template-modal');
if (modal) {
modal.hidden = true;
modal.style.display = 'none';
}
}
function renderModalMappings() {
const tbody = document.getElementById('modal-mappings');
// Build Excel columns options
let excelOptions = '<option value="">-- Select or type --</option>';
excelColumns.forEach(col => {
excelOptions += '<option value="' + escapeHtml(col) + '">' + escapeHtml(col) + '</option>';
});
// Build QBO fields options grouped by required/optional
let qboOptions = '<option value="">-- Select QBO Field --</option>';
const requiredFields = qboFields.filter(f => f.required);
const optionalFields = qboFields.filter(f => !f.required);
if (requiredFields.length > 0) {
qboOptions += '<optgroup label="Required Fields">';
requiredFields.forEach(f => {
qboOptions += '<option value="' + escapeHtml(f.field) + '" data-transform="' + (f.transform || '') + '">' +
escapeHtml(f.label) + ' *</option>';
});
qboOptions += '</optgroup>';
}
if (optionalFields.length > 0) {
qboOptions += '<optgroup label="Optional Fields">';
optionalFields.forEach(f => {
qboOptions += '<option value="' + escapeHtml(f.field) + '" data-transform="' + (f.transform || '') + '">' +
escapeHtml(f.label) + '</option>';
});
qboOptions += '</optgroup>';
}
tbody.innerHTML = modalMappings.map((m, i) => {
// Check if excel_column matches any in the list
const excelSelected = excelColumns.includes(m.excel_column);
// Check if qbo_field matches any in the list
const qboMatch = qboFields.find(f => f.field === m.qbo_field);
return '<tr>' +
// Excel Column - combo of select and input
'<td>' +
'<input type="text" list="excel-list-' + i + '" value="' + escapeHtml(m.excel_column || '') + '" ' +
'data-index="' + i + '" data-field="excel_column" placeholder="Type or select...">' +
'<datalist id="excel-list-' + i + '">' +
excelColumns.map(col => '<option value="' + escapeHtml(col) + '">').join('') +
'</datalist>' +
'</td>' +
// QBO Field - dropdown
'<td>' +
'<select class="qbo-field-select" data-index="' + i + '" data-field="qbo_field">' +
qboOptions.replace(
'value="' + escapeHtml(m.qbo_field || '') + '"',
'value="' + escapeHtml(m.qbo_field || '') + '" selected'
) +
'</select>' +
'</td>' +
// Transform
'<td>' +
'<select data-index="' + i + '" data-field="transform">' +
'<option value="">None</option>' +
'<option value="date"' + (m.transform === 'date' ? ' selected' : '') + '>Date</option>' +
'<option value="currency"' + (m.transform === 'currency' ? ' selected' : '') + '>Currency</option>' +
'<option value="number"' + (m.transform === 'number' ? ' selected' : '') + '>Number</option>' +
'</select>' +
'</td>' +
// Required checkbox
'<td style="text-align: center;">' +
'<input type="checkbox" data-index="' + i + '" data-field="required"' + (m.required ? ' checked' : '') + '>' +
'</td>' +
// Default value
'<td>' +
'<input type="text" value="' + escapeHtml(m.default_value || '') + '" data-index="' + i + '" data-field="default_value" placeholder="Default">' +
'</td>' +
// Remove button
'<td>' +
'<button type="button" class="remove-row-btn" onclick="removeMappingRow(' + i + ')">&times;</button>' +
'</td>' +
'</tr>';
}).join('');
// Add event listeners
tbody.querySelectorAll('input, select').forEach(el => {
el.addEventListener('change', updateModalMapping);
});
// Auto-set transform when QBO field changes
tbody.querySelectorAll('.qbo-field-select').forEach(select => {
select.addEventListener('change', function(e) {
const idx = parseInt(e.target.dataset.index);
const selectedOption = e.target.options[e.target.selectedIndex];
const transform = selectedOption.dataset.transform;
// Auto-set transform if defined for this field
if (transform) {
const transformSelect = tbody.querySelector('select[data-index="' + idx + '"][data-field="transform"]');
if (transformSelect) {
transformSelect.value = transform;
modalMappings[idx].transform = transform;
}
}
// Auto-set required if it's a required field
const qboField = qboFields.find(f => f.field === e.target.value);
if (qboField && qboField.required) {
const reqCheckbox = tbody.querySelector('input[data-index="' + idx + '"][data-field="required"]');
if (reqCheckbox) {
reqCheckbox.checked = true;
modalMappings[idx].required = true;
}
}
});
});
}
function updateModalMapping(e) {
const idx = parseInt(e.target.dataset.index);
const field = e.target.dataset.field;
if (field === 'required') {
modalMappings[idx][field] = e.target.checked;
} else {
modalMappings[idx][field] = e.target.value || null;
}
}
function addMappingRow() {
modalMappings.push({
excel_column: '',
qbo_field: '',
transform: null,
required: false,
default_value: null
});
renderModalMappings();
// Scroll to the new row
const tbody = document.getElementById('modal-mappings');
const lastRow = tbody.lastElementChild;
if (lastRow) {
lastRow.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
}
function removeMappingRow(idx) {
modalMappings.splice(idx, 1);
renderModalMappings();
}
async function loadDefaultFields() {
const type = document.getElementById('template-type').value;
try {
const response = await fetch('/api/templates/default/' + type);
const result = await response.json();
if (result.success) {
modalMappings = result.template.mappings;
renderModalMappings();
showToast('Default fields loaded', 'success');
}
} catch (error) {
showToast('Error loading defaults', 'error');
}
}
async function saveTemplate() {
const name = document.getElementById('template-name').value.trim();
const type = document.getElementById('template-type').value;
if (!name) {
showToast('Please enter a template name', 'warning');
return;
}
// Filter out empty mappings
const validMappings = modalMappings.filter(m => m.qbo_field);
if (validMappings.length === 0) {
showToast('Please add at least one field mapping', 'warning');
return;
}
try {
const response = await fetch('/api/templates', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: name,
data_type: type,
mappings: validMappings
})
});
const result = await response.json();
if (result.success) {
closeModal();
showToast('Template saved successfully', 'success');
setTimeout(function() {
location.reload();
}, 500);
} else {
showToast(result.error || 'Failed to save template', 'error');
}
} catch (error) {
showToast('Error saving template: ' + error.message, 'error');
}
}
async function deleteTemplate() {
if (!currentTemplate) return;
if (!confirm('Are you sure you want to delete template "' + currentTemplate.name + '"?')) return;
try {
const response = await fetch('/api/templates/' + encodeURIComponent(currentTemplate.name), {
method: 'DELETE'
});
const result = await response.json();
if (result.success) {
showToast('Template deleted successfully', 'success');
location.reload();
} else {
showToast(result.error || 'Failed to delete template', 'error');
}
} catch (error) {
showToast('Error deleting template: ' + error.message, 'error');
}
}
function escapeHtml(text) {
if (!text) return '';
const d = document.createElement('div');
d.textContent = String(text);
return d.innerHTML;
}
</script>
{% endblock %}