// 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 Pref Vendor', '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 All', '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 All', '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 & Payables → Vendor Balance Detail', 'Set date range to All', 'Click Customize ReportDisplay tab → check Memo 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', 'Or: 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', 'Tip: 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', 'Or: Expenses → Vendors → Export icon (box with arrow) → Export to Excel', 'Upload the .xlsx file below' ] }, { key: 'qbo-products', label: 'Products & 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 Category:Item Name 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 += `
Export All as ZIP

Or pick a specific data set below.

`; } // QB Desktop Import: recommended order callout if (wDir === 'import' && wFmt === 'qb-desktop') { html += `
Recommended order: Chart of Accounts → Customers → Vendors → Catalog Items → Inventory → Invoices → Transactions → Bills & Payments
`; } filteredItems().forEach(function (item) { const badgeHtml = item.badge ? ` ${item.badge}` : ''; html += `
${item.label}${badgeHtml}
${item.desc}
`; }); if (!filteredItems().length) { html += `

No options available for this selection.

`; } 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 ? `
    ${item.tips.map(t => `
  1. ${t}
  2. `).join('')}
` : ''; // Warning const warningHtml = item.warning ? `
${item.warning}
` : ''; // Template download button const templateHtml = item.template ? `
Download CSV Template
` : ''; // Extra account dropdowns let extraFieldsHtml = ''; (item.extraFields || []).forEach(function (f) { const opts = (accountData[f.accountKey] || []) .map(o => ``) .join(''); extraFieldsHtml += `
`; }); let actionHtml; if (isImport) { actionHtml = ` ${warningHtml} ${templateHtml}
${extraFieldsHtml}

`; } else { const exportUrl = getExportUrl(item); const qbHelpHtml = (wFmt === 'qb-desktop' || wFmt === 'qb-online') ? `
Importing into QuickBooks${wFmt === 'qb-online' ? ' Online' : ' Desktop'}: ${wFmt === 'qb-desktop' ? 'File → Utilities → Import → IIF Files → select the downloaded file.' : 'Settings → Import Data → select the data type, then upload the file.'}
` : ''; actionHtml = ` Download ${item.label} ${qbHelpHtml}`; } container.innerHTML = `
${item.label}
${item.desc}
${tipsHtml}
${actionHtml}
`; 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 = `
Import Failed: ${msg}
`; } if (errorsDiv) { errorsDiv.innerHTML = detail ? `
Error Details
${detail}
` : ''; } 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 ? '' : ''; summaryDiv.innerHTML = `
${icon}Results   Total: ${totalRecords}    Imported: ${importedCount}    ${updatedCount > 0 ? `Updated: ${updatedCount}   ` : ''} Skipped: ${skippedCount} ${errors.length > 0 ? `    Errors: ${errors.length}` : ''}
`; } if (errorsDiv) { let html = ''; if (errors.length) { html += `
${errors.length} Error(s)
`; } if (warnings.length) { html += `
${warnings.length} Warning(s)
`; } if (skipped.length) { html += `
${skipped.length} Skipped
`; } 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); } }; })();