Files
2026-04-23 21:38:24 -04:00

841 lines
43 KiB
JavaScript

// Tools Import/Export Wizard
(function () {
'use strict';
// ── Anti-forgery token ────────────────────────────────────────────────────
const tokenInput = document.querySelector('input[name="__RequestVerificationToken"]');
if (!tokenInput) { console.error('[Tools] Anti-forgery token not found'); return; }
const token = tokenInput.value;
// ── Account select data (embedded by Razor as JSON) ───────────────────────
let accountData = { revenueAccounts: [], cogsAccounts: [], inventoryAccounts: [] };
try {
const el = document.getElementById('toolsAccountData');
if (el) accountData = JSON.parse(el.textContent);
} catch (e) { console.warn('[Tools] Could not parse account data'); }
// ── Wizard state ──────────────────────────────────────────────────────────
let wDir = null; // 'import' | 'export'
let wFmt = null; // 'csv' | 'qb-desktop' | 'qb-online'
let wItem = null; // selected item object
let wStep = 1;
// ── Item catalog ──────────────────────────────────────────────────────────
const ITEMS = [
// ── CSV Import ──────────────────────────────────────────────────────
{ key: 'csv-customers',
label: 'Customers', icon: 'bi-people', color: '#2563eb',
desc: 'Contact info, addresses, and balances',
dir: ['import'], fmt: ['csv'],
endpoint: '/Tools/CsvImportCustomers', accept: '.csv',
template: '/Tools/DownloadCustomerTemplate',
tips: ['Download the CSV template to see the expected columns', 'One customer per row — existing records matched by company name are updated'] },
{ key: 'csv-vendors',
label: 'Vendors', icon: 'bi-truck', color: '#d97706',
desc: 'Supplier records and contact info',
dir: ['import'], fmt: ['csv'],
endpoint: '/Tools/CsvImportVendors', accept: '.csv',
template: '/Tools/DownloadVendorTemplate',
tips: ['Download the CSV template to see the expected columns', 'One vendor per row — existing records matched by company name are updated'] },
{ key: 'csv-catalog',
label: 'Catalog Items', icon: 'bi-box-seam', color: '#059669',
desc: 'Pre-priced service catalog entries',
dir: ['import'], fmt: ['csv'],
endpoint: '/Tools/CsvImportCatalogItems', accept: '.csv',
template: '/Tools/DownloadCatalogTemplate',
extraFields: [
{ name: 'revenueAccountId', label: 'Revenue Account', hint: 'Optional', accountKey: 'revenueAccounts' },
{ name: 'cogsAccountId', label: 'COGS Account', hint: 'Optional', accountKey: 'cogsAccounts' },
],
tips: ['Download the CSV template', 'Optionally map to GL accounts before uploading'] },
{ key: 'csv-inventory',
label: 'Inventory', icon: 'bi-boxes', color: '#0891b2',
desc: 'Stock items, quantities, and unit costs',
dir: ['import'], fmt: ['csv'],
endpoint: '/Tools/CsvImportInventoryItems', accept: '.csv',
template: '/Tools/DownloadInventoryTemplate',
extraFields: [
{ name: 'inventoryAccountId', label: 'Inventory Asset Account', hint: 'Optional', accountKey: 'inventoryAccounts' },
{ name: 'cogsAccountId', label: 'COGS Account', hint: 'Optional', accountKey: 'cogsAccounts' },
],
tips: ['Download the CSV template', 'Optionally map to GL accounts before uploading'] },
{ key: 'csv-quotes',
label: 'Quotes', icon: 'bi-file-earmark-text', color: '#d97706',
desc: 'Quote records with statuses and totals',
dir: ['import'], fmt: ['csv'],
endpoint: '/Tools/CsvImportQuotes', accept: '.csv',
template: '/Tools/DownloadQuoteTemplate',
tips: ['Download the CSV template', 'One quote per row'] },
{ key: 'csv-jobs',
label: 'Jobs', icon: 'bi-briefcase', color: '#059669',
desc: 'Job records with statuses and priorities',
dir: ['import'], fmt: ['csv'],
endpoint: '/Tools/CsvImportJobs', accept: '.csv',
template: '/Tools/DownloadJobTemplate',
tips: ['Download the CSV template', 'One job per row'] },
{ key: 'csv-appointments',
label: 'Appointments', icon: 'bi-calendar-check', color: '#2563eb',
desc: 'Scheduled customer appointments',
dir: ['import'], fmt: ['csv'],
endpoint: '/Tools/CsvImportAppointments', accept: '.csv',
template: '/Tools/DownloadAppointmentTemplate',
tips: ['Download the CSV template', 'One appointment per row'] },
{ key: 'csv-equipment',
label: 'Equipment', icon: 'bi-tools', color: '#dc2626',
desc: 'Equipment records and status',
dir: ['import'], fmt: ['csv'],
endpoint: '/Tools/CsvImportEquipment', accept: '.csv',
template: '/Tools/DownloadEquipmentTemplate',
tips: ['Download the CSV template', 'One equipment item per row'] },
{ key: 'csv-maintenance',
label: 'Maintenance Records', icon: 'bi-wrench', color: '#6b7280',
desc: 'Scheduled and completed maintenance',
dir: ['import'], fmt: ['csv'],
endpoint: '/Tools/CsvImportMaintenance', accept: '.csv',
template: '/Tools/DownloadMaintenanceTemplate',
tips: ['Download the CSV template', 'One maintenance record per row'] },
{ key: 'csv-prepservices',
label: 'Prep Services', icon: 'bi-hammer', color: '#7c3aed',
desc: 'Sandblasting, masking, and prep options',
dir: ['import'], fmt: ['csv'],
endpoint: '/Tools/CsvImportPrepServices', accept: '.csv',
template: '/Tools/DownloadPrepServiceTemplate',
tips: ['Existing services matched by name are updated'] },
{ key: 'csv-coa',
label: 'Chart of Accounts', icon: 'bi-journal-text', color: '#374151',
desc: 'GL accounts — import first (required for expenses and bills)',
badge: 'Import first',
dir: ['import'], fmt: ['csv'],
endpoint: '/Tools/CsvImportChartOfAccounts', accept: '.csv',
template: '/Tools/DownloadChartOfAccountsTemplate',
tips: ['Download the CSV template to see required columns',
'Valid AccountType values: Asset, Liability, Equity, Revenue, CostOfGoods, Expense',
'Existing accounts matched by AccountNumber are updated; system accounts are never modified'] },
{ key: 'csv-expenses',
label: 'Expenses', icon: 'bi-receipt-cutoff', color: '#dc2626',
desc: 'Direct expenses with account, vendor, and job links',
dir: ['import'], fmt: ['csv'],
endpoint: '/Tools/CsvImportExpenses', accept: '.csv',
template: '/Tools/DownloadExpenseTemplate',
tips: ['Download the CSV template to see required columns',
'ExpenseAccountNumber and PaymentAccountNumber must match account numbers in your Chart of Accounts',
'VendorName and JobNumber are optional — leave blank if not applicable',
'Valid PaymentMethod values: Cash, Check, CreditDebitCard, BankTransferACH, DigitalPayment'] },
{ key: 'csv-settings',
label: 'Company Settings', icon: 'bi-gear', color: '#d97706',
desc: 'Operating costs and configuration',
dir: ['import'], fmt: ['csv'],
endpoint: '/Tools/CsvImportCompanySettings', accept: '.csv',
template: '/Tools/DownloadCompanySettingsTemplate',
warning: 'This will overwrite your current company settings. Download a backup first.',
tips: ['Download a backup of your current settings first', 'Modify the CSV, then upload it below'] },
// ── CSV Export ──────────────────────────────────────────────────────
{ key: 'exp-customers',
label: 'Customers', icon: 'bi-people', color: '#2563eb',
desc: 'Contact info, addresses, and balances',
dir: ['export'], fmt: ['csv'], exportUrl: '/Tools/ExportCustomersCsv' },
{ key: 'exp-vendors',
label: 'Vendors', icon: 'bi-truck', color: '#d97706',
desc: 'Supplier records, contact info, and payment terms',
dir: ['export'], fmt: ['csv'], exportUrl: '/Tools/ExportVendorsCsv' },
{ key: 'exp-quotes',
label: 'Quotes', icon: 'bi-file-earmark-text', color: '#0891b2',
desc: 'Status, dates, totals, and customer info',
dir: ['export'], fmt: ['csv'], exportUrl: '/Tools/ExportQuotesCsv' },
{ key: 'exp-jobs',
label: 'Jobs', icon: 'bi-briefcase', color: '#059669',
desc: 'Status, priority, dates, and completion data',
dir: ['export'], fmt: ['csv'], exportUrl: '/Tools/ExportJobsCsv' },
{ key: 'exp-appointments',
label: 'Appointments', icon: 'bi-calendar-check', color: '#d97706',
desc: 'Customer, type, status, and scheduling details',
dir: ['export'], fmt: ['csv'], exportUrl: '/Tools/ExportAppointmentsCsv' },
{ key: 'exp-catalog',
label: 'Catalog Items', icon: 'bi-box-seam', color: '#6b7280',
desc: 'SKU, pricing, categories, and descriptions',
dir: ['export'], fmt: ['csv'], exportUrl: '/Tools/ExportCatalogCsv' },
{ key: 'exp-inventory',
label: 'Inventory', icon: 'bi-boxes', color: '#1f2937',
desc: 'Quantities, costs, and location details',
dir: ['export'], fmt: ['csv'], exportUrl: '/Tools/ExportInventoryCsv' },
{ key: 'exp-equipment',
label: 'Equipment', icon: 'bi-tools', color: '#dc2626',
desc: 'Details, purchase info, and current status',
dir: ['export'], fmt: ['csv'], exportUrl: '/Tools/ExportEquipmentCsv' },
{ key: 'exp-maintenance',
label: 'Maintenance Records', icon: 'bi-wrench', color: '#6b7280',
desc: 'Scheduled and completed maintenance with equipment and costs',
dir: ['export'], fmt: ['csv'], exportUrl: '/Tools/ExportMaintenanceCsv' },
{ key: 'exp-prepservices',
label: 'Prep Services', icon: 'bi-tools', color: '#7c3aed',
desc: 'Preparation service catalog (sandblasting, stripping, etc.)',
dir: ['export'], fmt: ['csv'], exportUrl: '/Tools/ExportPrepServicesCsv' },
{ key: 'exp-purchaseorders',
label: 'Purchase Orders', icon: 'bi-cart', color: '#6b7280',
desc: 'Vendor, status, dates, and totals',
dir: ['export'], fmt: ['csv'], exportUrl: '/Tools/ExportPurchaseOrdersCsv' },
{ key: 'exp-coa',
label: 'Chart of Accounts', icon: 'bi-journal-text', color: '#374151',
desc: 'Full GL account list with types, balances, and account numbers',
dir: ['export'], fmt: ['csv'], exportUrl: '/Tools/ExportChartOfAccountsCsv' },
{ key: 'exp-expenses',
label: 'Expenses', icon: 'bi-receipt-cutoff', color: '#dc2626',
desc: 'Direct expenses with dates, accounts, vendors, and amounts',
dir: ['export'], fmt: ['csv'], exportUrl: '/Tools/ExportExpensesCsv' },
{ key: 'exp-settings',
label: 'Company Settings', icon: 'bi-gear-fill', color: '#d97706',
desc: 'Company info, operating costs, and preferences',
dir: ['export'], fmt: ['csv'], exportUrl: '/Tools/ExportCompanySettingsCsv' },
// ── QB Desktop Import ───────────────────────────────────────────────
{ key: 'qbd-coa',
label: 'Chart of Accounts', icon: 'bi-journal-text', color: '#374151',
desc: 'Account list — import this first (required for bills)',
badge: 'Import first',
dir: ['import'], fmt: ['qb-desktop'],
endpoint: '/Tools/ImportChartOfAccounts', accept: '.iif,.txt',
tips: ['Lists → Chart of Accounts → right-click → Export → save as .iif', 'Upload the .iif file below'] },
{ key: 'qbd-customers',
label: 'Customers', icon: 'bi-people', color: '#2563eb',
desc: 'Customer list from QB Desktop',
dir: ['import'], fmt: ['qb-desktop'],
endpoint: '/Tools/ImportCustomers', accept: '.iif,.txt',
tips: ['File → Utilities → Export → Lists to IIF Files → select Customers', 'Upload the .iif file below'] },
{ key: 'qbd-vendors',
label: 'Vendors', icon: 'bi-truck', color: '#d97706',
desc: 'Vendor list from QB Desktop',
dir: ['import'], fmt: ['qb-desktop'],
endpoint: '/Tools/ImportVendors', accept: '.iif,.txt',
tips: ['File → Utilities → Export → Lists to IIF Files → select Vendors', 'Upload the .iif file below'] },
{ key: 'qbd-catalog',
label: 'Catalog Items', icon: 'bi-box-seam', color: '#059669',
desc: 'Service items from QB Desktop',
dir: ['import'], fmt: ['qb-desktop'],
endpoint: '/Tools/ImportCatalogItems', accept: '.iif,.txt',
tips: ['File → Utilities → Export → Lists to IIF Files → select Items', 'Upload the .iif file below'] },
{ key: 'qbd-inventory',
label: 'Inventory Stock', icon: 'bi-boxes', color: '#0891b2',
desc: 'Inventory Valuation Summary — include Pref Vendor column',
dir: ['import'], fmt: ['qb-desktop'],
endpoint: '/Tools/ImportQbInventoryValuation', accept: '.csv',
tips: [
'Reports → Inventory → Inventory Valuation Summary',
'Customize Report → Display tab → check <strong>Pref Vendor</strong>',
'Excel → Create New Worksheet → Comma Separated Values (.csv)',
'Upload the saved .csv file below'
] },
{ key: 'qbd-invoices',
label: 'Invoices', icon: 'bi-receipt', color: '#2563eb',
desc: 'Customer Balance Detail report',
dir: ['import'], fmt: ['qb-desktop'],
endpoint: '/Tools/ImportQbInvoices', accept: '.csv',
tips: ['Reports → Customers & Receivables → Customer Balance Detail', 'Set date range to <strong>All</strong>', 'Excel → Create New Worksheet → Comma Separated Values (.csv)', 'Upload the saved .csv file below'] },
{ key: 'qbd-transactions',
label: 'Transactions', icon: 'bi-arrow-left-right', color: '#059669',
desc: 'Transaction List by Customer report',
dir: ['import'], fmt: ['qb-desktop'],
endpoint: '/Tools/ImportQbTransactions', accept: '.csv',
tips: ['Reports → Customers & Receivables → Transaction List by Customer', 'Set date range to <strong>All</strong>', 'Excel → Create New Worksheet → Comma Separated Values (.csv)', 'Upload the saved .csv file below'] },
{ key: 'qbd-bills',
label: 'Bills & Payments', icon: 'bi-file-earmark-minus', color: '#dc2626',
desc: 'Vendor Balance Detail report — imports bills and payments in one pass',
dir: ['import'], fmt: ['qb-desktop'],
endpoint: '/Tools/ImportQbBillsAndPayments', accept: '.csv',
tips: [
'Reports → Vendors &amp; Payables → Vendor Balance Detail',
'Set date range to <strong>All</strong>',
'Click <strong>Customize Report</strong> → <strong>Display</strong> tab → check <strong>Memo</strong> to include bill and payment descriptions',
'Excel → Create New Worksheet → Comma Separated Values (.csv)',
'Upload the file — bills are imported first, then payments are matched against them automatically'
] },
// ── QB Online Import ────────────────────────────────────────────────
{ key: 'qbo-coa',
label: 'Chart of Accounts', icon: 'bi-journal-text', color: '#374151',
desc: 'Account List — import this first (required for invoices)',
badge: 'Import first',
dir: ['import'], fmt: ['qb-online'],
endpoint: '/Tools/ImportQboChartOfAccounts', accept: '.xlsx,.xls',
tips: [
'Accounting (left nav) → Chart of Accounts → Download/Export button',
'<strong>Or:</strong> Reports → search "Account List" → Export to Excel',
'Upload the .xlsx file below'
] },
{ key: 'qbo-customers',
label: 'Customers', icon: 'bi-people', color: '#2563eb',
desc: 'Customer Contact List from QuickBooks Online',
dir: ['import'], fmt: ['qb-online'],
endpoint: '/Tools/ImportQboCustomers', accept: '.xlsx,.xls',
tips: [
'Reports → search "Customer Contact List" → Customize → add all desired columns → Export to Excel',
'<strong>Tip:</strong> Add Billing Street, City, State, Zip columns via Customize → Rows/Columns for best results',
'Upload the .xlsx file below'
] },
{ key: 'qbo-vendors',
label: 'Vendors', icon: 'bi-truck', color: '#d97706',
desc: 'Vendor Contact List from QuickBooks Online',
dir: ['import'], fmt: ['qb-online'],
endpoint: '/Tools/ImportQboVendors', accept: '.xlsx,.xls',
tips: [
'Reports → search "Vendor Contact List" → Customize → add all desired columns → Export to Excel',
'<strong>Or:</strong> Expenses → Vendors → Export icon (box with arrow) → Export to Excel',
'Upload the .xlsx file below'
] },
{ key: 'qbo-products',
label: 'Products &amp; Services', icon: 'bi-box-seam', color: '#059669',
desc: 'Product/service catalog from QuickBooks Online',
dir: ['import'], fmt: ['qb-online'],
endpoint: '/Tools/ImportQboCatalogItems', accept: '.xlsx,.xls',
tips: [
'Sales → Products and Services → Export to Excel icon (top right)',
'Inventory-type items are imported as Inventory; all others as Catalog Items',
'Items named <em>Category:Item Name</em> will have the category prefix stripped automatically',
'Upload the .xlsx file below'
] },
{ key: 'qbo-invoices',
label: 'Invoices', icon: 'bi-receipt', color: '#2563eb',
desc: 'Invoice List report from QuickBooks Online',
dir: ['import'], fmt: ['qb-online'],
endpoint: '/Tools/ImportQboInvoices', accept: '.xlsx,.xls',
tips: [
'Reports → search "Invoice List" → set your date range → Export to Excel',
'Customers must be imported first so invoices can be linked',
'Invoice totals and open balances are imported; line item detail is not available in this report',
'Upload the .xlsx file below'
] },
{ key: 'qbo-transactions',
label: 'Transactions', icon: 'bi-arrow-left-right', color: '#059669',
desc: 'Transaction List — applies payments to imported invoices',
dir: ['import'], fmt: ['qb-online'],
endpoint: '/Tools/ImportQboTransactions', accept: '.xlsx,.xls',
tips: [
'Reports → search "Transaction List by Date" → set All Dates → Export to Excel',
'Only Payment/Receipt rows are processed; Invoice rows are skipped',
'Invoices must be imported first so payments can be matched by reference number',
'Upload the .xlsx file below'
] },
// ── QB Export ───────────────────────────────────────────────────────
{ key: 'qb-exp-customers',
label: 'Customers', icon: 'bi-people', color: '#2563eb',
desc: 'Export all active customers',
dir: ['export'], fmt: ['qb-desktop', 'qb-online'],
exportUrlDesktop: '/Tools/ExportCustomers?format=desktop',
exportUrlOnline: '/Tools/ExportCustomers?format=online' },
{ key: 'qb-exp-vendors',
label: 'Vendors', icon: 'bi-truck', color: '#d97706',
desc: 'Export all active vendors to IIF format',
dir: ['export'], fmt: ['qb-desktop'],
exportUrl: '/Tools/ExportVendors' },
{ key: 'qb-exp-catalog',
label: 'Catalog Items', icon: 'bi-box-seam', color: '#059669',
desc: 'Export active catalog items as service items',
dir: ['export'], fmt: ['qb-desktop', 'qb-online'],
exportUrlDesktop: '/Tools/ExportCatalogItems?format=desktop',
exportUrlOnline: '/Tools/ExportCatalogItems?format=online' },
];
// ── Helpers ───────────────────────────────────────────────────────────────
function getExportUrl(item) {
if (item.exportUrl) return item.exportUrl;
if (wFmt === 'qb-online' && item.exportUrlOnline) return item.exportUrlOnline;
if (item.exportUrlDesktop) return item.exportUrlDesktop;
return '#';
}
function filteredItems() {
return ITEMS.filter(it => it.dir.includes(wDir) && it.fmt.includes(wFmt));
}
// ── Wizard Navigation ─────────────────────────────────────────────────────
window.wizardSetDirection = function (dir) {
wDir = dir;
wStep = 2;
setBreadcrumb(2, dir === 'import' ? 'Import' : 'Export');
document.getElementById('step2-heading').textContent =
dir === 'import' ? 'Import from which format?' : 'Export to which format?';
showStep(2);
document.getElementById('wizardBackBtn').classList.remove('d-none');
};
window.wizardSetFormat = function (fmt) {
wFmt = fmt;
wStep = 3;
const labels = { csv: 'CSV', 'qb-desktop': 'QB Desktop', 'qb-online': 'QB Online' };
setBreadcrumb(3, labels[fmt] || fmt);
renderStep3();
showStep(3);
};
window.wizardBack = function () {
if (wStep === 4) {
wItem = null;
wStep = 3;
resetBreadcrumb(4, wDir === 'import' ? 'Import' : 'Export');
renderStep3();
showStep(3);
} else if (wStep === 3) {
wFmt = null;
wStep = 2;
resetBreadcrumb(3, 'Select');
showStep(2);
} else if (wStep === 2) {
wDir = null;
wStep = 1;
resetBreadcrumb(2, 'Format');
resetBreadcrumb(1, 'Direction');
showStep(1);
document.getElementById('wizardBackBtn').classList.add('d-none');
}
};
window.wizardSelectItem = function (key) {
wItem = ITEMS.find(it => it.key === key);
if (!wItem) return;
wStep = 4;
setBreadcrumb(4, wItem.label);
renderStep4();
showStep(4);
};
function showStep(n) {
[1, 2, 3, 4].forEach(function (s) {
const el = document.getElementById('wizard-step-' + s);
if (el) el.classList.toggle('d-none', s !== n);
});
updateBreadcrumbActive(n);
}
function setBreadcrumb(step, label) {
const lbl = document.getElementById('bc-' + step + '-label');
const bdg = document.getElementById('bc-' + step + '-badge');
if (lbl) { lbl.textContent = label; lbl.className = 'small fw-semibold'; }
if (bdg) bdg.className = 'badge rounded-pill bg-primary';
}
function resetBreadcrumb(step, label) {
const lbl = document.getElementById('bc-' + step + '-label');
const bdg = document.getElementById('bc-' + step + '-badge');
if (lbl) { lbl.textContent = label; lbl.className = 'small text-muted'; }
if (bdg) bdg.className = 'badge rounded-pill bg-secondary';
}
function updateBreadcrumbActive(currentStep) {
for (let s = 1; s <= 4; s++) {
const badge = document.getElementById('bc-' + s + '-badge');
if (badge && s < currentStep && badge.className.includes('primary')) {
badge.className = 'badge rounded-pill bg-success';
}
}
}
// ── Step 3: item selection grid ───────────────────────────────────────────
function renderStep3() {
const heading = document.getElementById('step3-heading');
const grid = document.getElementById('step3-grid');
if (!heading || !grid) return;
heading.textContent = wDir === 'import' ? 'What would you like to import?' : 'What would you like to export?';
let html = '';
// CSV Export: "Export All" shortcut
if (wDir === 'export' && wFmt === 'csv') {
html += `<div class="col-12 text-center mb-1">
<a href="/Tools/ExportAllCsv" class="btn btn-primary px-4">
<i class="bi bi-file-earmark-zip me-1"></i>Export All as ZIP
</a>
<p class="text-muted small mt-2 mb-0">Or pick a specific data set below.</p>
</div>`;
}
// QB Desktop Import: recommended order callout
if (wDir === 'import' && wFmt === 'qb-desktop') {
html += `<div class="col-12 mb-1">
<div class="alert alert-info py-2 mb-0 small">
<i class="bi bi-list-ol me-1"></i>
<strong>Recommended order:</strong>
Chart of Accounts &rarr; Customers &rarr; Vendors &rarr; Catalog Items &rarr; Inventory &rarr; Invoices &rarr; Transactions &rarr; Bills &amp; Payments
</div>
</div>`;
}
filteredItems().forEach(function (item) {
const badgeHtml = item.badge
? ` <span class="badge ms-1" style="background:#374151;font-size:0.68rem;vertical-align:middle">${item.badge}</span>`
: '';
html += `
<div class="col-sm-6 col-lg-4">
<div class="card h-100 border-2" role="button"
onclick="wizardSelectItem('${item.key}')"
style="cursor:pointer;border-color:${item.color}!important;transition:transform .1s"
onmouseover="this.style.transform='scale(1.02)'"
onmouseout="this.style.transform=''">
<div class="card-body py-3 px-3">
<div class="d-flex align-items-start gap-3">
<div style="width:42px;height:42px;border-radius:10px;background:${item.color}18;display:flex;align-items:center;justify-content:center;flex-shrink:0">
<i class="bi ${item.icon}" style="font-size:1.3rem;color:${item.color}"></i>
</div>
<div>
<div class="fw-semibold" style="font-size:0.93rem">${item.label}${badgeHtml}</div>
<div class="text-muted" style="font-size:0.8rem">${item.desc}</div>
</div>
</div>
</div>
</div>
</div>`;
});
if (!filteredItems().length) {
html += `<div class="col-12 text-center text-muted py-5">
<i class="bi bi-inbox" style="font-size:2.5rem"></i>
<p class="mt-2 mb-0">No options available for this selection.</p>
</div>`;
}
grid.innerHTML = html;
}
// ── Step 4: upload form or download button ────────────────────────────────
async function renderStep4() {
const container = document.getElementById('step4-content');
if (!container || !wItem) return;
const item = wItem;
const isImport = wDir === 'import';
// Refresh account data from server if this card has account dropdowns,
// so accounts imported earlier in the same page session are available.
if (item.extraFields && item.extraFields.length) {
try {
const resp = await fetch('/Tools/GetImportAccounts');
if (resp.ok) accountData = await resp.json();
} catch (e) { /* keep whatever was loaded at page render */ }
}
// Tips list
const tipsHtml = (item.tips || []).length
? `<ol class="small text-muted ps-3 mb-0">${item.tips.map(t => `<li>${t}</li>`).join('')}</ol>`
: '';
// Warning
const warningHtml = item.warning
? `<div class="alert alert-warning py-2 mb-3 small"><i class="bi bi-exclamation-triangle-fill me-1"></i>${item.warning}</div>`
: '';
// Template download button
const templateHtml = item.template
? `<div class="mb-4">
<a href="${item.template}" class="btn btn-outline-secondary btn-sm">
<i class="bi bi-file-earmark-arrow-down me-1"></i>Download CSV Template
</a>
</div>`
: '';
// Extra account dropdowns
let extraFieldsHtml = '';
(item.extraFields || []).forEach(function (f) {
const opts = (accountData[f.accountKey] || [])
.map(o => `<option value="${o.value}">${o.text}</option>`)
.join('');
extraFieldsHtml += `
<div class="mb-3">
<label class="form-label small fw-semibold mb-1">${f.label}
<span class="text-muted fw-normal">${f.hint ? '— ' + f.hint : ''}</span>
</label>
<select name="${f.name}" class="form-select form-select-sm">
<option value="">(none)</option>
${opts}
</select>
</div>`;
});
let actionHtml;
if (isImport) {
actionHtml = `
${warningHtml}
${templateHtml}
<form id="step4-form" enctype="multipart/form-data">
<div class="mb-3">
<label class="form-label small fw-semibold mb-1">
Select file <span class="text-muted fw-normal">${item.accept}</span>
</label>
<input type="file" class="form-control" id="step4-file" name="file" accept="${item.accept}" required />
</div>
${extraFieldsHtml}
<button type="submit" class="btn btn-primary" id="step4-btn">
<span class="spinner-border spinner-border-sm d-none me-1" id="step4-spinner" role="status"></span>
<i class="bi bi-upload me-1"></i>Import ${item.label}
</button>
</form>
<div id="step4-results" class="mt-4 d-none">
<hr>
<div id="step4-summary" class="mb-2"></div>
<div id="step4-errors"></div>
</div>`;
} else {
const exportUrl = getExportUrl(item);
const qbHelpHtml = (wFmt === 'qb-desktop' || wFmt === 'qb-online')
? `<div class="alert alert-info small mt-3 mb-0">
<i class="bi bi-info-circle me-1"></i>
<strong>Importing into QuickBooks${wFmt === 'qb-online' ? ' Online' : ' Desktop'}:</strong>
${wFmt === 'qb-desktop'
? 'File → Utilities → Import → IIF Files → select the downloaded file.'
: 'Settings → Import Data → select the data type, then upload the file.'}
</div>`
: '';
actionHtml = `
<a href="${exportUrl}" class="btn btn-success btn-lg">
<i class="bi bi-download me-2"></i>Download ${item.label}
</a>
${qbHelpHtml}`;
}
container.innerHTML = `
<div class="row g-4">
<div class="col-md-4">
<div class="d-flex align-items-center gap-2 mb-3">
<div style="width:40px;height:40px;border-radius:8px;background:${item.color}18;display:flex;align-items:center;justify-content:center;flex-shrink:0">
<i class="bi ${item.icon}" style="color:${item.color};font-size:1.2rem"></i>
</div>
<div>
<div class="fw-bold">${item.label}</div>
<div class="text-muted small">${item.desc}</div>
</div>
</div>
${tipsHtml}
</div>
<div class="col-md-8">
${actionHtml}
</div>
</div>`;
if (isImport) {
document.getElementById('step4-form').addEventListener('submit', runImport);
}
}
// ── Import runner ─────────────────────────────────────────────────────────
async function runImport(e) {
e.preventDefault();
const item = wItem;
const fileInput = document.getElementById('step4-file');
const btn = document.getElementById('step4-btn');
const spinner = document.getElementById('step4-spinner');
const resultsDiv = document.getElementById('step4-results');
const summaryDiv = document.getElementById('step4-summary');
const errorsDiv = document.getElementById('step4-errors');
if (!fileInput || !fileInput.files.length) {
if (typeof showWarning === 'function') showWarning('Please select a file first');
return;
}
btn.disabled = true;
if (spinner) spinner.classList.remove('d-none');
resultsDiv.classList.add('d-none');
const formData = new FormData(document.getElementById('step4-form'));
formData.append('__RequestVerificationToken', token);
try {
const response = await fetch(item.endpoint, { method: 'POST', body: formData });
if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);
const ct = response.headers.get('content-type') || '';
if (!ct.includes('application/json')) throw new Error('Unexpected response from server — check the browser console');
const result = await response.json();
displaySuccess(summaryDiv, errorsDiv, resultsDiv, result);
if (result.success) {
if (typeof showSuccess === 'function') showSuccess(result.message || 'Import completed');
} else {
if (typeof showError === 'function') showError(result.message || 'Import completed with errors');
}
} catch (err) {
console.error('[Tools] Import error:', err);
displayError(summaryDiv, errorsDiv, resultsDiv, err.message, err.stack);
if (typeof showError === 'function') showError(err.message);
} finally {
btn.disabled = false;
if (spinner) spinner.classList.add('d-none');
}
}
// ── Result display ────────────────────────────────────────────────────────
function displayError(summaryDiv, errorsDiv, resultsDiv, msg, detail) {
if (summaryDiv) {
summaryDiv.innerHTML = `<div class="alert alert-danger mb-0">
<i class="bi bi-exclamation-triangle me-1"></i>
<strong>Import Failed:</strong> ${msg}
</div>`;
}
if (errorsDiv) {
errorsDiv.innerHTML = detail
? `<details class="mt-2" open>
<summary class="text-danger"><strong>Error Details</strong></summary>
<pre class="mt-2 small bg-light p-2 rounded" style="max-height:300px;overflow-y:auto">${detail}</pre>
</details>`
: '';
}
if (resultsDiv) resultsDiv.classList.remove('d-none');
}
function displaySuccess(summaryDiv, errorsDiv, resultsDiv, result) {
// Normalise both QB-format (importedCount/totalRecords) and
// CSV bulk-import format (successCount/totalRows) so this function
// works regardless of which endpoint was called.
const totalRecords = result.totalRecords ?? result.totalRows ?? 0;
const importedCount = result.importedCount ?? result.successCount ?? 0;
const updatedCount = result.updatedCount ?? 0;
const skippedCount = result.skippedCount ?? 0;
// QB format puts everything in result.errors with a severity field.
// CSV format puts error strings in result.errors and warning strings
// in a separate result.warnings array. Normalise both into one list.
const allMessages = [
...(result.errors || []).map(function (e) {
return typeof e === 'string' ? { severity: 'Error', displayMessage: e } : e;
}),
...(result.warnings || []).map(function (w) {
return typeof w === 'string' ? { severity: 'Warning', displayMessage: w } : w;
}),
];
const errors = allMessages.filter(function (e) { return (e.severity || 'Error') === 'Error'; });
const warnings = allMessages.filter(function (e) { return e.severity === 'Warning'; });
const skipped = allMessages.filter(function (e) { return e.severity === 'Skipped'; });
if (summaryDiv) {
const icon = errors.length > 0
? '<i class="bi bi-exclamation-triangle text-warning me-1"></i>'
: '<i class="bi bi-check-circle text-success me-1"></i>';
summaryDiv.innerHTML = `
<div class="d-flex justify-content-between align-items-start flex-wrap gap-2">
<div>
${icon}<strong>Results</strong> &nbsp;
Total: <strong>${totalRecords}</strong> &nbsp;&nbsp;
Imported: <span class="text-success fw-bold">${importedCount}</span> &nbsp;&nbsp;
${updatedCount > 0 ? `Updated: <span class="text-info fw-bold">${updatedCount}</span> &nbsp;&nbsp;` : ''}
Skipped: <span class="text-warning fw-bold">${skippedCount}</span>
${errors.length > 0 ? ` &nbsp;&nbsp; Errors: <span class="text-danger fw-bold">${errors.length}</span>` : ''}
</div>
<button class="btn btn-outline-secondary btn-sm flex-shrink-0"
onclick="downloadImportReport(this)"
data-result='${JSON.stringify(result).replace(/'/g, '&#39;')}'>
<i class="bi bi-download me-1"></i>Download Report
</button>
</div>`;
}
if (errorsDiv) {
let html = '';
if (errors.length) {
html += `<details class="mt-2" open>
<summary class="text-danger"><strong>${errors.length} Error(s)</strong></summary>
<ul class="mt-1 small mb-0">${errors.map(function (e) {
return '<li>' + (e.displayMessage || e.errorMessage || JSON.stringify(e)) + '</li>';
}).join('')}</ul>
</details>`;
}
if (warnings.length) {
html += `<details class="mt-2">
<summary class="text-warning"><strong>${warnings.length} Warning(s)</strong></summary>
<ul class="mt-1 small mb-0">${warnings.map(function (e) {
return '<li>' + (e.displayMessage || e.errorMessage) + '</li>';
}).join('')}</ul>
</details>`;
}
if (skipped.length) {
html += `<details class="mt-2">
<summary class="text-secondary"><strong>${skipped.length} Skipped</strong></summary>
<ul class="mt-1 small mb-0">${skipped.map(function (e) {
return '<li>' + (e.recordName || '') + (e.errorMessage ? ' — ' + e.errorMessage : '') + '</li>';
}).join('')}</ul>
</details>`;
}
errorsDiv.innerHTML = html;
}
if (resultsDiv) resultsDiv.classList.remove('d-none');
}
// ── Download report (called from inline onclick) ──────────────────────────
window.downloadImportReport = function (btn) {
try {
const result = JSON.parse(btn.getAttribute('data-result'));
const importedCount = result.importedCount ?? result.successCount ?? 0;
const updatedCount = result.updatedCount ?? 0;
const skippedCount = result.skippedCount ?? 0;
const rows = [['Status', 'Record', 'Field', 'Message']];
rows.push(['Imported', importedCount, '', '']);
if (updatedCount > 0) rows.push(['Updated', updatedCount, '', '']);
rows.push(['Skipped', skippedCount, '', '']);
// Normalise errors: QB format = objects, CSV format = plain strings
const allErrors = [
...(result.errors || []).map(function (e) {
return typeof e === 'string' ? { severity: 'Error', recordName: '', fieldName: '', errorMessage: e } : e;
}),
...(result.warnings || []).map(function (w) {
return typeof w === 'string' ? { severity: 'Warning', recordName: '', fieldName: '', errorMessage: w } : w;
}),
];
allErrors.forEach(function (e) {
rows.push([e.severity || 'Error', e.recordName || '', e.fieldName || '', e.errorMessage || '']);
});
const csv = rows.map(function (r) {
return r.map(function (c) { return '"' + String(c).replace(/"/g, '""') + '"'; }).join(',');
}).join('\r\n');
const a = document.createElement('a');
a.href = URL.createObjectURL(new Blob([csv], { type: 'text/csv' }));
a.download = 'import-report-' + new Date().toISOString().slice(0, 10) + '.csv';
a.click();
URL.revokeObjectURL(a.href);
} catch (err) {
console.error('[Tools] Report error:', err);
}
};
})();