- InventoryController: extract RecordInventoryUsageAsync helper; both LogUsage
(scan page) and LogMaterial (jobs modal, moved from JobsController) call it —
no more duplicate save/GL logic across two controllers
- Log Material modal: replace radio buttons with prominent toggle buttons so the
active mode (Amount Used vs Amount Remaining) is always visually obvious; add
always-visible preview line showing exactly what will be logged before saving
- Edit Usage modal: add quantity field (pre-populated from existing transaction)
with delta adjustment to InventoryItem.QuantityOnHand on save; include
completed/terminal jobs in the dropdown so entries can be corrected after a
job is marked done
- Scan page job picker: include jobs completed within the last 7 days (marked
with '(completed)') so usage can be logged after a job is finished
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously FieldsJson was serialized as an escaped string in the export
file, which was fragile and unreadable. Now parsed into a JsonElement and
embedded as a proper JSON array under the key "fields". Import reads it
back with GetRawText() to reconstruct the stored string. This prevents
the null/empty fields bug caused by manually-edited or round-tripped files.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
System.Text.Json defaults to PascalCase for anonymous types, producing
"Name"/"OutputMode" etc., while the import used TryGetProperty("name")
causing every template to fail with "no name". Adding CamelCase naming
policy aligns the export format with what the import expects.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Export: GET /CompanySettings/ExportCustomItemTemplates downloads all
company templates as an indented JSON backup (strips internal IDs/paths)
- Import: POST /CompanySettings/ImportCustomItemTemplates restores from
that file; runs full field + formula validation, skips name duplicates,
returns per-item results (imported / skipped / errors)
- Unsaved-changes guard: cfModal now intercepts backdrop/ESC/X when the
form is dirty and prompts before discarding work
- Export and Import buttons added to the Custom Formulas card header
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Formula Library ratings: thumbs up/down per company per formula; toggle on/off; sorts by net score; own formulas not rateable; FormulaLibraryRating entity + migration AddFormulaLibraryRatings
- Job Profitability report: actual labor cost (logged hours x StandardLaborRate) vs powder cost vs billed price per job; gross margin % color-coded; time-tracked-only filter; totals footer
- Quote Revision History: track Total price changes on every save; log Sent/Resent events with recipient email; replace flat table with grouped timeline UI (icons per event type, total-change badge on header)
- Setup Wizard: cap CompletedCount at TotalSteps so old 10-step data no longer shows 10/5
- Formula Library card: fix badge overflow on long titles; add Rate: label to make voting buttons discoverable
- Help docs and AI knowledge base updated for all three features
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Normalize IF/Abs/Pow/etc. to lowercase before evaluation so AI-generated
or manually typed uppercase function names no longer cause "Function not
found" errors
- Add NormalizeAndValidate() which normalizes then does a parse-only check
on save — invalid formulas are rejected with a clear error before storing
- Update AI system prompt to list all functions in lowercase and explicitly
call out case-sensitivity; add if() to the supported function list
- Add collapsible NCalc quick-reference panel in the formula editor showing
all operators, functions (lowercase), built-in variables, and an example
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Companies can now share their custom formula templates to a platform-wide
community library. Other tenants can browse, preview, and import formulas
as independent local copies. Includes attribution (source company name),
"Inspired by" lineage for re-contributed formulas, import counts, own-formula
badge, cascade diagram nullification, and AI assistant + help docs updates.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three bugs fixed:
1. Wrong timing — inventory items with IsIncoming=true were auto-created during
quote save (in QuotePricingAssemblyService). Now deferred to quote approval so
inventory only reflects powders the shop is actually going to process.
2. Duplicate records — same powder on multiple items in one quote created multiple
inventory records. Now grouped by PowderCatalogItemId: one record per unique
catalog powder, all matching coats linked to the same record.
3. Wrong category — category resolution used first IsCoating=true by DisplayOrder,
which could land items in Cerakote or other unintended categories. Now prefers
CategoryCode==POWDER explicitly, with DisplayOrder fallback.
Changes:
- QuoteItemCoat: add PowderCatalogItemId int? — persists catalog reference at quote
save time so the approval path knows what to create
- QuotePricingAssemblyService.BuildQuoteItemCoatsAsync: store PowderCatalogItemId
on coat instead of calling CreateIncomingInventoryItemAsync immediately
- QuotePricingAssemblyService.CreateIncomingInventoryItemAsync: signature changed
from (coatDto, companyId) to (catalogItemId, companyId); category lookup prefers
POWDER code; no longer clears PowderCostPerLb on the DTO
- QuotePricingAssemblyService.EnsureIncomingInventoryForApprovedQuoteAsync: new
public method called at approval — loads pending coats, groups by catalog ID,
creates one inventory item per group, links all coats in each group
- IQuotePricingAssemblyService: exposes EnsureIncomingInventoryForApprovedQuoteAsync
- QuotesController.ApproveQuote: calls EnsureIncomingInventory after save
- QuotesController.ChangeQuoteStatus: calls EnsureIncomingInventory on Approved
- QuoteApprovalController: injects IQuotePricingAssemblyService; calls
EnsureIncomingInventory in ApproveInternal (customer-facing portal path)
- InventoryController.CreateIncomingFromCatalog: same category fix (prefers POWDER)
- Migration: AddPowderCatalogItemIdToCoat (nullable int on QuoteItemCoats)
- Tests: updated AddAsIncoming test to verify deferred behavior; new deduplication test
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ClockEntryType enum (Work/Break/Lunch) on EmployeeClockEntry; default 0 = Work
so all existing entries are unaffected
- Migration AddClockEntryType applied
- Break and Lunch buttons on clock status card (only when AllowMultiplePunchesPerDay
is enabled); GoOnBreak closes current Work segment and opens Break/Lunch segment
- Return to Work button when on break/lunch; closes break segment, opens new Work
- Status badges on clock card and Who'\''s In grid: Working / On Break / At Lunch
- Break/Lunch hours excluded from all day totals, week totals, metrics, and CSV
- Manager: Manual Entry modal to create a time entry for any company employee
- Attendance report defaults to current ISO week; Week/Month mode toggle with
auto-submitting dropdowns (last 12 weeks or months); period label shown inline
- Attendance CSV: Type column added; day/week totals blank on Break/Lunch rows;
filename uses period label
- Week subtotal rows suppressed in single-week view (shown in month view only)
- Help article and AI knowledge base updated for all new features
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Settings tab (Company Settings > Timeclock):
- Enable/disable timeclock toggle (hides nav link and attendance report when off)
- Allow multiple clock-ins per day toggle
- Auto clock-out after X hours (auto-closes forgotten open entries on next punch)
- Kiosk devices table: lists activated tablets with name, activated date, last seen;
Deactivate button removes that device's access immediately
Multi-kiosk support (replaces single TimeclockKioskToken on Company):
- New TimeclockKioskDevice entity (one row per tablet, unique token, DeviceName, LastSeenAt)
- KioskActivate GET shows a form for optional device name before activating
- KioskDeactivate POST accepts device ID, deletes specific row (not all devices)
- Kiosk validation (Kiosk, KioskEmployees, KioskPunch) queries device table with
ignoreQueryFilters since no user is logged in on kiosk requests
- LastSeenAt updated on each Kiosk page load
Enforcement:
- ClockIn and KioskPunch both auto-close stale entries if AutoClockOutHours is set
- ClockIn and KioskPunch both block second same-day punch if AllowMultiplePunches=false
- TimeclockEnabled=false hides nav link (SubscriptionMiddleware sets Items key) and
returns Forbid on kiosk punch
- Migration: AddTimeclockSettings (adds 3 columns to Companies, new TimeclockKioskDevices table)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New EmployeeClockEntry entity (facility-level attendance, separate from job time entries)
- KioskPin added to ApplicationUser; TimeclockKioskToken added to Company
- TimeclockController: clock in/out, who's in, 14-day history, manager edit/delete,
tablet kiosk with device-cookie auth, PIN management via Users edit page
- Kiosk UI: employee tile grid + 4-digit PIN pad + auto-detect clock-in vs clock-out
- Attendance report at /Reports/Attendance with weekly subtotal rows
- Payroll CSV export at /Reports/AttendanceCsv (flat, one row per segment)
- AllowCustomFormulas wired through PlatformSubscriptionController + subscription views
- Fix soft-delete bug on CustomItemTemplate (missing HasQueryFilter in OnModelCreating)
- Help article (Help/Timeclock.cshtml) and AI knowledge base updated
- Migrations: AddEmployeeTimeclock, AddTimeclockKioskToken
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Both PricingBreakdown constructions in QuotesController.Details were
missing FacilityOverheadCost and FacilityOverheadRatePerHour mappings,
so the view condition (FacilityOverheadCost > 0) was always false even
though the overhead was correctly stored on the Quote entity and included
in the total. The quote-to-job conversion block already had them; now all
three are consistent.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
DataPurgeController was deleting Jobs without first clearing the nullable
Appointments.JobId FK, causing FK_Appointments_Jobs_JobId violations.
Fix nulls out the FK on any linked appointments before the DELETE runs.
Also applies migration AddAllowCustomFormulas (AllowCustomFormulas column
on SubscriptionPlanConfigs for custom formula pricing feature gating).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Custom powder/incoming powder material cost now flows into a separate
auto-generated 'Custom Powder Order' line item instead of rolling into
individual item prices, so users can add shipping charges before the
customer sees the total. A dashed yellow preview card in the wizard
shows the material cost and lets users edit the total (including shipping)
before saving. After first save the price is user-owned.
Also fixes a fatal CSV import crash when FinalPrice contains a non-numeric
value (e.g. 'false' from a spreadsheet formula): the job CSV importer now
streams rows one at a time with a lenient decimal converter, treating bad
values as $0 with a per-row warning instead of aborting the entire import.
Updated HelpKnowledgeBase.cs and Help articles (Jobs, Quotes) with
Custom Powder Order behavior and a new Data Import / Export section.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Vendor Create: reject duplicate company names (case-insensitive) before
saving; works for both the standalone form and the inline quick-add modal
- _Pagination: define changePageSize() JS function (was called but never
existed, breaking page size dropdown on every paginated list)
- Inventory Label: show bin/location on printed QR code labels
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds an 'All Colors' dropdown to the inventory filter bar populated from
the ColorFamilies values already stored on inventory items. Selecting a
family (e.g. 'Red') returns only items tagged with that family.
Also refactors the 16-branch if/else filter builder into a single
composable predicate, making future filter additions trivial.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
RecommendedMaintenanceIntervalDays was a non-nullable int with [Range(1,3650)],
so submitting the form without filling it in bound to 0 and failed validation.
Made nullable on the entity, both DTOs, and the one controller callsite that
calls .AddDays() (now uses .Value). Migration applied.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix Add Field blanking inputs: cfFields was IIFE-scoped so inline oninput
handlers couldn't reach it; expose cfUpdateField on window
- Fix ManualUnitPrice dropped in buildItemFromData: condition excluded
isCustomFormulaItem, causing FixedRate items to reprice from scratch
- Fix formula card missing on job pages: load CustomFormulaTemplates in
PopulateJobItemDropDownsAsync so Details, EditItems, and Edit all get it;
add customFormulaTemplates + formulaEvalUrl to Details and EditItems pageMeta
- Add NCalc field name validation: client-side inline feedback (is-invalid +
message on oninput) and pre-save sweep; server-side ValidateTemplateFields
on Create and Update; rules: letter-start, letters/digits/underscores only,
no duplicates, "rate" reserved
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduces per-company reusable NCalc2 pricing formula templates for complex
fabricated items (roof curbs, enclosures, welded frames). Templates support
two output modes — FixedRate (formula yields a dollar amount) and SurfaceAreaSqFt
(formula yields sq ft fed into the standard coating engine). Includes:
- CustomItemTemplate entity, migration (AddCustomItemTemplates), IUnitOfWork repo
- IsCustomFormulaItem / CustomItemTemplateId / FormulaFieldValuesJson flags on
QuoteItem, JobItem, CreateQuoteItemDto; mapped in all 3 JobItemAssemblyService
overloads and all existingItemsData JSON projections + pageMeta blocks
- ICustomFormulaAiService / CustomFormulaAiService: Claude-powered formula
generator (natural language + optional diagram image) and NCalc2 evaluator
- CompanySettings CRUD endpoints: GetCustomItemTemplates, Create/Update/Delete,
UploadTemplateDiagram, TemplateDiagram (blob serve), EvaluateFormula, GenerateFormulaFromAi
- Company Settings "Custom Formulas" tab + cfModal + company-settings-custom-formulas.js
- item-wizard.js: formula item type card, renderFormulaFields, wzFormulaRecalc
(live evaluate via POST), collectStep2 formula branch, buildCardHtml / emitHiddenFields
- Formula badge in Quotes/Details and Jobs/Details; AI badge gap fixed in Jobs/Details
- Help article (CustomFormulaTemplates.cshtml), Help Index card, HelpController action,
HelpKnowledgeBase entry; 225/225 unit tests passing
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Vendors can now be tagged with one or more inventory categories (Powder,
Chemical, etc.) via checkboxes on the Create/Edit form. The inventory
Create/Edit vendor dropdown automatically filters to matching vendors when
a category is selected; falls back to all vendors if none are tagged.
Includes migration AddVendorCategories (VendorInventoryCategories join table).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds an embed mode to the Label view (hides standalone nav controls) and
an iframe-based modal on Inventory Details. The modal footer Print button
calls contentWindow.print() so the print dialog opens without spawning a
new window.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a PricingType enum to ReworkRecord (FixedPrice | PerItem), surfaces the
choice in the rework modal on Job Details, and wires the resulting unit/total
price display. Includes migration AddReworkPricingType, updated repository
query for rework history, and help article updates.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
LookupAsync builds SpecPageUrl from the ProductUrlTemplate via TryBuildDirectUrl.
If the template is stored without a scheme the link is scheme-less and browsers
treat it as relative, appending it to the app URL.
The scanned QR URL is always fully-qualified and always the correct product page
(it came from the manufacturer's bag), so use it unconditionally as SpecPageUrl
on the pattern-matched QR path instead of only when SpecPageUrl was null.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Inventory: location filter dropdown + Print Bin page (line #, name, color, SKU)
- Fix: Prismatic Powders QR scan now extracts manufacturer/SKU/color from URL path
and uses full LookupAsync pipeline instead of relying on page fetch alone
- Fix: iOS Safari 'Login / data Zero KB' download -- add OnRejected HTML response to rate limiter
- Fix: mobile session logout -- ConfigureApplicationCookie with 30-day MaxAge persistent cookie
- Help: new 'Location Filtering & Bin Print' section in Inventory help article
- Help: HelpKnowledgeBase updated with bin filter and print bin details
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- PatchItem: add case-insensitive JSON deserialization; add legacy fallback
that computes a live breakdown from job items when PricingBreakdownJson is null
- PatchItem: return itemsSubtotal, subtotalBeforeDiscount, subtotalAfterDiscount,
taxAmount in JSON response for immediate DOM updates
- GetCostingBreakdown: use job.FinalPrice as revenue (not invoice total) so
costing figures reflect inline edits before an invoice exists
- Details.cshtml: add data-pb attributes to visible pricing rows; add
job-final-price-display class to visible Total element
- Details.cshtml: wire afterSave callback to call costing.load() after each edit
- inline-item-edit.js: add afterSave hook in commit(); clean up debug logging
- Help docs: add Inline Price Editing sections to Jobs, Quotes, and Invoices
help articles; add inline editing + job costing revenue notes to AI knowledge base
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Jobs/PatchItem now returns the full breakdown (itemsSubtotal,
subtotalBeforeDiscount, subtotalAfterDiscount, taxAmount) so all rows
in the pricing card update live without a page refresh.
Added data-pb attributes to the matching spans in the pricing panel.
Updated window.inlineItemEdit.totals config for jobs to map each
response key to its DOM selector.
updateTotals in inline-item-edit.js is now fully generic — cfg.totals
keys must match server response property names directly, eliminating
the old hardcoded tax/taxAmount and balance/balanceDue mismatches.
Updated Quote and Invoice configs accordingly (tax→taxAmount,
balance→balanceDue).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
For quote-based jobs, invoice creation now reads fee components (oven,
facility overhead, shop supplies, rush fee) from the job's
PricingBreakdownJson snapshot rather than the source quote. The
FacilityOverheadCost column was added to Quotes in May 2026; older
quotes have 0 there even though overhead was included in their total,
causing invoices to silently drop the overhead charge. The job snapshot
is updated on every save so it always reflects the current pricing.
Tax rate and discount still come from the source quote as agreed terms.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Generates a no-price packing slip (items, color, qty + signature line) via
QuestPDF. New DownloadPackingSlip action reuses existing invoice data pipeline;
Packing Slip button opens inline in a new tab same as Print/PDF.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When voiding an invoice that has non-deposit payments (e.g. CC charges),
those payments are now converted to CRED- Deposit records so the money
trail is preserved and the credit auto-applies to the replacement invoice.
Deposits that were applied to the voided invoice are also re-released so
they can auto-apply again. Void confirmation dialog and success message
both reflect the credit amount when applicable.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When an invoice was voided, deposits auto-applied at invoice creation
kept their AppliedToInvoiceId pointing at the voided invoice. The
replacement invoice lookup (AppliedToInvoiceId == null) skipped them,
so the deposit was never re-applied and the customer was charged in full.
Void now clears AppliedToInvoiceId/AppliedDate on all deposits tied to
the invoice so they're available for the next invoice, and credits the
CustomerDeposits 2300 liability account to restore the balance that was
debited when the deposits were originally applied.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Razor numeric expressions emitted into JS literals (MAX_TOTAL,
SURCHARGE_VALUE) now use InvariantCulture, matching the pattern already
used on the deposit page. Without this, a server culture with comma
decimal separators would silently truncate values like 2.5% to 2.
CustomerEmail removed from PaymentPageViewModel and
DepositPaymentPageViewModel — it was populated from the DB on every
payment page load but never consumed after receipt_email was removed
from the Stripe PaymentIntent.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Allow description, quantity, and price to be edited inline on Quote,
Job, and Invoice details pages without re-opening the wizard. Coating
and prep service rows remain read-only by design. Invoice editing is
gated to Draft/Sent/Overdue statuses; totals update live in the DOM.
Remove receipt_email from Stripe PaymentIntent creation so customers
can use any email they choose at checkout — Stripe validates format
and sends the receipt to whatever the customer enters in the Payment
Element, eliminating the risk of a stored email mismatch blocking a
payment from processing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- /Jobs now redirects to ?statusGroup=active so completed jobs don't clutter the default view
- Add Completed pill (filters Completed + ReadyForPickup + Delivered)
- Pill badge counts are now global DB counts, not page-local item counts
- Ready pill badge now shows ReadyForPickup-only count
- All pill links to ?statusGroup=all to bypass the redirect
- Fix double-encoded & in Completed filter alert label
- Fix corrupted em dash (â€") in Customers/Details billing email fallback text
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Job Details: hide desktop item tables on mobile (d-none d-lg-block) so only
the existing mobile-card layout shows on small screens — prevents 20-line rows
on a narrow phone display
- Invoices Create (ForJob path): load job item coats and derive ColorName from
coat colors when the item itself has no explicit color set; multiple coats join
as 'Color1 / Color2' — lets customers distinguish repeated items (e.g. multiple
caliper sets) on the invoice
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After deleting a job item, the remaining-items DTO projection was missing
NoExtraLayerCharge, causing PricingCalculationService to treat all coats as
extra-charge when recalculating the job total post-delete.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
WizardExistingItems coat serialization in Details GET omitted noExtraLayerCharge,
so editing a line item from the Details page always lost the no-charge flag.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- QuotePricingAssemblyService.BuildQuoteItemCoat: map NoExtraLayerCharge from
CreateQuoteItemCoatDto to QuoteItemCoat on every quote save (was always omitted)
- JobsController.EditItems GET: include NoExtraLayerCharge in coat mapping when
reloading existing items for the wizard (was dropped, causing revert on second edit)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CreateJobDto and UpdateJobDto now carry OvenCostId, OvenBatches, and
OvenCycleMinutes. The Create POST sets these on the new Job entity and
passes them to the pricing engine; the Edit GET populates them from the
existing job so the form reflects saved values, and the Edit POST writes
them back before repricing.
Both Jobs/Create.cshtml and Jobs/Edit.cshtml now include an Oven & Batch
Settings card (matching the quote form) with oven selector, batch count,
and cycle time inputs. The wizard init block now passes the selected
OvenCostId instead of null so live auto-pricing reflects the oven cost.
ViewBag.DefaultOvenCycleMinutes added to PopulateCreateEditWizardViewBagsAsync
so the placeholder in both views shows the company default.
Also fixed: NoExtraLayerCharge was missing from the Edit GET coat DTO
mapping (would have caused the flag to reset to false on next edit).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Edit POST now detects if ScheduledStartTime changed (via previousStart
comparison after AutoMapper merge) and nulls ReminderSentAt so the
background service will fire the reminder again at the new time.
Calendar drag-drop (UpdateEventTime) always clears ReminderSentAt since
rescheduling is its only purpose.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Added explicit CompanyId == companyId predicates to every tenant-scoped
query in 22 controllers so cross-tenant data leakage is impossible even
if EF Core global query filters are bypassed or misconfigured.
Also fixed ApplicationDbContext.IsPlatformAdmin to correctly return true
for SuperAdmins with no CompanyId claim (break-glass accounts) and when
no HTTP context is present (background services, unit tests), resolving
225 unit test failures that stemmed from the global filter blocking all
in-memory test data.
New MultiTenantIsolationTests class (8 tests) verifies the explicit
predicate layer independently of the global query filters.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Powders already assigned to this job's coats appear under a 'This Job'
section header, then a divider, then 'All Inventory' — so the most
relevant choices are always one click away.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Shows manufacturer name as muted secondary text in each dropdown row
and includes it in the search filter, so users can find a powder by
brand when multiple items share a similar name.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
System.Text.Json defaults to PascalCase; JS reads camelCase. Add
JsonNamingPolicy.CamelCase to the InventoryItemsForModal serialization.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The modal was showing one row per coat per item, so a job with 5 items
each with 2 coats of the same powder produced 10 identical input rows.
Now groups by unique InventoryItemId and shows one row per powder color
for the whole job. The controller distributes the entered total across
coats proportionally by their estimated PowderToOrder so per-coat
reporting data is preserved. A single inventory transaction is created
per powder (net of any pre-logged scan credit).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PC users were blocked to QR scan only for logging material usage. Now a
"Log Material" button opens an inline modal with:
- Inventory item dropdown (name + unit of measure, current stock shown on select)
- Entry method toggle: "Amount Used" or "Amount Remaining" (computes used = onHand - remaining)
- Reason: Job Usage or Waste/Spillage
- Notes field
Submits via AJAX to Jobs/LogMaterial (new POST action) which mirrors the
InventoryController.LogUsage flow — updates QuantityOnHand, creates InventoryTransaction,
posts GL entries (DR COGS / CR Inventory). QR scan button retained as icon.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The quote discount must come from the agreed quote price, not the job's pricing
snapshot (which may have DiscountAmount=0 for legacy or unset reasons). The job
snapshot fix only applies to direct jobs where no source quote exists.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- DueDate was computed from DefaultTurnaroundDays (a shop ops setting) instead
of from the payment terms string; now uses PaymentTermsParser throughout
- Discount was never applied for direct jobs (PricingBreakdownJson was read for
fees but DiscountAmount was silently skipped)
- Quote-based jobs used sourceQuote.DiscountAmount, ignoring any discount edits
made to the job after quote conversion; now prefers the job's pricing snapshot
- Payment terms and due date now inherit from sourceQuote.Terms → customer.PaymentTerms
→ company default, so the invoice reflects the agreed or customer-specific terms
- EarlyPaymentDiscount fields now populated from inherited terms
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>