841 lines
43 KiB
JavaScript
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 & 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 & 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 → Customers → Vendors → Catalog Items → Inventory → Invoices → Transactions → Bills & 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>
|
|
Total: <strong>${totalRecords}</strong>
|
|
Imported: <span class="text-success fw-bold">${importedCount}</span>
|
|
${updatedCount > 0 ? `Updated: <span class="text-info fw-bold">${updatedCount}</span> ` : ''}
|
|
Skipped: <span class="text-warning fw-bold">${skippedCount}</span>
|
|
${errors.length > 0 ? ` 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, ''')}'>
|
|
<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);
|
|
}
|
|
};
|
|
|
|
})();
|