- add grouped platform admin hub pages, view models, and shared card UI\n- simplify the super admin nav and dashboard quick links around the new hubs\n- fix the AiQuoteService EstimatedMinutes assignment so the infrastructure project builds cleanly
Two bugs caused AI estimates to collapse to the shop minimum floor:
1. Coating rate with no guard: when a shop hadn't calibrated their
coating gun (rate = 0), the prompt injected '~0 sqft/hr' paired
with 'MUST use shop-specific rates' — Claude returned near-zero
estimatedMinutes, zeroing labor cost and triggering the floor.
Fixed to mirror the existing blast-rate guard: rate=0 now sends
a fallback instruction to use conservative industry-average times.
2. Per-item minutes divided by quantity: both the system prompt and
user prompt explicitly tell Claude to return estimatedMinutes 'per
single item', but CalculatePricingPreview() was dividing by qty
anyway. For qty > 1 this halved (or more) the labor cost, again
pushing toward the floor. Removed the incorrect divide.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Customer validation now accepts Mobile Phone as a valid contact method;
previously only Email or Phone satisfied the requirement
- Create customer hint text updated to match the new validation rule
- Pagination page-size dropdown gets min-width: 5rem so the number has
breathing room from the caret arrow across all paginated lists
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Quotes index now excludes Converted status by default so the list
stays focused on active work. Converted quotes remain accessible via
the status filter dropdown. Selecting any explicit status filter
(including Converted) bypasses the exclusion as expected.
Also consolidated the two GetQuoteStatusLookupsAsync calls into one
at the top of the action since the cache makes it free.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bug 1 — Invoice total didn't match job total for direct jobs:
- Root cause: all three item-save paths in JobsController passed null for
ovenCostId, so FinalPrice/ShopSuppliesAmount were stored without oven cost
while the Details page recalculated live with OvenCostId and showed higher.
- Add OvenBatchCost stored field to Job entity (migration AddJobOvenBatchCost,
default 0 for existing rows).
- Fix Create, Edit, and UpdateItems to pass job.OvenCostId and save OvenBatchCost.
- Fix InvoicesController.Create GET for direct jobs to use stored OvenBatchCost
and ShopSuppliesAmount as separate labeled lines instead of recalculating
shop supplies from scratch (which excluded the oven cost base).
Bug 2 — Quote status stayed Draft after "Send Quote via Email":
- ResendQuote advanced the approval token and sent the email but never
updated the status. Added Draft → Sent advancement (same guard used by
the SMS send path) so the status updates on successful email send.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Added form-text blurbs under every permission checkbox on both pages
so admins know exactly what each permission unlocks at a glance
- Replaced single Accountant default with a full roleDefaults map covering
Viewer, Worker, Accountant, and Manager roles
- Create page applies defaults on load and on role change (fresh form)
- Edit page preserves saved permissions on load; only resets to defaults
when the role is explicitly changed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Settings.cshtml: add Accountant to roles table with description; add
Fine-Grained Permissions subsection with a full table of all 16 permissions
including the new Can Manage Bills & AP and Can Manage Accounting entries
- HelpKnowledgeBase.cs: add Accountant to ROLE AWARENESS section at top;
add Accountant to USER MANAGEMENT roles list with auto-checked permissions
note; add Fine-grained permissions paragraph documenting CanManageBills,
CanManageAccounting, and Accountant role defaults
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- AppConstants: add Accountant to CompanyRoles; add CanManageBills and
CanManageAccounting to Policies
- ApplicationUser: add CanManageBills and CanManageAccounting bool fields
- UserManagementDtos: expose new fields in all three DTOs
- ClaimsPrincipalFactory: emit ManageBills and ManageAccounting claims
- Program.cs: add CanManageBills and CanManageAccounting policies;
update CanManageInvoices, CanViewReports, CanManagePurchaseOrders,
and CanManageVendors to auto-pass for Accountant role
- BillsController: replace CanManageInventory with CanManageBills on
all write actions (correct policy — bills are not inventory)
- BankReconciliationsController: replace CanManageJobs with
CanManageAccounting on write actions
- CompanyUsersController: add Accountant to validCompanyRoles (both
Create/Edit), legacyRole switch, and all permission assignment blocks
- Create/Edit views: add Accountant option to role dropdown; add
CanManageBills and CanManageAccounting checkboxes; JS auto-checks
financial permissions when Accountant role is selected
- Migration AddAccountantRolePermissions: adds columns + backfills
CanManageBills=1 and CanManageAccounting=1 for all CompanyAdmin users
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Reports.cshtml: added AI Payment Risk Prediction, Ask Your Financials,
and Bank Rec Auto-Match subsections under AI-Powered Reports; updated
on-this-page nav with sub-links for all three
- AccountsPayable.cshtml: added Recurring Bill Detection section explaining
pattern cards, frequency types, confidence badges, next expected date,
and the 2-occurrence minimum
- HelpKnowledgeBase.cs: added Recurring Bill Detection to BILLS section;
added AI Payment Risk Prediction and Ask Your Financials to REPORTS
available-reports list; added features 12–15 to AI FEATURES section
(Recurring Detection, Payment Risk, Financial Query, Bank Rec Auto-Match)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Feature 7: Bank Rec Auto-Match — AiSuggestMatches endpoint scores uncleared
transactions vs statement ending balance; AI Auto-Match panel in Reconcile.cshtml
with confidence highlights and Apply All button.
Feature 8: Late Payment Prediction — PredictLatePayments endpoint scores open AR
customers by risk (high/medium/low) using historical avg-days-to-pay + late rate;
rendered as badge table in AR Aging view via ar-aging-ai.js.
Feature 9: Natural Language Financial Queries — FinancialQuery GET page + RunFinancialQuery
POST; 12-month context snapshot pre-loaded; answers grounded in real data with
supporting facts, follow-up suggestions, session history, and example chips.
Feature 10: Recurring Bill Detection — RunRecurringDetection scans 12 months of bills
for vendor payment patterns (monthly/quarterly/annual); card grid view in Bills/RecurringDetection.cshtml
with confidence badges, next-expected-date, and suggested actions.
Supporting: 4 new DTO groups in AccountingAiDtos.cs, 4 method signatures in
IAccountingAiService.cs, 4 implementations in AccountingAiService.cs, 4 new
AiFeatures constants, 2 new Landing page AI report cards.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Templates button sits alongside Board and Blank Work Order in the
Jobs index action bar. Nav item and the now-redundant "& Templates"
section title are removed, trimming one more item from the sidebar.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace button-style strip with proper underline tabs — active side gets
an ember bottom-border matching the sidebar's existing active-link
language. Add scrollbar-gutter: stable so the sidebar interior width
does not shift when the scrollbar appears/disappears between modes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two-tab strip between Dashboard and the Operations section lets users
toggle between the shop-management view and the accounting view.
FOUC-prevention: server-side controller detection stamps data-nav-mode
on <html> before first paint; CSS hides the inactive side instantly.
JS (nav-mode.js) auto-switches to Finance when navigating to an
accounting controller, restores saved preference everywhere else.
Vendors appears in both modes; all 39 nav items carry data-nav tags.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Budgeting:
- Budget + BudgetLine entities with Jan–Dec monthly columns per GL account
- BudgetsController: Index, Create, Edit, SetDefault, Copy, Delete
- Copy action rolls a budget forward to a new fiscal year
- Budget vs. Actual report (BudgetVsActual): compares monthly budget amounts to
real P&L by calling GetProfitAndLossAsync once per month; variance shown as
favorable/unfavorable; year + budget selectors in header
- Views: Budgets/Index, Create, Edit with inline annual totals via budget-edit.js
- Nav link + report card on Landing
Year-End Close:
- YearEndClose entity records each closed year + JE reference for audit trail
- AccountsController.YearEndClose GET (history + form) + CloseYear POST
- Close zeroes all Revenue and Expense/COGS account balances into Retained Earnings
via IAccountBalanceService and posts a supporting JE dated Dec 31
- Idempotency: rejects attempt to close an already-closed year
- Pre-close checklist in view to guide the workflow
- Nav link under Finance
Migration AddBudgetsAndYearEndClose applied
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Invoice Write-Off: WriteOff POST action in InvoicesController posts bad-debt JE
(DR bad debt expense / CR AR), reduces customer balance, marks invoice WrittenOff;
write-off modal added to Invoice Details view with expense account selector
- Fixed Assets: FixedAsset + FixedAssetDepreciationEntry entities with straight-line
depreciation; FixedAssetsController (Index/Create/Edit/Details/PostDepreciation/Delete);
PostDepreciation auto-generates one JE per asset per period, skips already-posted,
fully-depreciated, and disposed assets; full CRUD views + nav link
- Period Locking: Company.BookLockedThrough field; AccountingPeriodValidator static helper;
lock check added to JE Post and Bill Create (blocks backdating into closed periods);
SetPeriodLock action + date picker UI in Company Settings Accounting section
- 1099 Tracking: Is1099Vendor flag on Vendor entity + DTOs; checkbox in Create/Edit views;
TaxReporting1099 report action + view lists payments by year, flags vendors >= $600;
report card added to Reports Landing
- Migration AddFixedAssetsLockAnd1099 applied
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CreditMemosController with Index, Details, Create, Apply, and Void actions.
All business logic (atomic apply transaction, RemainingBalance cap,
customer.CreditBalance adjustment, auto-Paid invoice when BalanceDue hits zero)
mirrors the invoice-centric IssueCreditMemo/ApplyCredit/VoidCreditMemo actions in
InvoicesController but redirects back to the credit memo rather than an invoice.
Views: Index (stats bar, status+search filter, table), Details (two-col layout
with application history table and Bootstrap Apply/Void confirm modals),
Create (customer dropdown, amount, reason, notes, optional expiry).
Apply modal populates amount automatically from min(remaining credit,
invoice balance due) via credit-memo.js data-attribute wiring (no inline scripts).
Nav: Credit Memos added to Billing & Payments section in _Layout.
Build: 0 errors. Unit tests: 200/200.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Drop orphan VendorCreditId1 column from VendorCreditApplications (was
scaffolded by EF because WithMany() lacked inverse navigation name;
fixed WithMany() → WithMany(vc => vc.Applications) in ApplicationDbContext)
- Wire EarlyPaymentDiscount fields through full data path: added
EarlyPaymentDiscountPercent/Days to CreateInvoiceDto, hidden inputs to
Invoice Create view, and JS to populate from customer AJAX response
- Add missing [HttpGet] attribute to TaxRatesController.Index
- Document GenerateNow architecture exception with XML rationale
Migration DropOrphanVendorCreditId1 applied. Build: 0 errors, 168 warnings.
Unit tests: 200/200 passing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- VendorCredit, VendorCreditLineItem, VendorCreditApplication entities
- VendorCreditStatus enum (Open, PartiallyApplied, Applied, Voided)
- Migration AddVendorCredits: three new tables
- IUnitOfWork/UnitOfWork wired with all three repositories
- VendorCreditsController: Index (status tabs), Create, Details, Post, Apply, Void
- Post action: DR AP, CR each expense line (reverses original expense)
- Apply action: links credit to bill, updates Bill.AmountPaid and bill status
- Views: Index (summary cards + table), Create (dynamic line grid), Details (apply panel)
- Nav: Vendor Credits added to Finance section in _Layout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- JournalEntry + JournalEntryLine entities with Draft/Posted/Reversed lifecycle
- JournalEntryStatus enum (Draft, Posted, Reversed)
- Migration AddJournalEntries: two new tables with self-referencing reversal FK
- IUnitOfWork/UnitOfWork wired with JournalEntries + JournalEntryLines repos
- ApplicationDbContext: DbSets, tenant query filters, reversal FK config
- LedgerService: JE lines added as 10th source in GetAccountLedgerAsync and ComputePriorBalanceAsync
- JournalEntriesController: Index (All/Draft/Posted tabs), Create, Details, Post, Reverse, Delete
- Views: Index, Create (dynamic balanced line grid with running debit/credit totals), Details
- journal-entry-create.js: dynamic line management with balance indicator
- Nav: Journal Entries added to Finance section in _Layout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When powder is consumed via a job (JobsController) or scan (InventoryController.LogUsage),
debit the item's CogsAccountId and credit its InventoryAccountId for the cost of the
quantity consumed (using AverageCost if available, else UnitCost). No-op when either
GL account is not configured on the InventoryItem.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- AP Aging report (GetApAgingAsync, controller actions, view, PDF export)
mirrors AR Aging — groups open bills by vendor, buckets by days past due date
- Trial Balance report (GetTrialBalanceAsync, view, PDF export)
uses Account.CurrentBalance, groups by AccountType, validates debits == credits
- Cash vs Accrual accounting method setting on Company entity
switchable at any time — report-time only, no GL re-posting on change
P&L cash: revenue = payments received; expenses = bills/expenses paid in period
Balance Sheet cash: omits AR and AP lines (no receivables/payables concept)
AccountingMethod badge shown on P&L and Balance Sheet views
- Migration A (AddAccountingMethod) applied, default = Accrual for all existing companies
- AP Aging and Trial Balance added to Reports Landing page
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- AccountingDropdownHelper: wired into BillsController and ExpensesController,
replacing 35-40 lines of duplicated DB queries per controller
- AppConstants.StatusCodes: added Job.* and Quote.* constants to replace all
magic status strings across Jobs, Quotes, Appointments, OvenScheduler,
AiQuickQuote, QuoteApproval, and AccountingDropdownHelper
- AccountingRules: extracted IsNormalDebitBalance into shared Infrastructure
helper; removed duplicate private method from AccountBalanceService and
LedgerService (~50 lines deleted)
- AccountDataExportController: extracted 9 Fetch*Async methods (superset of
includes) so Add*Sheet and Build*Csv no longer duplicate DB queries; each
entity is queried once regardless of whether XLSX or CSV format is requested
- BillsController.Create and ExpensesController.Create wrapped in
ExecuteInTransactionAsync; blob uploads moved after commit to keep
financial data atomic and prevent orphaned blobs from rolling back
- Number generators (Appointments, CreditMemo, OvenBatch) fixed from full-table
GetAllAsync to prefix-filtered FindAsync
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Non-terminal jobs scheduled for past dates now appear in a red 'Carried
Over' section at the top of today's board so they can't silently disappear.
Also added alert-permanent to the board tip so the layout doesn't auto-dismiss it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
AccountSubType.Cash was not included in IsNormalDebitBalance in both
AccountBalanceService and LedgerService, causing Cash accounts to be
treated as credit-normal. Payments deposited to a Cash account were
debited in the wrong direction, producing a negative balance.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Quotes: ad-hoc email modal on Quote Details lets staff send to an address not on file;
QuotesController passes overrideEmail through to NotificationService
- Quotes/Details view: SMS consent display, email/SMS send button state based on consent
- Accounting module: AccountingDisplayHelpers for consistent ledger formatting;
AccountsController + Accounts views improvements; AccountingEnums additions
- Bills/Expenses: AI account categorization fixes in BillsController and ExpensesController
- InventoryAiLookupService: TDS cure fallback no longer fires on AiAugmentFromUrl path
(LookupByUrlAsync already has it built in — was double-fetching)
- PdfService: quote/invoice PDF updates
- PricingCalculationService: minor pricing logic fix
- QuoteProfile: mapping updates for new quote fields
- ApplicationDbContextModelSnapshot: catches up to all 4 migrations in this branch
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Voided invoices no longer block creating a new invoice for the same job: voided invoice's
JobId FK is cleared so the unique index slot is freed for the replacement
- Invoice Details view shows voided invoices as history rather than hiding them
- Payment terms: standardized SelectList (Due on Receipt, Net 15/30/45/60/90, 2% 10 Net 30,
COD) with custom-term preservation; invoice-due-date.js auto-updates Due Date on term change
- Shop supplies on direct (no-quote) jobs: InvoicesController derives the shop supplies line
from the company rate when the job has no source quote to read the pre-agreed amount from
- Job entity: ShopSuppliesAmount + ShopSuppliesPercent fields preserved through job lifecycle
- Migration: AddShopSuppliesAmountToJob
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- InventoryItem.IsIncoming: marks powder ordered but not yet received; enables QR code
printing on work orders while the shipment is in transit
- InventoryController.CreateIncomingFromCatalog: POST endpoint creates a 0-balance inventory
record from a PowderCatalogItem and returns it in wizard-compatible shape
- item-wizard.js: custom coat tab now searches the platform powder catalog as a fallback;
catalog results show an 'Add as Incoming Order' option; createIncomingFromCatalog POSTs
to server and selects the new item without a page refresh
- QuoteItemCoatDto: CatalogItemId + AddAsIncoming fields so the wizard can signal server-side
incoming-item creation during quote save
- Inventory Create/Edit/Index views: IsIncoming badge and field
- IInventoryAiLookupService: minor interface update
- Migration: AddInventoryIsIncoming
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Quote entity: ProspectSmsConsent (bool) + ProspectSmsConsentedAt (DateTime?) fields
- QuoteDtos: consent fields on Create/Update/Convert DTOs with TCPA guidance text
- Quote Create/Edit views: SMS consent checkbox shown when mobile number is entered
- Quote ConvertToCustomer view: staff must re-confirm consent carries over to customer record
- QuoteApproval: consent state exposed in ViewModel and ApprovalPage for transparency
- Consent timestamp cleared when prospect quote is linked to an existing customer
- Migration: AddProspectSmsConsent
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Customer entity + DTO: new BillingEmail field (accounting/invoicing address)
- Email fields now accept comma-separated lists; DTO validates each address individually
- NotificationService: SendToEmailListAsync helper fans out to all addresses in a list;
NotifyQuoteSentAsync accepts optional overrideEmail so staff can send to an ad-hoc address
- Migration: AddCustomerBillingEmail
- Customer Create/Edit/Details views updated to show Billing Email field
- customer-billing-email.js: client-side helpers for billing email input
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Prospect quotes have no CustomerId so Customer is null — email is stored
in ProspectEmail directly on the quote. The send-button visibility check
was always seeing null and showing the 'no contact info' warning.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- CreateJobFromQuote now sets QuoteStatusId to CONVERTED after creating the job
- Added ConvertedToJobNumber to QuoteDto, populated in Details action
- 'View Job' button on Quote Details now shows the job number (e.g. 'View Job JOB-2505-0001')
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CustomerEmail, CustomerMobilePhone, CustomerNotifyBySms, and
CustomerNotifyByEmail were added to QuoteDto but never mapped in
QuoteProfile, causing the email/SMS visibility logic on Quote Details
to always see null and show the 'no contact info' warning.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Extract _CompleteJobModal.cshtml partial; Details.cshtml uses PartialAsync
- Job board COMPLETED drop fetches partial via AJAX and shows modal in-place
- Add GET Jobs/CompleteJobModal action to load job data for the board modal
- install-app.js: persist installed state in localStorage; clears automatically when browser re-fires beforeinstallprompt after uninstall
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Dragging a card to the Completed column on the job board previously called
MoveCard directly, skipping email/SMS notifications, CompletedDate, powder
deduction, and the completion modal entirely.
Now detects the COMPLETED status code on the target column, reverts the
visual drag, and navigates to the job Details page with #completeModal in
the hash. Details.cshtml auto-opens the Mark Complete modal on arrival,
so the user goes through the same code path as the Complete Job button.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Quotes Create/Edit: hide 'Send via email' checkbox when customer has no
email; show badge 'send via SMS from details' or 'SMS consent required'
when customer has a mobile number. JS responds to customer dropdown change.
- Quotes Details: hide 'Send Quote via Email' button and approval email
checkbox; hide SMS button when no mobile; show consent-required note.
- Jobs Details (Mark Complete modal): hide email checkbox; show
'SMS notification will be sent' badge or consent-required note.
- Jobs Index (status modal): hide email row when customer has no email.
- Jobs Edit: hide 'Notify customer if status changes' when no email.
- Invoices Details: hide Send/Re-send buttons when no email (vs. disabled).
DTOs: added CustomerEmail + CustomerNotifyByEmail to JobDto/JobListDto;
added CustomerNotifyByEmail/CustomerMobilePhone/CustomerNotifyBySms to
QuoteDto. Mapped in JobProfile and QuotesController customer blocks.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
QuoteDeclinedByCustomer was used for both approve and decline responses,
so approval notifications showed the wrong type in the log. Added a distinct
QuoteApprovedByCustomer = 16 enum value, wired up the correct type in
NotificationService, added default templates in both the service fallback
dictionary and SeedData, and updated placeholder hints in CompanySettings.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The SMS path was sending the message but never updating QuoteStatusId or
SentDate, leaving the quote in Draft. Now mirrors the email send path:
transitions Draft → Sent and stamps SentDate on first send only.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Non-commercial individuals have their full name stored in both CompanyName
and ContactFirstName/ContactLastName, causing the PDF to render the name
twice. Skip the company name line when it matches the assembled contact name.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>