Fix preferred powder selection and expand company settings export

- customer-details.js: encode double quotes in JSON.stringify output as " so onclick attributes parse correctly when powder names contain double quotes
- ToolsController: add company_settings CSV to ExportAllCsv ZIP archive (was missing entirely)
- ToolsController: add ~30 missing fields to GenerateCompanySettingsCsv — AccountingMethod, timeclock settings, all shop capability/blast/coat rate fields, complexity surcharge percents, pricing mode, invoice number prefix, email-from fields, per-event notification flags, payment reminder settings, document accent colors/terms/footer notes, kiosk intake output
- Update GenerateCompanySettingsTemplate to match so import template stays in sync with export

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 21:12:49 -04:00
parent 0b839d0746
commit 35264e6b2a
2 changed files with 101 additions and 2 deletions
@@ -2092,6 +2092,27 @@ public class ToolsController : Controller
{
await writer.WriteAsync(purchaseOrdersCsv);
}
// 16. Company Settings
var settingsCompany = await _unitOfWork.Companies.GetByIdAsync(companyId.Value, false,
c => c.OperatingCosts, c => c.Preferences, c => c.PricingTiers);
if (settingsCompany != null)
{
var settingsJobStatuses = await _unitOfWork.JobStatusLookups.FindAsync(s => s.CompanyId == companyId.Value);
var settingsJobPriorities = await _unitOfWork.JobPriorityLookups.FindAsync(p => p.CompanyId == companyId.Value);
var settingsQuoteStatuses = await _unitOfWork.QuoteStatusLookups.FindAsync(s => s.CompanyId == companyId.Value);
var settingsInventoryCategories = await _unitOfWork.InventoryCategoryLookups.FindAsync(c => c.CompanyId == companyId.Value);
var settingsApptStatuses = await _unitOfWork.AppointmentStatusLookups.FindAsync(s => s.CompanyId == companyId.Value);
var settingsApptTypes = await _unitOfWork.AppointmentTypeLookups.FindAsync(t => t.CompanyId == companyId.Value);
var settingsCsv = GenerateCompanySettingsCsv(settingsCompany, settingsJobStatuses, settingsJobPriorities,
settingsQuoteStatuses, settingsInventoryCategories, settingsApptStatuses, settingsApptTypes);
var settingsEntry = archive.CreateEntry($"company_settings_{timestamp}.csv");
using (var entryStream = settingsEntry.Open())
using (var writer = new System.IO.StreamWriter(entryStream))
{
await writer.WriteAsync(settingsCsv);
}
}
}
memoryStream.Position = 0;
@@ -2785,23 +2806,46 @@ public class ToolsController : Controller
sb.AppendLine("State,");
sb.AppendLine("ZipCode,");
sb.AppendLine("TimeZone,America/New_York");
sb.AppendLine("AccountingMethod,Accrual");
sb.AppendLine("TimeclockEnabled,true");
sb.AppendLine("TimeclockAllowMultiplePunchesPerDay,true");
sb.AppendLine("TimeclockAutoClockOutHours,");
sb.AppendLine();
// Operating Costs
sb.AppendLine("[Operating Costs]");
sb.AppendLine("StandardLaborRate,65.00");
sb.AppendLine("LaborCostPerHour,");
sb.AppendLine("AdditionalCoatLaborPercent,30");
sb.AppendLine("OvenOperatingCostPerHour,25.00");
sb.AppendLine("DefaultOvenCycleMinutes,45");
sb.AppendLine("SandblasterCostPerHour,35.00");
sb.AppendLine("CoatingBoothCostPerHour,30.00");
sb.AppendLine("PowderCoatingCostPerSqFt,0.50");
sb.AppendLine("PricingMode,MarkupOnMaterial");
sb.AppendLine("GeneralMarkupPercentage,35");
sb.AppendLine("TargetMarginPercent,0");
sb.AppendLine("TaxPercent,8.5");
sb.AppendLine("ShopSuppliesRate,5");
sb.AppendLine("RushChargeType,Percentage");
sb.AppendLine("RushChargePercentage,25");
sb.AppendLine("RushChargeFixedAmount,0");
sb.AppendLine("ShopMinimumCharge,50.00");
sb.AppendLine("ComplexitySimplePercent,0");
sb.AppendLine("ComplexityModeratePercent,5");
sb.AppendLine("ComplexityComplexPercent,15");
sb.AppendLine("ComplexityExtremePercent,25");
sb.AppendLine("ShopCapabilityTier,Small");
sb.AppendLine("BlastSetupType,SiphonCabinet");
sb.AppendLine("CompressorCfm,0");
sb.AppendLine("BlastNozzleSize,4");
sb.AppendLine("PrimaryBlastSubstrate,Mixed");
sb.AppendLine("BlastRateSqFtPerHourOverride,");
sb.AppendLine("CoatingGunType,Corona");
sb.AppendLine("CoatingRateSqFtPerHourOverride,");
sb.AppendLine("MonthlyRent,0");
sb.AppendLine("MonthlyUtilities,0");
sb.AppendLine("MonthlyBillableHours,160");
sb.AppendLine();
// Preferences
@@ -2813,16 +2857,22 @@ public class ToolsController : Controller
sb.AppendLine("DefaultQuoteValidityDays,30");
sb.AppendLine("QuoteNumberPrefix,QT");
sb.AppendLine("JobNumberPrefix,JOB");
sb.AppendLine("InvoiceNumberPrefix,INV");
sb.AppendLine("UseMetricSystem,false");
sb.AppendLine("DefaultJobPriority,Normal");
sb.AppendLine("RequireCustomerPO,false");
sb.AppendLine("AllowCustomerApproval,true");
sb.AppendLine("DefaultTurnaroundDays,7");
sb.AppendLine("EmailFromAddress,");
sb.AppendLine("EmailFromName,");
sb.AppendLine("EmailNotificationsEnabled,true");
sb.AppendLine("NotifyOnNewJob,true");
sb.AppendLine("NotifyOnNewQuote,true");
sb.AppendLine("NotifyOnJobStatusChange,true");
sb.AppendLine("NotifyOnQuoteApproval,true");
sb.AppendLine("NotifyOnPaymentReceived,true");
sb.AppendLine("PaymentRemindersEnabled,false");
sb.AppendLine("PaymentReminderDays,7,14,30");
sb.AppendLine("QuoteExpiryWarningDays,3");
sb.AppendLine("DueDateWarningDays,2");
sb.AppendLine("MaintenanceAlertDays,7");
@@ -2831,6 +2881,16 @@ public class ToolsController : Controller
sb.AppendLine("LogRetentionDays,90");
sb.AppendLine("AutoArchiveJobsDays,365");
sb.AppendLine("DeletedRecordRetentionDays,30");
sb.AppendLine("QtAccentColor,#374151");
sb.AppendLine("QtDefaultTerms,");
sb.AppendLine("QtFooterNote,");
sb.AppendLine("InAccentColor,#374151");
sb.AppendLine("InDefaultTerms,");
sb.AppendLine("InFooterNote,");
sb.AppendLine("WoAccentColor,#374151");
sb.AppendLine("WoTerms,");
sb.AppendLine("KioskIntakeOutput,Quote");
sb.AppendLine("MigratingFromQuickBooks,false");
sb.AppendLine();
// Pricing Tiers
@@ -2925,6 +2985,10 @@ public class ToolsController : Controller
sb.AppendLine($"State,{EscapeCsv(company.State)}");
sb.AppendLine($"ZipCode,{EscapeCsv(company.ZipCode)}");
sb.AppendLine($"TimeZone,{EscapeCsv(company.TimeZone)}");
sb.AppendLine($"AccountingMethod,{company.AccountingMethod}");
sb.AppendLine($"TimeclockEnabled,{company.TimeclockEnabled.ToString().ToLower()}");
sb.AppendLine($"TimeclockAllowMultiplePunchesPerDay,{company.TimeclockAllowMultiplePunchesPerDay.ToString().ToLower()}");
sb.AppendLine($"TimeclockAutoClockOutHours,{company.TimeclockAutoClockOutHours?.ToString() ?? ""}");
sb.AppendLine();
// Operating Costs
@@ -2933,18 +2997,37 @@ public class ToolsController : Controller
var costs = company.OperatingCosts;
sb.AppendLine("[Operating Costs]");
sb.AppendLine($"StandardLaborRate,{costs.StandardLaborRate}");
sb.AppendLine($"LaborCostPerHour,{costs.LaborCostPerHour?.ToString() ?? ""}");
sb.AppendLine($"AdditionalCoatLaborPercent,{costs.AdditionalCoatLaborPercent}");
sb.AppendLine($"OvenOperatingCostPerHour,{costs.OvenOperatingCostPerHour}");
sb.AppendLine($"DefaultOvenCycleMinutes,{costs.DefaultOvenCycleMinutes}");
sb.AppendLine($"SandblasterCostPerHour,{costs.SandblasterCostPerHour}");
sb.AppendLine($"CoatingBoothCostPerHour,{costs.CoatingBoothCostPerHour}");
sb.AppendLine($"PowderCoatingCostPerSqFt,{costs.PowderCoatingCostPerSqFt}");
sb.AppendLine($"PricingMode,{costs.PricingMode}");
sb.AppendLine($"GeneralMarkupPercentage,{costs.GeneralMarkupPercentage}");
sb.AppendLine($"TargetMarginPercent,{costs.TargetMarginPercent}");
sb.AppendLine($"TaxPercent,{costs.TaxPercent}");
sb.AppendLine($"ShopSuppliesRate,{costs.ShopSuppliesRate}");
sb.AppendLine($"RushChargeType,{EscapeCsv(costs.RushChargeType)}");
sb.AppendLine($"RushChargePercentage,{costs.RushChargePercentage}");
sb.AppendLine($"RushChargeFixedAmount,{costs.RushChargeFixedAmount}");
sb.AppendLine($"ShopMinimumCharge,{costs.ShopMinimumCharge}");
sb.AppendLine($"ComplexitySimplePercent,{costs.ComplexitySimplePercent}");
sb.AppendLine($"ComplexityModeratePercent,{costs.ComplexityModeratePercent}");
sb.AppendLine($"ComplexityComplexPercent,{costs.ComplexityComplexPercent}");
sb.AppendLine($"ComplexityExtremePercent,{costs.ComplexityExtremePercent}");
sb.AppendLine($"ShopCapabilityTier,{costs.ShopCapabilityTier}");
sb.AppendLine($"BlastSetupType,{costs.BlastSetupType}");
sb.AppendLine($"CompressorCfm,{costs.CompressorCfm}");
sb.AppendLine($"BlastNozzleSize,{costs.BlastNozzleSize}");
sb.AppendLine($"PrimaryBlastSubstrate,{costs.PrimaryBlastSubstrate}");
sb.AppendLine($"BlastRateSqFtPerHourOverride,{costs.BlastRateSqFtPerHourOverride?.ToString() ?? ""}");
sb.AppendLine($"CoatingGunType,{costs.CoatingGunType}");
sb.AppendLine($"CoatingRateSqFtPerHourOverride,{costs.CoatingRateSqFtPerHourOverride?.ToString() ?? ""}");
sb.AppendLine($"MonthlyRent,{costs.MonthlyRent}");
sb.AppendLine($"MonthlyUtilities,{costs.MonthlyUtilities}");
sb.AppendLine($"MonthlyBillableHours,{costs.MonthlyBillableHours}");
sb.AppendLine();
}
@@ -2960,16 +3043,22 @@ public class ToolsController : Controller
sb.AppendLine($"DefaultQuoteValidityDays,{prefs.DefaultQuoteValidityDays}");
sb.AppendLine($"QuoteNumberPrefix,{EscapeCsv(prefs.QuoteNumberPrefix)}");
sb.AppendLine($"JobNumberPrefix,{EscapeCsv(prefs.JobNumberPrefix)}");
sb.AppendLine($"InvoiceNumberPrefix,{EscapeCsv(prefs.InvoiceNumberPrefix)}");
sb.AppendLine($"UseMetricSystem,{prefs.UseMetricSystem.ToString().ToLower()}");
sb.AppendLine($"DefaultJobPriority,{EscapeCsv(prefs.DefaultJobPriority)}");
sb.AppendLine($"RequireCustomerPO,{prefs.RequireCustomerPO.ToString().ToLower()}");
sb.AppendLine($"AllowCustomerApproval,{prefs.AllowCustomerApproval.ToString().ToLower()}");
sb.AppendLine($"DefaultTurnaroundDays,{prefs.DefaultTurnaroundDays}");
sb.AppendLine($"EmailFromAddress,{EscapeCsv(prefs.EmailFromAddress)}");
sb.AppendLine($"EmailFromName,{EscapeCsv(prefs.EmailFromName)}");
sb.AppendLine($"EmailNotificationsEnabled,{prefs.EmailNotificationsEnabled.ToString().ToLower()}");
sb.AppendLine($"NotifyOnNewJob,{prefs.NotifyOnNewJob.ToString().ToLower()}");
sb.AppendLine($"NotifyOnNewQuote,{prefs.NotifyOnNewQuote.ToString().ToLower()}");
sb.AppendLine($"NotifyOnJobStatusChange,{prefs.NotifyOnJobStatusChange.ToString().ToLower()}");
sb.AppendLine($"NotifyOnQuoteApproval,{prefs.NotifyOnQuoteApproval.ToString().ToLower()}");
sb.AppendLine($"NotifyOnPaymentReceived,{prefs.NotifyOnPaymentReceived.ToString().ToLower()}");
sb.AppendLine($"PaymentRemindersEnabled,{prefs.PaymentRemindersEnabled.ToString().ToLower()}");
sb.AppendLine($"PaymentReminderDays,{EscapeCsv(prefs.PaymentReminderDays)}");
sb.AppendLine($"QuoteExpiryWarningDays,{prefs.QuoteExpiryWarningDays}");
sb.AppendLine($"DueDateWarningDays,{prefs.DueDateWarningDays}");
sb.AppendLine($"MaintenanceAlertDays,{prefs.MaintenanceAlertDays}");
@@ -2978,6 +3067,16 @@ public class ToolsController : Controller
sb.AppendLine($"LogRetentionDays,{prefs.LogRetentionDays}");
sb.AppendLine($"AutoArchiveJobsDays,{prefs.AutoArchiveJobsDays}");
sb.AppendLine($"DeletedRecordRetentionDays,{prefs.DeletedRecordRetentionDays}");
sb.AppendLine($"QtAccentColor,{EscapeCsv(prefs.QtAccentColor)}");
sb.AppendLine($"QtDefaultTerms,{EscapeCsv(prefs.QtDefaultTerms)}");
sb.AppendLine($"QtFooterNote,{EscapeCsv(prefs.QtFooterNote)}");
sb.AppendLine($"InAccentColor,{EscapeCsv(prefs.InAccentColor)}");
sb.AppendLine($"InDefaultTerms,{EscapeCsv(prefs.InDefaultTerms)}");
sb.AppendLine($"InFooterNote,{EscapeCsv(prefs.InFooterNote)}");
sb.AppendLine($"WoAccentColor,{EscapeCsv(prefs.WoAccentColor)}");
sb.AppendLine($"WoTerms,{EscapeCsv(prefs.WoTerms)}");
sb.AppendLine($"KioskIntakeOutput,{EscapeCsv(prefs.KioskIntakeOutput)}");
sb.AppendLine($"MigratingFromQuickBooks,{prefs.MigratingFromQuickBooks.ToString().ToLower()}");
sb.AppendLine();
}
@@ -111,8 +111,8 @@ function searchInventoryItems(term) {
if (!dropdown) return;
dropdown.innerHTML = data.length === 0
? '<div class="dropdown-item text-muted small">No results</div>'
: data.map(i => `<button type="button" class="dropdown-item small"
onclick="selectPowder(${i.id}, ${JSON.stringify(i.name + (i.colorName ? ' — ' + i.colorName : ''))})">${i.name}${i.colorName ? ' <span class=\'text-muted\'>' + i.colorName + '</span>' : ''} <span class="badge bg-light text-muted border">${i.sku ?? ''}</span></button>`).join('');
: data.map(i => { const label = JSON.stringify(i.name + (i.colorName ? ' — ' + i.colorName : '')).replace(/"/g, '&quot;'); return `<button type="button" class="dropdown-item small"
onclick="selectPowder(${i.id}, ${label})">${i.name}${i.colorName ? ' <span class=\'text-muted\'>' + i.colorName + '</span>' : ''} <span class="badge bg-light text-muted border">${i.sku ?? ''}</span></button>`; }).join('');
dropdown.style.display = 'block';
} catch { /* silent */ }
}, 300);