diff --git a/src/PowderCoating.Web/Views/AccountDataExport/Index.cshtml b/src/PowderCoating.Web/Views/AccountDataExport/Index.cshtml index cfe22eb..e69de29 100644 --- a/src/PowderCoating.Web/Views/AccountDataExport/Index.cshtml +++ b/src/PowderCoating.Web/Views/AccountDataExport/Index.cshtml @@ -1,122 +0,0 @@ -@{ - ViewData["Title"] = "Download Your Data"; - ViewData["PageIcon"] = "bi-download"; - Layout = "~/Views/Shared/_Layout.cshtml"; -} - -
- Export a copy of your company's data. Select which data types to include, then choose a format. - Your download will be generated immediately. -
- - @if (TempData["Error"] != null) - { -- - Your export is generated on-demand and delivered directly to your browser. Nothing is stored. -
- -This will recompute every account's balance from transaction history. The page will reload when done.
-Get started quickly by loading a standard chart of accounts
tailored for a powder coating business.
| Number | -Name | -Sub-Type | -Parent | -Status | -Balance | -Actions | -
|---|---|---|---|---|---|---|
- @acct.AccountNumber
- |
- - @acct.Name - @if (acct.IsSystem) - { - sys - } - | -@acct.AccountSubType.ToDisplayName() | -- @if (!string.IsNullOrEmpty(acct.ParentAccountName)) - { - @acct.ParentAccountName - } - | -- @if (acct.IsActive) - { - Active - } - else - { - Inactive - } - | -- @acct.CurrentBalance.ToString("C") - | -- - - - - - - @if (!acct.IsSystem) - { - - } - | -
No accounts found. Use the Seed Data page to generate default accounts.
-- @typeLabel - @Model.AccountSubType.ToDisplayName() · @balanceLabel -
-No transactions found for this account in the selected period.
-| Date | -Reference | -Source | -Description | -Debit | -Credit | -Balance | -
|---|---|---|---|---|---|---|
| @Model.From.ToString("MM/dd/yyyy") | -— | -Opening Balance | -Balance brought forward as of @Model.From.ToString("MMM d, yyyy") | -- | - | - - @Model.OpeningBalance.ToString("C") - - | -
| @entry.Date.ToString("MM/dd/yyyy") | -- @if (entry.LinkController != null && entry.LinkId.HasValue) - { - - @entry.Reference - - } - else - { - @entry.Reference - } - | -- @{ - string sourceBadge = entry.Source switch - { - "Invoice" => "bg-info-subtle text-info", - "Invoice Payment" => "bg-success-subtle text-success", - "Customer Payment" => "bg-success-subtle text-success", - "Bill" => "bg-warning-subtle text-warning", - "Bill Payment" => "bg-danger-subtle text-danger", - "Expense" => "bg-secondary-subtle text-secondary", - "Sales Tax" => "bg-primary-subtle text-primary", - _ => "bg-secondary-subtle text-secondary" - }; - } - @entry.Source - | -@entry.Description | -- @if (entry.Debit > 0) - { - @entry.Debit.ToString("C") - } - | -- @if (entry.Credit > 0) - { - @entry.Credit.ToString("C") - } - | -- - @entry.RunningBalance.ToString("C") - - | -
| Period Totals | -@Model.PeriodDebits.ToString("C") | -@Model.PeriodCredits.ToString("C") | -@Model.ClosingBalance.ToString("C") | -|||
Anthropic API call volume and photo uploads per tenant. Last 30 days unless noted.
-| Company | -Plan | -Today | -7 Days | -30 Days | -All Time | -Photos | -Top Feature (30d) | -Tier | -
|---|---|---|---|---|---|---|---|---|
| - - @row.CompanyName - - @if (!row.IsActive) - { - Inactive - } - | -- - @row.Plan - - | -- @(row.Today > 0 ? row.Today.ToString("N0") : "—") - | -- @(row.Last7Days > 0 ? row.Last7Days.ToString("N0") : "—") - | -- @(row.Last30Days > 0 ? row.Last30Days.ToString("N0") : "—") - | -- @(row.AllTime > 0 ? row.AllTime.ToString("N0") : "—") - | -- @if (row.PhotoCount > 0) - { - @row.PhotoCount.ToString("N0") - } - else - { - — - } - | -- @if (row.TopFeature != null) - { - kv.Value).Select(kv => $"{row.FeatureDisplayName(kv.Key)}: {kv.Value}"))"> - - @row.FeatureDisplayName(row.TopFeature) - @if (row.FeatureBreakdown.Count > 1) - { - - +@(row.FeatureBreakdown.Count - 1) more - - } - - } - else - { - — - } - | -- @row.UsageTier - | -
| - - No AI usage logged yet. Usage data will appear here once tenants start using AI features. - | -||||||||
| Title | -Type | -Target | -Starts | -Expires | -Active | -Dismissible | -- |
|---|---|---|---|---|---|---|---|
| No announcements yet. | |||||||
|
- @a.Title
- @a.Message.Substring(0, Math.Min(60, a.Message.Length))@(a.Message.Length > 60 ? "…" : "")
- |
- @a.Type | -- @if (a.Target == "All") { All } - else if (a.Target == "Plan") { Plan @a.TargetPlan } - else { Co. #@a.TargetCompanyId } - | -@a.StartsAt.Tz(ViewBag.CompanyTimeZone as string).ToString("MM/dd/yy HH:mm") | -@(a.ExpiresAt.HasValue ? a.ExpiresAt.Value.Tz(ViewBag.CompanyTimeZone as string).ToString("MM/dd/yy HH:mm") : "Never") | -- @if (isLive) - { Live } - else if (!a.IsActive) - { Disabled } - else if (a.ExpiresAt.HasValue && a.ExpiresAt < now) - { Expired } - else - { Scheduled } - | -- @(a.IsDismissible - ? Html.Raw("") - : Html.Raw("")) - | -- - - - - - | -
No announcements yet.
- } - @foreach (var a in Model) - { - var now = DateTime.UtcNow; - var isLive = a.IsActive && a.StartsAt <= now && (a.ExpiresAt == null || a.ExpiresAt > now); - string statusBadge, statusText; - if (isLive) { statusBadge = "bg-success"; statusText = "Live"; } - else if (!a.IsActive) { statusBadge = "bg-secondary"; statusText = "Disabled"; } - else if (a.ExpiresAt.HasValue && a.ExpiresAt < now) { statusBadge = "bg-secondary"; statusText = "Expired"; } - else { statusBadge = "bg-warning"; statusText = "Scheduled"; } - - -Record when the customer actually arrived and when the appointment was completed. Useful for tracking punctuality and duration accuracy.
-| Field | -Old Value | -New Value | -
|---|---|---|
| @key | -@(oldVal ?? "—") | -@(newVal ?? "—") | -
@Model.NewValues-
| Timestamp | -Action | -Entity | -Description | -User | -Company | -- |
|---|---|---|---|---|---|---|
| No audit entries found. | ||||||
| @log.Timestamp.Tz(ViewBag.CompanyTimeZone as string).ToString("MM/dd/yy HH:mm:ss") | -@log.Action | -@log.EntityType @log.EntityId | -@log.EntityDescription | -@log.UserName | -@log.CompanyName | -- @if (log.OldValues != null || log.NewValues != null) - { - - - - } - | -
| Date | Reference | Amount | Cleared |
|---|---|---|---|
| No deposits found. | |||
| @item.Date.ToString("MMM d") | -@item.Reference | -@item.Amount.ToString("C") | -- - | -
| Date | Reference | Amount | Cleared |
|---|---|---|---|
| No payments found. | |||
| @item.Date.ToString("MMM d") | -@item.Reference | -@item.Amount.ToString("C") | -- - | -
Statement Date: @Model.StatementDate.ToString("MMMM d, yyyy")
- @if (Model.CompletedAt.HasValue) - { -Completed by @Model.CompletedBy on @Model.CompletedAt.Value.ToLocalTime().ToString("MMM d, yyyy")
- } -| Beginning Balance: | -@Model.BeginningBalance.ToString("C") | -
| + Cleared Deposits: | -@clearedDeposits.Sum(p => p.Amount).ToString("C") | -
| – Cleared Payments: | -@clearedPayments.Sum(p => p.Amount).ToString("C") | -
| Statement Ending Balance: | -@Model.EndingBalance.ToString("C") | -
| Date | Reference | Amount |
|---|---|---|
| @p.PaymentDate.ToString("MMM d") | -@p.Reference | -@p.Amount.ToString("C") | -
| Total | @clearedDeposits.Sum(p=>p.Amount).ToString("C") | |
| Date | Reference | Amount |
|---|---|---|
| @p.Date.ToString("MMM d") | -@p.Reference | -@p.Amount.ToString("C") | -
| Total | @clearedPayments.Sum(p=>p.Amount).ToString("C") | |
| IP Address | -Reason | -Banned | -Expires | -Actions | -
|---|---|---|---|---|
@ban.IpAddress |
- @(ban.Reason ?? "No reason given") | -@ban.BannedAt.ToString("MMM dd, yyyy HH:mm") | -- @if (ban.ExpiresAt.HasValue) - { - @ban.ExpiresAt.Value.ToString("MMM dd, yyyy HH:mm") - } - else - { - Permanent - } - | -
-
-
-
-
- |
-
No active IP bans.
-| IP Address | -Reason | -Banned | -Status | -Actions | -
|---|---|---|---|---|
@ban.IpAddress |
- @(ban.Reason ?? "—") | -@ban.BannedAt.ToString("MMM dd, yyyy") | -- @if (!ban.IsActive) - { - Lifted - } - else - { - Expired - } - | -- - | -
Pre-filled from @fromPoNumber — review and save
- } -Vendor
-@Model.VendorName
- @if (!string.IsNullOrEmpty(Model.VendorEmail)) - {@Model.VendorEmail
} - @if (!string.IsNullOrEmpty(Model.VendorPhone)) - {@Model.VendorPhone
} -Bill Date
-@Model.BillDate.ToString("MMM d, yyyy")
-Due Date
-- @Model.DueDate.Value.ToString("MMM d, yyyy") -
-Vendor Ref #
-@Model.VendorInvoiceNumber
-Terms
-@Model.Terms
-Memo
-@Model.Memo
-| Account | -Description | -Job | -Qty | -Unit Price | -Amount | -
|---|---|---|---|---|---|
| @li.AccountNumber @li.AccountName | -@li.Description | -@li.JobNumber | -@li.Quantity.ToString("G") | -@li.UnitPrice.ToString("C") | -@li.Amount.ToString("C") | -
| Subtotal | -@Model.SubTotal.ToString("C") | -||||
| Tax (@Model.TaxPercent.ToString("G")%) | -@Model.TaxAmount.ToString("C") | -||||
| Total | -@Model.Total.ToString("C") | -||||
| Payment # | -Date | -Method | -Check # | -Bank Account | -Memo | -Amount | -- |
|---|---|---|---|---|---|---|---|
| @pmt.PaymentNumber | -@pmt.PaymentDate.ToString("MMM d, yyyy") | -@pmt.PaymentMethod | -@pmt.CheckNumber | -@pmt.BankAccountName | -@pmt.Memo | -@pmt.Amount.ToString("C") | -- - - | -
@Model.APAccountName
-| Type | -Number | -Vendor | -Memo / Account | -Date | -Due Date | -Status | -Amount | -Balance Due | -- |
|---|---|---|---|---|---|---|---|---|---|
| - @if (entry.EntryType == "Bill") - { - - Bill - - } - else - { - - Expense - - } - | -- @if (entry.EntryType == "Bill") - { - @entry.Number - } - else - { - @entry.Number - } - | -@entry.VendorName | -- @(entry.EntryType == "Bill" ? entry.Memo : entry.AccountName) - @if (entry.HasReceipt) - { - - } - | -@entry.Date.ToString("MMM d, yyyy") | -- @if (entry.DueDate.HasValue) - { - - @entry.DueDate.Value.ToString("MMM d, yyyy") - @if (entry.IsOverdue) { } - - } - else if (entry.EntryType == "Expense") - { - — - } - | -@entry.StatusLabel | -@entry.Total.ToString("C") | -- @(entry.EntryType == "Bill" ? entry.BalanceDue.ToString("C") : "—") - | -- @if (entry.EntryType == "Bill") - { - - } - else - { - - } - | -
Claude analyzes your last 12 months of bills to find recurring payment patterns and help you anticipate upcoming expenses.
-Claude is reviewing your bill history…
-No recurring patterns detected
-Need at least 2 occurrences of a vendor bill at a similar cadence. Add more bill history and try again.
-| Account | -Annual | - @foreach (var m in months) - { -@m | - } -|||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| REVENUE | -|||||||||||||
| - - - - - @line.AccountNumber - @line.AccountName - | -@line.Annual.ToString("N2") | - @for (int m = 0; m < 12; m++) - { -- - | - } -|||||||||||
| EXPENSE | -|||||||||||||
| - - - - - @line.AccountNumber - @line.AccountName - | -@line.Annual.ToString("N2") | - @for (int m = 0; m < 12; m++) - { -- - | - } -|||||||||||
No budgets yet. Create your first budget to start tracking variance against actual results.
-| Budget Name | -Lines | -Total Revenue Budget | -Total Expense Budget | -Default | -- |
|---|---|---|---|---|---|
|
- @b.Name
- @if (!string.IsNullOrWhiteSpace(b.Notes))
- {
- @b.Notes
- }
- |
- @b.Lines.Count | -@b.Lines.Sum(l => l.Annual).ToString("C") | -— | -
- @if (b.IsDefault)
- {
- Default
- }
- else
- {
- |
-
-
-
-
-
-
-
- |
-
No bug reports found.
-| - - Title - - | -- - Priority - - | -- - Status - - | -- - Submitted By - - | -Company | -- - Submitted - - | -- |
|---|---|---|---|---|---|---|
|
- @report.Title
-
- @report.Description
-
- |
- - @{ - var priClass = report.Priority switch - { - BugReportPriority.Critical => "bg-danger", - BugReportPriority.High => "bg-warning text-dark", - BugReportPriority.Normal => "bg-primary", - _ => "bg-secondary" - }; - } - @report.Priority - | -- @{ - var statusClass = report.Status switch - { - BugReportStatus.New => "bg-info text-dark", - BugReportStatus.InProgress => "bg-warning text-dark", - BugReportStatus.Completed => "bg-success", - BugReportStatus.Cancelled => "bg-secondary", - _ => "bg-secondary" - }; - var statusLabel = report.Status switch - { - BugReportStatus.InProgress => "In Progress", - _ => report.Status.ToString() - }; - } - @statusLabel - | -@report.SubmittedByUserName | -@report.CompanyId | -@report.CreatedAt.Tz(ViewBag.CompanyTimeZone as string).ToString("MM/dd/yyyy h:mm tt") | -- - Edit - - | -
Preparing items…
- -- Our AI system reviews every active, priced item in your catalog against your shop's actual operating costs — - labor, oven time, sandblasting, coating booth, and powder material. For each item it estimates a - realistic surface area and processing time, calculates a cost floor, then compares that to your - current price and returns one of four verdicts: -
-- - Results are estimates based on industry knowledge and your shop's rates. Always apply your own - judgment before changing prices. Make sure your - operating costs are up to date for the most accurate results. - Analysis can be run once per quarter. -
-- Click Analyze Catalog with AI above to get started. -
-@item.Reasoning
- -@item.Assumptions
-Map this item to chart-of-account entries for proper revenue and cost tracking. Leave blank to use defaults.
-Map this item to chart-of-account entries for proper revenue and cost tracking. Leave blank to use defaults.
-Current Image
-Total Items
-Active Items
-Average Price
-Categories
-Try adjusting your filters
- - Clear Filters - - } - else - { -Get started by creating your first catalog item
- - Create Your First Item - - } -| Company Name | @Model.CompanyName |
|---|---|
| Code | @(Model.CompanyCode ?? "—") |
| Status | @(Model.IsActive ? "Active" : "Inactive") |
| Time Zone | @(Model.TimeZone ?? "America/New_York") |
| Created | @Model.CreatedAt.ToString("MMM d, yyyy h:mm tt") |
| Created By | @Model.CreatedBy |
| Last Updated | @Model.UpdatedAt.Value.ToString("MMM d, yyyy") |
| Contact Name | @Model.PrimaryContactName |
|---|---|
| @Model.PrimaryContactEmail | |
| Phone | @(Model.Phone ?? "—") |
| Customer | @Model.StripeCustomerId |
|---|---|
| Subscription | @Model.StripeSubscriptionId |
| Name | -Role | -Department | -Status | -Last Login | -Actions | -|
|---|---|---|---|---|---|---|
| - @user.FullName - @if (user.CompanyRole == null) - { - - SuperAdmin - - } - | -@user.Email | -- @if (!string.IsNullOrEmpty(user.CompanyRole)) - { - "bg-primary", - "Manager" => "bg-info", - "Worker" => "bg-secondary", - _ => "bg-light text-dark" - })">@user.CompanyRole.Replace("Company", "") - } - else { N/A } - | -@(user.Department ?? "—") | -- - @(user.IsActive ? "Active" : "Inactive") - - | -- - @(user.LastLoginDate.HasValue - ? user.LastLoginDate.Value.ToString("MMM d, yyyy") - : "Never") - - | -- @if (user.CompanyRole != null) - { - - } - else - { - Platform User - } - | -
No users found for this company.
- - Create First Admin User - -| Plan | -@PlanName(Model.SubscriptionPlan) | -
|---|---|
| Status | -- @{ - var (ssBadge, ssLabel) = Model.SubscriptionStatus switch { - PowderCoating.Core.Enums.SubscriptionStatus.Active => ("bg-success", "Active"), - PowderCoating.Core.Enums.SubscriptionStatus.GracePeriod => ("bg-warning text-dark", "Grace Period"), - PowderCoating.Core.Enums.SubscriptionStatus.Expired => ("bg-danger", "Expired"), - PowderCoating.Core.Enums.SubscriptionStatus.Canceled => ("bg-secondary", "Canceled"), - PowderCoating.Core.Enums.SubscriptionStatus.Inactive => ("bg-secondary", "Inactive"), - _ => ("bg-secondary", Model.SubscriptionStatus.ToString()) - }; - } - @ssLabel - | -
| Start Date | -@Model.SubscriptionStartDate.ToString("MMMM d, yyyy") | -
| End Date | -- @if (Model.SubscriptionEndDate.HasValue) - { - var daysLeft = (int)(Model.SubscriptionEndDate.Value.Date - DateTime.UtcNow.Date).TotalDays; - - @Model.SubscriptionEndDate.Value.ToString("MMMM d, yyyy") - @if (daysLeft >= 0 && daysLeft <= 30) { (in @daysLeft days) } - @if (daysLeft < 0) { (expired @(-daysLeft)d ago) } - - } - else - { - Ongoing - } - | -
| Last Login | -- @if (lastLogin.HasValue) - { - var days = (int)(DateTime.UtcNow - lastLogin.Value).TotalDays; - = 30 ? "text-warning" : "text-success")"> - @lastLogin.Value.ToString("MMM d, yyyy") (@days days ago) - - } - else { Never } - | -
|---|---|
| Jobs (last 30d) | @jobs30 |
| Jobs (last 90d) | @jobs90 |
| Jobs total | @Model.JobCount |
| Customers | @Model.CustomerCount |
| Setup Wizard | -- @if (onboarding.WizardCompleted) - { - Complete - } - else - { - Pending - } - | -
|---|---|
| Milestones | -@onboarding.StepsCompleted / @onboarding.TotalSteps | -
| First Job / Quote | -- @{ - var firstActivity = onboarding.FirstJobCreatedAt ?? onboarding.FirstQuoteCreatedAt; - } - @(firstActivity.HasValue ? firstActivity.Value.ToString("MMM d, yyyy") : "—") - | -
|---|---|
| First Invoice | -@(onboarding.FirstInvoiceCreatedAt.HasValue ? onboarding.FirstInvoiceCreatedAt.Value.ToString("MMM d, yyyy") : "—") | -
| Workflow Completed | -@(onboarding.FirstWorkflowCompletedAt.HasValue ? onboarding.FirstWorkflowCompletedAt.Value.ToString("MMM d, yyyy") : "—") | -
| Widget Dismissed | -@(onboarding.GuidedActivationDismissedAt.HasValue ? onboarding.GuidedActivationDismissedAt.Value.ToString("MMM d, yyyy") : "—") | -
Onboarding data not available.
- } -No risk signals detected.
-- Permanently deletes all business data — customers, jobs, quotes, invoices, inventory, and more. - The company record, users, and settings are preserved. Use this to wipe a migration and start fresh. -
-
- Permanently deletes the company and everything in it. There is no going back.
- @if (Model.UserCount > 0)
- {
-
This company has @Model.UserCount user(s) — remove them first.
- }
-
Override plan-level feature access for this company. Leave blank (—) to inherit from the subscription plan.
-Use this to immediately cut off SMS for a company — for example if they are sending abusive messages or have a billing dispute. This overrides the plan entitlement and the company's own opt-in setting.
-| - - Company Name - - | -Code | -Contact Email | -Phone | -- - Plan - - | -Health | -Users | -Setup Wizard | -- - Status - - | -- - Created - - | -Actions | -
|---|---|---|---|---|---|---|---|---|---|---|
| - @company.CompanyName - @if (isImpersonating) - { - - Active - - } - | -- @if (!string.IsNullOrEmpty(company.CompanyCode)) - { - @company.CompanyCode - } - | -@company.PrimaryContactEmail | -@company.Phone | -- @PlanName(company.SubscriptionPlan) - | -- @{ - var (hBadge, hLabel) = company.HealthRisk switch { - "Healthy" => ("bg-success-subtle text-success-emphasis border border-success-subtle", "Healthy"), - "AtRisk" => ("bg-warning-subtle text-warning-emphasis border border-warning-subtle", "At Risk"), - "Critical" => ("bg-danger-subtle text-danger-emphasis border border-danger-subtle", "Critical"), - "NeverActivated" => ("bg-secondary-subtle text-secondary-emphasis border border-secondary-subtle", "Never Active"), - _ => ("bg-secondary-subtle text-secondary-emphasis border border-secondary-subtle", company.HealthRisk) - }; - } - @hLabel - | -- @company.UserCount - | -- @if (company.WizardCompleted) - { - var tooltip = company.WizardCompletedByName != null - ? $"Completed by {company.WizardCompletedByName}" - + (company.WizardCompletedAt.HasValue - ? $" on {company.WizardCompletedAt.Value.Tz(ViewBag.CompanyTimeZone as string):MMM d, yyyy}" - : "") - : "Completed"; - - Done - - } - else - { - - Pending - - } - | -- @if (company.IsActive) - { - Active - } - else - { - Inactive - } - | -- @company.CreatedAt.ToString("MMM d, yyyy") - | -- - | -
- @if (!string.IsNullOrWhiteSpace(searchTerm))
- {
-
| Company | -Plan | -Risk | -Score | -Last Login | -Jobs 30d | -Jobs 90d | -Total | -Engagement Signals | -Config | -- |
|---|---|---|---|---|---|---|---|---|---|---|
| No companies match the current filter. | ||||||||||
|
-
- @h.CompanyName
- @if (h.IsComped)
- {
- Comped
- }
- @if (!h.IsActive)
- {
- Inactive
- }
-
- @h.PrimaryContactEmail
- |
- @h.PlanDisplayName | -- @RiskLabel(h.RiskLevel) - | -- @if (h.RiskLevel == ChurnRisk.NeverActivated) - { - — - } - else - { - @h.HealthScore - } - | -- @LoginLabel(h.DaysSinceLastLogin) - | -- @if (h.JobsLast30Days > 0) - { @h.JobsLast30Days } - else - { 0 } - | -- @if (h.JobsLast90Days > 0) - { @h.JobsLast90Days } - else - { 0 } - | -@h.TotalJobs | -
- @if (h.RiskSignals.Any())
- {
-
- @foreach (var s in h.RiskSignals)
- {
- @s
- }
-
- }
- else
- {
- All clear
- }
- |
- - @if (h.ConfigHealth.IsHealthy) - { - OK - } - else - { - var configUrl = Url.Action("Details", "Companies", new { id = h.Id }); - i.Title))"> - - @h.ConfigHealth.Issues.Count issue@(h.ConfigHealth.Issues.Count == 1 ? "" : "s") - - } - | -- - - - | -
This action is permanent and cannot be undone.
-- Deleting your account will permanently deactivate access to - @companyName and mark the following data for removal: -
- -{{placeholder}} tokens anywhere in the body.
- Reply STOP to opt out for CTIA compliance.
- - Click any placeholder to copy it to your clipboard, then paste it into the template body. -
-No logo uploaded
- } -Configure your operating costs for accurate job quoting calculations.
- -Used for job costing only — not shown to customers. Leave blank to use the Standard Labor Rate.
-| Role | -Cost Rate / hr | -Fallback if blank | -
|---|---|---|
| Loading... | ||
Percentage added to the calculated item price based on part intricacy. Applied to calculated items only (not catalog, generic, or labor items).
-Tell the AI about your shop so it produces better estimates for your specific work and pricing style.
- -Layer 1 — Pricing config: Your operating costs (labor, equipment, markup) are always injected automatically.
-Layer 2 — Your shop profile: The description you write here is added to every AI analysis, guiding estimates toward your typical work.
-Layer 3 — Automatic learning: Each time your team accepts an AI estimate without changing it, that item is silently added as a calibration example. The AI improves on its own the more you use it.
-- Tell the quoting engine what size shop you're running. This gives the AI context about your capacity - when generating estimates and is used as a fallback when specific equipment rates aren't configured. -
- -- Define each blast rig your shop uses (cabinet, pressure pot, blast room, etc.). - When quoting, workers pick the rig they'll actually use so time estimates are accurate. - Mark one as the Default to pre-select it in AI Photo Quotes. -
-| Name | -Type | -CFM | -Derived Rate | -Status | -Actions | -
|---|
- Add each oven with its rate and capacity. The Oven Scheduler uses capacity and cycle time to plan batches; quotes use the per-hour rate. - If no specific oven is selected on a quote, the Default Oven Rate from Operating Costs is used. -
-| Label | -Rate/hr | -Capacity (sqft) | -Order | -Status | -Actions | -
|---|
Default values used when creating new records.
-Configure default behavior for jobs and the production workflow.
-Control which events trigger email notifications and alert thresholds.
- @if (ViewBag.SmsEnabled == true) - { -When enabled, customers who have given SMS consent will receive text alerts for job status changes (e.g. ready for pickup).
-7,14,30).| Template | -Channel | -Last Modified | -Actions | -
|---|---|---|---|
| @tmpl.DisplayName | -- @if (tmpl.IsEmail) - { - Email - } - else - { - SMS - } - | -- @(tmpl.UpdatedAt?.ToString("MMM d, yyyy h:mm tt") ?? "Using defaults") - | -- - | -
Define how long records are kept before archiving or deletion.
-Customize dropdown values and workflow statuses for your company
- - - - -| - | Display Name | -Status Code | -Color | -Category | -Flags | -Usage | -Actions | -
|---|---|---|---|---|---|---|---|
| - - Loading... - | -|||||||
| - | Display Name | -Priority Code | -Color | -Usage | -Actions | -
|---|---|---|---|---|---|
| - - Loading... - | -|||||
| - | Display Name | -Status Code | -Color | -Business Flags | -Usage | -Actions | -
|---|---|---|---|---|---|---|
| - - Loading... - | -||||||
| Display Name | -Type Code | -Color | -Requires Job | -Active | -Usage | -Actions | -
|---|---|---|---|---|---|---|
|
-
- Loading...
-
- Loading appointment types...
- |
- ||||||
| - | Display Name | -Category Code | -Description | -Usage | -Actions | -
|---|---|---|---|---|---|
| - - Loading... - | -|||||
| - | Service Name | -Description | -Status | -Actions | -
|---|---|---|---|---|
| - - Loading... - | -||||
- Customise how your printable and emailed quote PDFs look. After saving, open any quote and click - Download PDF to preview the result. -
- - -- Customise how your printable and emailed invoice PDFs look. After saving, open any invoice and click - Download PDF to preview the result. -
- - -- Customise the blank work order form your shop prints for walk-in or drop-off customers. - After saving, click Print Blank Work Order from the Jobs page to preview. -
- -
- A platform administrator must set Stripe:ConnectClientId in
- appsettings.json before companies can connect their Stripe accounts.
- The value starts with ca_ and can be found in your
- Stripe Dashboard → Connect → Settings.
-
Connect your Stripe account to start accepting online payments from customers.
-- When a customer completes the intake form, what should be created in the system? -
- -- A draft quote is created and reviewed by staff before work begins. - Best for shops that price after seeing the parts. -
-- A job is created immediately on submission. - Best for shops that price on the spot and want the work order ready right away. -
-- Response time: We typically reply within 1 business day. -
-- You can also email us directly at - support@powdercoatinglogix.com. -
-No contact submissions yet.
-@s.Message
- - @if (!string.IsNullOrWhiteSpace(s.AdminNotes)) - { -| Invoice | -Date Applied | -Amount | -Applied By | -
|---|---|---|---|
| - - @(a.Invoice?.InvoiceNumber ?? $"#{a.InvoiceId}") - - | -@a.AppliedDate.ToLocalTime().ToString("MM/dd/yyyy") | -@a.AmountApplied.ToString("C") | -@(a.AppliedBy?.FullName ?? "—") | -
| Memo # | -Customer | -Amount | -Applied | -Remaining | -Issue Date | -Expires | -Status | -- |
|---|---|---|---|---|---|---|---|---|
| - - @m.MemoNumber - - | -@(string.IsNullOrWhiteSpace(m.Customer?.CompanyName) ? $"{m.Customer?.ContactFirstName} {m.Customer?.ContactLastName}".Trim() : m.Customer.CompanyName) | -@m.Amount.ToString("C") | -@m.AmountApplied.ToString("C") | -- @m.RemainingBalance.ToString("C") - | -@m.IssueDate.ToLocalTime().ToString("MM/dd/yyyy") | -- @(m.ExpiryDate.HasValue ? m.ExpiryDate.Value.ToLocalTime().ToString("MM/dd/yyyy") : "—") - @if (expired) { (Expired) } - | -- @{ - var (badgeClass, badgeLabel) = m.Status switch - { - CreditMemoStatus.Active => ("bg-success-subtle text-success", "Active"), - CreditMemoStatus.PartiallyApplied => ("bg-warning-subtle text-warning", "Partial"), - CreditMemoStatus.FullyApplied => ("bg-secondary-subtle text-secondary", "Applied"), - CreditMemoStatus.Voided => ("bg-danger-subtle text-danger", "Voided"), - _ => ("bg-secondary-subtle text-secondary", m.Status.ToString()) - }; - } - @badgeLabel - | -- Details - | -
| - - Job Number - - | -Description | -- - Status - - | -- - Priority - - | -- - Due Date - - | -- - Price - - | -- - Created - - | -Actions | -
|---|---|---|---|---|---|---|---|
|
-
-
-
-
-
- @job.JobNumber
- |
- @job.Description | -- - @job.StatusDisplayName - - | -- @job.PriorityDisplayName - | -- @if (job.DueDate.HasValue) - { - var overdue = job.DueDate.Value < DateTime.Now - && job.StatusCode != "COMPLETED" - && job.StatusCode != "DELIVERED"; - - @job.DueDate.Value.ToString("MMM dd, yyyy") - @if (overdue) { } - - } - else - { - — - } - | -@job.FinalPrice.ToString("C") | -@job.CreatedAt.ToString("MMM dd, yyyy") | -- - | -
| - - Quote # - - | -- - Status - - | -- - Quote Date - - | -- - Expires - - | -- - Total - - | -Actions | -
|---|---|---|---|---|---|
|
-
-
-
-
-
- @quote.QuoteNumber
- |
- - @{ - var isExpired = quote.IsExpired; - } - @if (isExpired) - { - Expired - } - else - { - - @quote.StatusDisplayName - - } - | -@quote.QuoteDate.ToString("MMM dd, yyyy") | -- @if (quote.ExpirationDate.HasValue) - { - - @quote.ExpirationDate.Value.ToString("MMM dd, yyyy") - @if (isExpired) { } - - } - else - { - — - } - | -@quote.Total.ToString("C") | -- - | -
- Federal law (TCPA) requires explicit prior written or verbal consent before sending SMS messages to a customer. - Before enabling SMS notifications, you must: -
-- Only check the box below after the customer has given consent. - A confirmation text will be sent automatically to verify enrollment. -
-| Company | -Contact | -Phone | -Type | -Balance | -Status | -Notifications | -Actions | -|
|---|---|---|---|---|---|---|---|---|
|
-
-
-
- @{
- var initial = "?";
- if (!string.IsNullOrEmpty(customer.CompanyName) && customer.CompanyName.Length > 0)
- {
- initial = customer.CompanyName.Substring(0, 1).ToUpper();
- }
- else if (!string.IsNullOrEmpty(customer.ContactName) && customer.ContactName.Length > 0)
- {
- initial = customer.ContactName.Substring(0, 1).ToUpper();
- }
- }
- @initial
-
-
-
- @(string.IsNullOrEmpty(customer.CompanyName) ? customer.ContactName ?? "Individual Customer" : customer.CompanyName)
- @if (customer.LastContactDate.HasValue)
- {
- Last contact: @customer.LastContactDate.Value.ToString("MMM dd, yyyy")
- }
- |
- - @if (!string.IsNullOrEmpty(customer.ContactName)) - { - @customer.ContactName - } - else - { - — - } - | -- @if (!string.IsNullOrEmpty(customer.Email)) - { - - @customer.Email - - } - else - { - — - } - | -- @if (!string.IsNullOrEmpty(customer.Phone)) - { - - @customer.Phone - - } - else - { - — - } - | -- @await Html.PartialAsync("_StatusChip", (Kind: customer.IsCommercial ? "cool" : "neutral", Text: customer.IsCommercial ? "Commercial" : "Individual")) - | -- - @customer.CurrentBalance.ToString("C") - - | -- @await Html.PartialAsync("_StatusChip", (Kind: StatusChipHelper.Active(customer.IsActive), Text: customer.IsActive ? "Active" : "Inactive")) - | -- - - - - - - | -- - | -
| Invoice # | -Job # | -Status | -Due Date | -Total | -Balance Due | -Actions | -
|---|---|---|---|---|---|---|
|
-
-
-
-
-
-
-
- @inv.InvoiceNumber
- @inv.InvoiceDate.Tz(ViewBag.CompanyTimeZone as string).ToString("MMM dd, yyyy")
- |
- - @if (inv.JobId.HasValue && !string.IsNullOrEmpty(inv.JobNumber)) - { - - @inv.JobNumber - - } - else - { - No job - } - | -- - @InvoicesController.GetStatusDisplay(inv.Status) - - @if (inv.IsOverdue) - { - Overdue - } - | -- @if (inv.DueDate.HasValue) - { - - @inv.DueDate.Value.Tz(ViewBag.CompanyTimeZone as string).ToString("MMM dd, yyyy") - @if (inv.IsOverdue) - { - - } - - } - else - { - — - } - | -@inv.Total.ToString("C") | -- @if (inv.BalanceDue > 0) - { - @inv.BalanceDue.ToString("C") - } - else - { - Paid - } - | -- - | -
@Model.CustomerName · @Model.From.ToString("MMM d, yyyy") – @Model.To.ToString("MMM d, yyyy")
-| Date | -Type | -Reference | -Description | -Debit | -Credit | -Balance | -
|---|---|---|---|---|---|---|
| @Model.From.AddDays(-1).ToString("MM/dd/yy") | -Opening Balance | -@Model.OpeningBalance.ToString("C") | -||||
| No activity in this period. | -||||||
| @line.Date.ToString("MM/dd/yy") | -- - @line.Type - - | -@line.Reference | -@line.Description | -@(line.Debit.HasValue ? line.Debit.Value.ToString("C") : "") | -@(line.Credit.HasValue ? line.Credit.Value.ToString("C") : "") | -- @line.RunningBalance.ToString("C") - | -
| Closing Balance | -- @Model.ClosingBalance.ToString("C") - | -|||||
- @if (_attnCount > 0) - { - Shop is running hot — @_attnCount item@(_attnCount == 1 ? "" : "s") need attention. - } - else - { - Everything's on track. @Model.TodaysJobsCount job@(Model.TodaysJobsCount == 1 ? "" : "s") scheduled for today. - } -
-No jobs or appointments scheduled for today
-No open bills!
-No invoices yet this month
-| Customer | -Color | -Lbs to Order | -Est. Cost | -- |
|---|---|---|---|---|
| - @line.CustomerName - (@line.JobNumber) - | -- @if (!string.IsNullOrEmpty(line.ColorName)) - {@line.ColorName} - @if (!string.IsNullOrEmpty(line.ColorCode)) - {(@line.ColorCode)} - @if (!string.IsNullOrEmpty(line.Finish)) - {@line.Finish} - | -@line.LbsToOrder.ToString("N2") lbs | -- @if (line.EstCost.HasValue) - {@line.EstCost.Value.ToString("C")} - else - {—} - | -- - | -
| Vendor Total | -@vendorGroup.TotalLbsNeeded.ToString("N2") lbs | -@(vendorGroup.TotalEstCost > 0 ? vendorGroup.TotalEstCost.ToString("C") : "—") | -- | |
| Customer | -Color | -Lbs Ordered | -Est. Cost | -Ordered | -Receive | -
|---|---|---|---|---|---|
| - @line.CustomerName - (@line.JobNumber) - | -- @if (!string.IsNullOrEmpty(line.ColorName)) - {@line.ColorName} - @if (!string.IsNullOrEmpty(line.ColorCode)) - {(@line.ColorCode)} - @if (!string.IsNullOrEmpty(line.Finish)) - {@line.Finish} - | -@line.LbsToOrder.ToString("N2") lbs | -- @if (line.EstCost.HasValue) - {@line.EstCost.Value.ToString("C")} - else - {—} - | -- @if (line.OrderedAt.HasValue) - {@line.OrderedAt.Value.Tz(ViewBag.CompanyTimeZone as string).ToString("MMM d")} - else - {—} - | -
-
-
-
-
- |
-
| Vendor Total | -@vendorGroup.TotalLbsNeeded.ToString("N2") lbs | -@(vendorGroup.TotalEstCost > 0 ? vendorGroup.TotalEstCost.ToString("C") : "—") | -- | ||
Tips shown to users on the dashboard welcome section. Displayed randomly, one per session.
-| # | -Tip Text | -Status | -Added | -- |
|---|---|---|---|---|
| - - No tips found. - @if (string.IsNullOrEmpty(search) && !activeOnly) - { - Add the first one - } - | -||||
| @tip.Id | -- - @tip.TipText - - | -- @if (tip.IsActive) - { - Active - } - else - { - Inactive - } - | -@tip.CreatedAt.ToString("MMM d, yyyy") | -
-
-
-
- |
-
- No tips found. - @if (string.IsNullOrEmpty(search) && !activeOnly) - { - Add the first one - } -
- } - @foreach (var tip in Model) - { - var tipPreview = tip.TipText.Length > 60 ? tip.TipText.Substring(0, 60) + "…" : tip.TipText; -| Company | -Plan | -Status | -Created | -- |
|---|---|---|---|---|
|
- @c.CompanyName
- |
- @c.Plan | -- @if (c.IsActive) - { - Active - } - else - { - Inactive - } - | -@c.CreatedAt.ToString("MM/dd/yyyy") | -- - | -
@TempData["PurgeDetail"]- } - -
| - | Entity | -Total | -0–30d | -30–90d | ->90d | -Oldest | -- - | -
|---|---|---|---|---|---|---|---|
| - - | -@s.Label | -- @if (s.Total > 0) - { - @s.Total - } - else - { - — - } - | -- @if (s.DeletedLast30Days > 0) - { - @s.DeletedLast30Days - } - else { — } - | -- @if (s.Deleted30To90Days > 0) - { - @s.Deleted30To90Days - } - else { — } - | -- @if (s.DeletedOlderThan90Days > 0) - { - @s.DeletedOlderThan90Days - } - else { — } - | -- @(s.OldestDeletion.HasValue ? s.OldestDeletion.Value.ToString("MM/dd/yyyy") : "—") - | -- - | -
| Current Time: | -@Model.CurrentTime.ToString("yyyy-MM-dd HH:mm:ss") | -
|---|---|
| Environment: | -@Model.EnvironmentName | -
| Application Path: | -@Model.ApplicationPath |
-
| Running As: | -@Model.UserIdentity |
-
| Can Write to App Path: | -- @if (Model.CanWriteToAppPath) - { - ✓ YES - } - else - { - ✗ NO - PERMISSION ISSUE - } - | -
|---|---|
| Logs Directory Exists: | -- @if (Model.LogsDirectoryExists) - { - ✓ YES - } - else - { - ✗ NO - } - | -
| Can Write to Logs: | -- @if (Model.CanWriteToLogsPath) - { - ✓ YES - } - else - { - ✗ NO - PERMISSION ISSUE - } - | -
| Logs Path: | -@Model.LogsPath |
-
Status: - @if (Model.LoggingTestSuccess) - { - SUCCESS - } - else - { - FAILED - } -
-Message: @Model.LoggingTestMessage
- @if (Model.LoggingTestSuccess) - { -| File Name | -Size | -Last Modified | -Full Path | -
|---|---|---|---|
@file.Name |
- @(file.Size / 1024.0).ToString("N2") KB | -@file.LastModified.ToString("yyyy-MM-dd HH:mm:ss") | -@file.FullPath |
-
This means logs are not being written. Check the permissions above.
-IIS AppPool\YourAppPoolName on the application folderStep 2 of 3: choose which companies should receive this message.
-| - | Company | -Primary Contact | -Company Admin | -Status | -|
|---|---|---|---|---|---|
| - - | -
- @company.CompanyName
- #@company.CompanyId
- |
- @(string.IsNullOrWhiteSpace(company.PrimaryContactName) ? "—" : company.PrimaryContactName) | -- @if (string.IsNullOrWhiteSpace(company.PrimaryContactEmail)) - { - Missing - } - else - { - @company.PrimaryContactEmail - } - | -
- @(string.IsNullOrWhiteSpace(company.CompanyAdminName) ? "—" : company.CompanyAdminName)
- @if (!string.IsNullOrWhiteSpace(company.CompanyAdminEmail))
- {
- @company.CompanyAdminEmail
- }
- |
- - @if (company.IsActive) - { - Active - } - else - { - Inactive - } - | -